# -*- coding: utf-8 -*- # # Picard, the next-generation MusicBrainz tagger # # Copyright (C) 2017 Sambhav Kothari # Copyright (C) 2018 Wieland Hoffmann # Copyright (C) 2018, 2020-2021, 2023 Laurent Monin # Copyright (C) 2019-2023 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from unittest.mock import MagicMock from PyQt5.QtCore import QUrl from test.picardtestcase import PicardTestCase from picard.acoustid.manager import Submission from picard.metadata import Metadata from picard.webservice import WebService from picard.webservice.api_helpers import ( AcoustIdAPIHelper, APIHelper, MBAPIHelper, build_lucene_query, escape_lucene_query, ) class APITest(PicardTestCase): def setUp(self): super().setUp() base_url = "http://abc.com/v1" self.path = "/test/more/test" self.complete_url = QUrl(base_url + self.path) self.ws = MagicMock(auto_spec=WebService) self.api = APIHelper(self.ws, base_url=base_url) def _test_ws_function_args(self, ws_function): self.assertGreater(ws_function.call_count, 0) self.assertEqual(ws_function.call_args[1]['url'], self.complete_url) def test_get(self): self.api.get(self.path, None) self._test_ws_function_args(self.ws.get_url) def test_post(self): self.api.post(self.path, None, None) self._test_ws_function_args(self.ws.post_url) def test_put(self): self.api.put(self.path, None, None) self._test_ws_function_args(self.ws.put_url) def test_delete(self): self.api.delete(self.path, None) self._test_ws_function_args(self.ws.delete_url) class MBAPITest(PicardTestCase): def setUp(self): super().setUp() self.config = {'server_host': "mb.org", "server_port": 443} self.set_config_values(self.config) self.ws = MagicMock(auto_spec=WebService) self.api = MBAPIHelper(self.ws) def _test_ws_function_args(self, ws_function): self.assertGreater(ws_function.call_count, 0) url = ws_function.call_args[1]['url'] self.assertTrue(url.toString().startswith("https://mb.org/")) self.assertTrue(url.path().startswith("/ws/2/")) def assertInPath(self, ws_function, path): self.assertIn(path, ws_function.call_args[1]['url'].path()) def assertNotInPath(self, ws_function, path): self.assertNotIn(path, ws_function.call_args[1]['url'].path()) def assertInQuery(self, ws_function, argname, value=None): unencoded_query_args = ws_function.call_args[1]['unencoded_queryargs'] self.assertIn(argname, unencoded_query_args) self.assertEqual(value, unencoded_query_args[argname]) def _test_inc_args(self, ws_function, arg_list): self.assertInQuery(self.ws.get_url, 'inc', self.api._make_inc_arg(arg_list)) def test_get_release(self): inc_args_list = ['test'] self.api.get_release_by_id("1", None, inc=inc_args_list) self._test_ws_function_args(self.ws.get_url) self.assertInPath(self.ws.get_url, "/release/1") self._test_inc_args(self.ws.get_url, inc_args_list) def test_get_track(self): inc_args_list = ['test'] self.api.get_track_by_id("1", None, inc=inc_args_list) self._test_ws_function_args(self.ws.get_url) self.assertInPath(self.ws.get_url, "/recording/1") self._test_inc_args(self.ws.get_url, inc_args_list) def test_get_collection(self): inc_args_list = ["releases", "artist-credits", "media"] self.api.get_collection("1", None) self._test_ws_function_args(self.ws.get_url) self.assertInPath(self.ws.get_url, "collection") self.assertInPath(self.ws.get_url, "1/releases") self._test_inc_args(self.ws.get_url, inc_args_list) def test_get_collection_list(self): self.api.get_collection_list(None) self._test_ws_function_args(self.ws.get_url) self.assertInPath(self.ws.get_url, "collection") self.assertNotInPath(self.ws.get_url, "releases") def test_put_collection(self): self.api.put_to_collection("1", ["1", "2", "3"], None) self._test_ws_function_args(self.ws.put_url) self.assertInPath(self.ws.put_url, "collection/1/releases/1;2;3") def test_delete_collection(self): self.api.delete_from_collection("1", ["1", "2", "3", "4"] * 200, None) collection_string = ";".join(["1", "2", "3", "4"] * 100) self._test_ws_function_args(self.ws.delete_url) self.assertInPath(self.ws.delete_url, "collection/1/releases/" + collection_string) self.assertNotInPath(self.ws.delete_url, collection_string + ";" + collection_string) self.assertEqual(self.ws.delete_url.call_count, 2) def test_xml_ratings_empty(self): ratings = dict() xmldata = self.api._xml_ratings(ratings) self.assertEqual( xmldata, '' '' '' '' ) def test_xml_ratings_one(self): ratings = {("recording", 'a'): 1} xmldata = self.api._xml_ratings(ratings) self.assertEqual( xmldata, '' '' '' '20' '' '' ) def test_xml_ratings_multiple(self): ratings = { ("recording", 'a'): 1, ("recording", 'b'): 2, ("nonrecording", 'c'): 3, } xmldata = self.api._xml_ratings(ratings) self.assertEqual( xmldata, '' '' '' '20' '40' '' '' ) def test_xml_ratings_encode(self): ratings = {("recording", ''): 0} xmldata = self.api._xml_ratings(ratings) self.assertEqual( xmldata, '' '' '' '0' '' '' ) def test_xml_ratings_raises_value_error(self): ratings = {("recording", 'a'): 'foo'} self.assertRaises(ValueError, self.api._xml_ratings, ratings) def test_collection_request(self): releases = tuple("r"+str(i) for i in range(13)) generator = self.api._collection_request("test", releases, batchsize=5) batch = next(generator) self.assertEqual(batch, '/collection/test/releases/r0;r1;r2;r3;r4') batch = next(generator) self.assertEqual(batch, '/collection/test/releases/r5;r6;r7;r8;r9') batch = next(generator) self.assertEqual(batch, '/collection/test/releases/r10;r11;r12') with self.assertRaises(StopIteration): next(generator) def test_make_inc_arg(self): result = self.api._make_inc_arg(['b', 'a', '', 1, (), 0]) expected = '1+a+b' self.assertEqual(result, expected) class AcoustdIdAPITest(PicardTestCase): def setUp(self): super().setUp() self.config = {'acoustid_apikey': "apikey"} self.set_config_values(self.config) self.ws = MagicMock(auto_spec=WebService) self.api = AcoustIdAPIHelper(self.ws) self.api.acoustid_host = 'acoustid_host' self.api.acoustid_port = 443 self.api.client_key = "key" self.api.client_version = "ver" def test_encode_acoustid_args_static(self): args = {'a': '1', 'b': 'v a l'} result = self.api._encode_acoustid_args(args) expected = 'a=1&b=v%20a%20l&client=key&clientversion=ver&format=json' self.assertEqual(result, expected) def test_encode_acoustid_args_static_empty(self): args = dict() result = self.api._encode_acoustid_args(args) expected = 'client=key&clientversion=ver&format=json' self.assertEqual(result, expected) def test_submissions_to_args(self): submissions = [ Submission('f1', 1, recordingid='or1', metadata=Metadata(musicip_puid='p1')), Submission('f2', 2, recordingid='or2', metadata=Metadata(musicip_puid='p2')), ] submissions[0].recordingid = 'r1' submissions[1].recordingid = 'r2' result = self.api._submissions_to_args(submissions) expected = { 'user': 'apikey', 'fingerprint.0': 'f1', 'duration.0': '1', 'mbid.0': 'r1', 'puid.0': 'p1', 'fingerprint.1': 'f2', 'duration.1': '2', 'mbid.1': 'r2', 'puid.1': 'p2' } self.assertEqual(result, expected) def test_submissions_to_args_invalid_duration(self): metadata1 = Metadata({ 'title': 'The Track', 'artist': 'The Artist', 'album': 'The Album', 'albumartist': 'The Album Artist', 'tracknumber': '4', 'discnumber': '2', }, length=100000) metadata2 = Metadata({ 'year': '2022' }, length=100000) metadata3 = Metadata({ 'date': '1980-08-30' }, length=100000) metadata4 = Metadata({ 'date': '08-30' }, length=100000) submissions = [ Submission('f1', 500000, recordingid='or1', metadata=metadata1), Submission('f2', 500000, recordingid='or2', metadata=metadata2), Submission('f3', 500000, recordingid='or3', metadata=metadata3), Submission('f4', 500000, recordingid='or4', metadata=metadata4), ] submissions[0].recordingid = 'r1' submissions[1].recordingid = 'r2' submissions[1].recordingid = 'r3' submissions[1].recordingid = 'r4' result = self.api._submissions_to_args(submissions) expected = { 'user': 'apikey', 'fingerprint.0': 'f1', 'duration.0': '500000', 'track.0': metadata1['title'], 'artist.0': metadata1['artist'], 'album.0': metadata1['album'], 'albumartist.0': metadata1['albumartist'], 'trackno.0': metadata1['tracknumber'], 'discno.0': metadata1['discnumber'], 'fingerprint.1': 'f2', 'duration.1': '500000', 'year.1': '2022', 'fingerprint.2': 'f3', 'duration.2': '500000', 'year.2': '1980', 'fingerprint.3': 'f4', 'duration.3': '500000', } self.assertEqual(result, expected) class LuceneHelpersTest(PicardTestCase): def test_escape_lucene_query(self): self.assertEqual('', escape_lucene_query('')) self.assertEqual( '\\+\\-\\&\\|\\!\\(\\)\\{\\}\\[\\]\\^\\"\\~\\*\\?\\:\\\\\\/', escape_lucene_query('+-&|!(){}[]^"~*?:\\/')) def test_build_lucene_query(self): args = { 'title': 'test', 'artist': 'foo:bar', 'tnum': '3' } query = build_lucene_query(args) self.assertEqual('title:(test) artist:(foo\\:bar) tnum:(3)', query)