diff --git a/tests/api/__init__.py b/tests/api/__init__.py index c78f9d3..1e9b6ce 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -17,6 +17,8 @@ from .test_user import UserTestCase from .test_chat import ChatTestCase from .test_search import SearchTestCase from .test_playlist import PlaylistTestCase +from .test_browse import BrowseTestCase +from .test_album_songs import AlbumSongsTestCase def suite(): suite = unittest.TestSuite() @@ -28,6 +30,8 @@ def suite(): suite.addTest(unittest.makeSuite(ChatTestCase)) suite.addTest(unittest.makeSuite(SearchTestCase)) suite.addTest(unittest.makeSuite(PlaylistTestCase)) + suite.addTest(unittest.makeSuite(BrowseTestCase)) + suite.addTest(unittest.makeSuite(AlbumSongsTestCase)) return suite diff --git a/tests/api/apitestbase.py b/tests/api/apitestbase.py index 3d22f22..aa34d2f 100644 --- a/tests/api/apitestbase.py +++ b/tests/api/apitestbase.py @@ -97,7 +97,7 @@ class ApiTestBase(unittest.TestCase): xml = etree.fromstring(rg.data) self.schema.assert_(xml) - if 'status="ok"' in rg.data: + if xml.get('status') == 'ok': self.assertIsNone(error) if tag: self.assertEqual(xml[0].tag, '{{{}}}{}'.format(NS, tag)) diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py new file mode 100644 index 0000000..7319041 --- /dev/null +++ b/tests/api/test_album_songs.py @@ -0,0 +1,120 @@ +#!/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 uuid + +from supysonic.db import Folder, Artist, Album, Track + +from .apitestbase import ApiTestBase + +class AlbumSongsTestCase(ApiTestBase): + # I'm too lazy to write proper tests concerning the data on those endpoints + # Let's just check paramter validation and ensure coverage + + def setUp(self): + super(AlbumSongsTestCase, self).setUp() + + folder = Folder() + folder.name = 'Root' + folder.root = True + folder.path = 'tests/assets' + + artist = Artist() + artist.name = 'Artist' + + album = Album() + album.name = 'Album' + album.artist = artist + + track = Track() + track.title = 'Track' + track.album = album + track.artist = artist + track.disc = 1 + track.number = 1 + track.path = 'tests/assets/empty' + track.folder = folder + track.root_folder = folder + track.duration = 2 + track.bitrate = 320 + track.content_type = 'audio/mpeg' + track.last_modification = 0 + + self.store.add(track) + self.store.commit() + + def test_get_album_list(self): + self._make_request('getAlbumList', error = 10) + self._make_request('getAlbumList', { 'type': 'kraken' }, error = 0) + self._make_request('getAlbumList', { 'type': 'random', 'size': 'huge' }, error = 0) + self._make_request('getAlbumList', { 'type': 'newest', 'offset': 'minus one' }, error = 0) + + types = [ 'random', 'newest', 'highest', 'frequent', 'recent', 'alphabeticalByName', + 'alphabeticalByArtist', 'starred' ] + for t in types: + self._make_request('getAlbumList', { 'type': t }, tag = 'albumList', skip_post = True) + + rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList', skip_post = True) + self.assertEqual(len(child), 10) + rv, child = self._make_request('getAlbumList', { 'type': 'random', 'size': 3 }, tag = 'albumList', skip_post = True) + self.assertEqual(len(child), 3) + + self.store.remove(self.store.find(Folder).one()) + rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList') + self.assertEqual(len(child), 0) + + def test_get_album_list2(self): + self._make_request('getAlbumList2', error = 10) + self._make_request('getAlbumList2', { 'type': 'void' }, error = 0) + self._make_request('getAlbumList2', { 'type': 'random', 'size': 'size_t' }, error = 0) + self._make_request('getAlbumList2', { 'type': 'newest', 'offset': '&v + 2' }, error = 0) + + types = [ 'random', 'newest', 'frequent', 'recent', 'starred', 'alphabeticalByName', 'alphabeticalByArtist' ] + for t in types: + self._make_request('getAlbumList2', { 'type': t }, tag = 'albumList2', skip_post = True) + + rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2', skip_post = True) + self.assertEqual(len(child), 10) + rv, child = self._make_request('getAlbumList2', { 'type': 'random', 'size': 3 }, tag = 'albumList2', skip_post = True) + self.assertEqual(len(child), 3) + + self.store.remove(self.store.find(Track).one()) + self.store.remove(self.store.find(Album).one()) + rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2') + self.assertEqual(len(child), 0) + + def test_get_random_songs(self): + self._make_request('getRandomSongs', { 'size': '8 floors' }, error = 0) + self._make_request('getRandomSongs', { 'fromYear': 'year' }, error = 0) + self._make_request('getRandomSongs', { 'toYear': 'year' }, error = 0) + self._make_request('getRandomSongs', { 'musicFolderId': 'idid' }, error = 0) + self._make_request('getRandomSongs', { 'musicFolderId': uuid.uuid4() }, error = 70) + + rv, child = self._make_request('getRandomSongs', tag = 'randomSongs') + self.assertEqual(len(child), 10) + rv, child = self._make_request('getRandomSongs', { 'size': 3 }, tag = 'randomSongs') + self.assertEqual(len(child), 3) + + fid = self.store.find(Folder).one().id + self._make_request('getRandomSongs', { 'fromYear': -52, 'toYear': '1984', 'genre': 'some cryptic subgenre youve never heard of', 'musicFolderId': fid }, tag = 'randomSongs') + + def test_now_playing(self): + self._make_request('getNowPlaying', tag = 'nowPlaying') + + def test_get_starred(self): + self._make_request('getStarred', tag = 'starred') + + def test_get_starred2(self): + self._make_request('getStarred2', tag = 'starred2') + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py new file mode 100644 index 0000000..17d1826 --- /dev/null +++ b/tests/api/test_browse.py @@ -0,0 +1,188 @@ +#!/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. + +from lxml import etree +import time +import uuid + +from supysonic.db import Folder, Artist, Album, Track + +from .apitestbase import ApiTestBase + +class BrowseTestCase(ApiTestBase): + def setUp(self): + super(BrowseTestCase, self).setUp() + + empty = Folder() + empty.root = True + empty.name = 'Empty root' + empty.path = '/tmp' + self.store.add(empty) + + root = Folder() + root.root = True + root.name = 'Root folder' + root.path = 'tests/assets' + self.store.add(root) + + for letter in 'ABC': + folder = Folder() + folder.name = letter + 'rtist' + folder.path = 'tests/assets/{}rtist'.format(letter) + folder.parent = root + + artist = Artist() + artist.name = letter + 'rtist' + + for lether in 'AB': + afolder = Folder() + afolder.name = letter + lether + 'lbum' + afolder.path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether) + afolder.parent = folder + + album = Album() + album.name = letter + lether + 'lbum' + album.artist = artist + + for num, song in enumerate([ 'One', 'Two', 'Three' ]): + track = Track() + track.disc = 1 + track.number = num + track.title = song + track.duration = 2 + track.album = album + track.artist = artist + track.bitrate = 320 + track.path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song) + track.content_type = 'audio/mpeg' + track.last_modification = 0 + track.root_folder = root + track.folder = afolder + self.store.add(track) + + self.store.commit() + + self.assertEqual(self.store.find(Folder).count(), 11) + self.assertEqual(self.store.find(Folder, Folder.root == True).count(), 2) + self.assertEqual(self.store.find(Artist).count(), 3) + self.assertEqual(self.store.find(Album).count(), 6) + self.assertEqual(self.store.find(Track).count(), 18) + + def test_get_music_folders(self): + # Do not validate against the XSD here, this is the only place where the API should return ids as ints + # all our ids are uuids :/ + rv = self.client.get('/rest/getMusicFolders.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + self.assertEqual(rv.status_code, 200) + xml = etree.fromstring(rv.data) + + NS = '{http://subsonic.org/restapi}' + self.assertEqual(xml.tag, NS + 'subsonic-response') + self.assertEqual(xml.get('status'), 'ok') + self.assertEqual(len(xml), 1) + self.assertEqual(xml[0].tag, NS + 'musicFolders') + self.assertEqual(len(xml[0]), 2) + self.assertSequenceEqual(sorted(self._xpath(xml, './musicFolders/musicFolder/@name')), [ 'Empty root', 'Root folder' ]) + + def test_get_indexes(self): + self._make_request('getIndexes', { 'musicFolderId': 'abcdef' }, error = 0) + self._make_request('getIndexes', { 'musicFolderId': str(uuid.uuid4()) }, error = 70) + self._make_request('getIndexes', { 'ifModifiedSince': 'quoi' }, error = 0) + + rv, child = self._make_request('getIndexes', { 'ifModifiedSince': int(time.time() * 1000 + 1000) }, tag = 'indexes') + self.assertEqual(len(child), 0) + + fid = self.store.find(Folder, Folder.name == 'Empty root').one().id + rv, child = self._make_request('getIndexes', { 'musicFolderId': str(fid) }, tag = 'indexes') + self.assertEqual(len(child), 0) + + rv, child = self._make_request('getIndexes', tag = 'indexes') + self.assertEqual(len(child), 3) + for i, letter in enumerate([ 'A', 'B', 'C' ]): + self.assertEqual(child[i].get('name'), letter) + self.assertEqual(len(child[i]), 1) + self.assertEqual(child[i][0].get('name'), letter + 'rtist') + + def test_get_music_directory(self): + self._make_request('getMusicDirectory', error = 10) + self._make_request('getMusicDirectory', { 'id': 'id' }, error = 0) + self._make_request('getMusicDirectory', { 'id': str(uuid.uuid4()) }, error = 70) + + # should test with folders with both children folders and tracks. this code would break in that case + for f in self.store.find(Folder): + rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory') + self.assertEqual(child.get('id'), str(f.id)) + self.assertEqual(child.get('name'), f.name) + self.assertEqual(len(child), f.children.count() + f.tracks.count()) + for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))): + self.assertEqual(dbc.name, xmlc.get('title')) + self.assertEqual(xmlc.get('artist'), f.name) + self.assertEqual(xmlc.get('parent'), str(f.id)) + for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): + self.assertEqual(t.title, xmlc.get('title')) + self.assertEqual(xmlc.get('parent'), str(f.id)) + + def test_get_artists(self): + # same as getIndexes standard case + # dataset should be improved to have a different directory structure than /root/Artist/Album/Track + + rv, child = self._make_request('getArtists', tag = 'artists') + self.assertEqual(len(child), 3) + for i, letter in enumerate([ 'A', 'B', 'C' ]): + self.assertEqual(child[i].get('name'), letter) + self.assertEqual(len(child[i]), 1) + self.assertEqual(child[i][0].get('name'), letter + 'rtist') + + def test_get_artist(self): + # dataset should be improved to have tracks by a different artist than the album's artist + self._make_request('getArtist', error = 10) + self._make_request('getArtist', { 'id': 'artist' }, error = 0) + self._make_request('getArtist', { 'id': str(uuid.uuid4()) }, error = 70) + + for ar in self.store.find(Artist): + rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist') + self.assertEqual(child.get('id'), str(ar.id)) + self.assertEqual(child.get('albumCount'), str(len(child))) + self.assertEqual(len(child), ar.albums.count()) + for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))): + self.assertEqual(dal.name, xal.get('name')) + self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset + self.assertEqual(xal.get('artistId'), str(ar.id)) # see above + + def test_get_album(self): + self._make_request('getAlbum', error = 10) + self._make_request('getAlbum', { 'id': 'nastynasty' }, error = 0) + self._make_request('getAlbum', { 'id': str(uuid.uuid4()) }, error = 70) + + a = self.store.find(Album)[0] + rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album') + self.assertEqual(child.get('id'), str(a.id)) + self.assertEqual(child.get('songCount'), str(len(child))) + + self.assertEqual(len(child), a.tracks.count()) + for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): + self.assertEqual(dal.title, xal.get('title')) + self.assertEqual(xal.get('album'), a.name) + self.assertEqual(xal.get('albumId'), str(a.id)) + + def test_get_song(self): + self._make_request('getSong', error = 10) + self._make_request('getSong', { 'id': 'nastynasty' }, error = 0) + self._make_request('getSong', { 'id': str(uuid.uuid4()) }, error = 70) + + s = self.store.find(Track)[0] + self._make_request('getSong', { 'id': str(s.id) }, tag = 'song') + + def test_get_videos(self): + self._make_request('getVideos', error = 0) + +if __name__ == '__main__': + unittest.main() +