import json import logging import re from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Generator, Tuple import pytest from sublime.adapters.subsonic import ( api_objects as SubsonicAPI, SubsonicAdapter, ) MOCK_DATA_FILES = Path(__file__).parent.joinpath("mock_data") @pytest.fixture def adapter(tmp_path: Path): adapter = SubsonicAdapter( { "server_address": "http://subsonic.example.com", "username": "test", "password": "testpass", }, tmp_path, ) adapter._is_mock = True yield adapter def mock_data_files( request_name: str, mode: str = "r" ) -> Generator[Tuple[Path, Any], None, None]: """ Yields all of the files in the mock_data directory that start with ``request_name``. """ for file in MOCK_DATA_FILES.iterdir(): if file.name.split("-")[0] in request_name: with open(file, mode) as f: yield file, f.read() def mock_json(**obj: Any) -> str: return json.dumps( {"subsonic-response": {"status": "ok", "version": "1.15.0", **obj}} ) def camel_to_snake(name: str) -> str: name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() def test_request_making_methods(adapter: SubsonicAdapter): expected = { "u": "test", "p": "testpass", "c": "Sublime Music", "f": "json", "v": "1.15.0", } assert sorted(expected.items()) == sorted(adapter._get_params().items()) assert adapter._make_url("foo") == "http://subsonic.example.com/rest/foo.view" def test_can_service_requests(adapter: SubsonicAdapter): # Mock a connection error adapter._set_mock_data(Exception()) assert adapter.can_service_requests is False # Simulate some sort of ping error for filename, data in mock_data_files("ping_failed"): logging.info(filename) logging.debug(data) adapter._set_mock_data(data) assert adapter.can_service_requests is False # Simulate valid ping adapter._set_mock_data(mock_json()) assert adapter.can_service_requests is True def test_get_playlists(adapter: SubsonicAdapter): expected = [ SubsonicAPI.Playlist( id="2", name="Test", song_count=132, duration=timedelta(seconds=33072), created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=timezone.utc), changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=timezone.utc), comment="Foo", owner="foo", public=True, cover_art="pl-2", ), SubsonicAPI.Playlist( id="3", name="Bar", song_count=23, duration=timedelta(seconds=847), created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=timezone.utc), changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=timezone.utc), comment="", owner="foo", public=False, cover_art="pl-3", ), ] for filename, data in mock_data_files("get_playlists"): logging.info(filename) logging.debug(data) adapter._set_mock_data(data) assert adapter.get_playlists() == expected # When playlists is null, expect an empty list. adapter._set_mock_data(mock_json()) assert adapter.get_playlists() == [] def test_get_playlist_details(adapter: SubsonicAdapter): for filename, data in mock_data_files("get_playlist_details"): logging.info(filename) logging.debug(data) adapter._set_mock_data(data) playlist_details = adapter.get_playlist_details("2") # Make sure that the song count is correct even if it's not provided. # Old versions of Subsonic don't have these properties. assert len(playlist_details.songs) == 2 assert playlist_details.duration == timedelta(seconds=470) # Make sure that at least the first song got decoded properly. assert playlist_details.songs[0] == SubsonicAPI.Song( id="202", parent="318", title="What a Beautiful Name", album="What a Beautiful Name - Single", artist="Hillsong Worship", track=1, year=2016, genre="Christian & Gospel", cover_art="318", size=8381640, content_type="audio/mp4", suffix="m4a", transcoded_content_type="audio/mpeg", transcoded_suffix="mp3", duration=timedelta(seconds=238), bit_rate=256, path="/".join( ( "Hillsong Worship", "What a Beautiful Name - Single", "01 What a Beautiful Name.m4a", ) ), is_video=False, play_count=20, disc_number=1, created=datetime(2020, 3, 27, 5, 17, 7, tzinfo=timezone.utc), album_id="48", artist_id="38", type=SubsonicAPI.SublimeAPI.MediaType.MUSIC, )