Files
sublime-music/tests/adapter_tests/subsonic_adapter_tests.py
2020-09-30 09:27:34 -06:00

570 lines
18 KiB
Python

import hashlib
import json
import logging
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Generator, List, Tuple
import pytest
from dateutil.tz import tzutc
from sublime_music.adapters import ConfigurationStore
from sublime_music.adapters.subsonic import api_objects as SubsonicAPI, SubsonicAdapter
MOCK_DATA_FILES = Path(__file__).parent.joinpath("mock_data")
@pytest.fixture
def adapter(tmp_path: Path):
ConfigurationStore.MOCK = True
config = ConfigurationStore(
server_address="https://subsonic.example.com",
username="test",
verify_cert=True,
salt_auth=False,
)
config.set_secret("password", "testpass")
adapter = SubsonicAdapter(config, tmp_path)
adapter._is_mock = True
yield adapter
adapter.shutdown()
@pytest.fixture
def salt_auth_adapter(tmp_path: Path):
ConfigurationStore.MOCK = True
config = ConfigurationStore(
server_address="https://subsonic.example.com",
username="test",
verify_cert=True,
salt_auth=True,
)
config.set_secret("password", "testpass")
adapter = SubsonicAdapter(config, tmp_path)
adapter._is_mock = True
yield adapter
adapter.shutdown()
def mock_data_files(
request_name: str, mode: str = "r"
) -> Generator[Tuple[Path, Any], None, None]:
"""
Yields all of the files, and each of the elements of in the file (separated by a
line of ='s), for all files in the mock_data directory that start with
``request_name``. This only works for text such as JSON.
"""
sep_re = re.compile(r"=+\n")
num_files = 0
for file in MOCK_DATA_FILES.iterdir():
if file.name.split("-")[0] == request_name:
with open(file, mode) as f:
parts: List[str] = []
aggregate: List[str] = []
for line in f:
if sep_re.match(line):
parts.append("\n".join(aggregate))
aggregate = []
continue
aggregate.append(line)
parts.append("\n".join(aggregate))
print(file) # noqa: T001
num_files += 1
yield file, iter(parts)
# Make sure that is at least one test file
assert num_files > 0
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_config_form():
# Just make sure that the functions work. That's half of the battle.
config_store = ConfigurationStore()
SubsonicAdapter.get_configuration_form(config_store)
def test_plain_auth_logic(adapter: SubsonicAdapter):
expected = {
"u": "test",
"p": "testpass",
"c": "Sublime Music",
"f": "json",
"v": "1.8.0",
}
assert sorted(expected.items()) == sorted(adapter._get_params().items())
def test_salt_auth_logic(salt_auth_adapter: SubsonicAdapter):
expected = {"u": "test", "c": "Sublime Music", "f": "json", "v": "1.8.0"}
params = salt_auth_adapter._get_params()
assert "p" not in params
assert "s" in params
salt = params["s"]
assert "t" in params
assert params["t"] == hashlib.md5(f"testpass{salt}".encode()).hexdigest()
assert all(key in params and params[key] == expected[key] for key in expected)
def test_migrate_configuration_populate_salt_auth():
config = ConfigurationStore(
server_address="https://subsonic.example.com",
username="test",
verify_cert=True,
)
SubsonicAdapter.migrate_configuration(config)
assert "salt_auth" in config
assert config["salt_auth"]
def test_migrate_configuration_salt_auth_present():
config = ConfigurationStore(
server_address="https://subsonic.example.com",
username="test",
verify_cert=True,
salt_auth=False,
)
SubsonicAdapter.migrate_configuration(config)
assert "salt_auth" in config
assert not config["salt_auth"]
def test_make_url(adapter: SubsonicAdapter):
assert adapter._make_url("foo") == "https://subsonic.example.com/rest/foo.view"
def test_ping_status(adapter: SubsonicAdapter):
# Mock a connection error
adapter._set_mock_data(Exception())
assert not adapter.ping_status
# 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 not adapter.ping_status
# Simulate valid ping
adapter._set_mock_data(mock_json())
adapter._last_ping_timestamp.value = 0.0
adapter._set_ping_status()
assert adapter.ping_status
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=tzutc()),
changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=tzutc()),
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=tzutc()),
changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=tzutc()),
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() == sorted(expected, key=lambda e: e.name)
# 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_id="318",
title="What a Beautiful Name",
_album="What a Beautiful Name - Single",
album_id="48",
_artist="Hillsong Worship",
artist_id="38",
track=1,
year=2016,
_genre="Christian & Gospel",
cover_art="318",
size=8381640,
duration=timedelta(seconds=238),
path="/".join(
(
"Hillsong Worship",
"What a Beautiful Name - Single",
"01 What a Beautiful Name.m4a",
)
),
disc_number=1,
)
def test_create_playlist(adapter: SubsonicAdapter):
for filename, data in mock_data_files("create_playlist"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
adapter.create_playlist(
name="Foo",
songs=[
SubsonicAPI.Song(
id="202",
parent_id="318",
title="What a Beautiful Name",
_album="What a Beautiful Name - Single",
album_id="48",
_artist="Hillsong Worship",
artist_id="38",
track=1,
year=2016,
_genre="Christian & Gospel",
cover_art="318",
duration=timedelta(seconds=238),
path="/".join(
(
"Hillsong Worship",
"What a Beautiful Name - Single",
"01 What a Beautiful Name.m4a",
)
),
disc_number=1,
)
],
)
def test_update_playlist(adapter: SubsonicAdapter):
for filename, data in mock_data_files("update_playlist"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
result_playlist = adapter.update_playlist(
"1", name="Foo", comment="Bar", public=True, song_ids=["202"]
)
assert result_playlist.comment == "Bar"
assert result_playlist.public is False
def test_get_song_details(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_song_details"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
song = adapter.get_song_details("1")
assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
"1",
"Sweet Caroline",
2017,
"544",
timedelta(seconds=203),
)
assert song.path and song.path.endswith("Sweet Caroline.mp3")
assert song.parent_id == "544"
assert song.artist
assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
assert song.album
assert (song.album.id, song.album.name) == ("88", "50th Anniversary Collection")
assert song.genre and song.genre.name == "Pop"
def test_get_song_details_missing_data(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_song_details_no_albumid"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
song = adapter.get_song_details("1")
assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
"1",
"Sweet Caroline",
2017,
"544",
timedelta(seconds=203),
)
assert song.path and song.path.endswith("Sweet Caroline.mp3")
assert song.parent_id == "544"
assert song.artist
assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
assert song.album
assert (song.album.id, song.album.name) == (None, "50th Anniversary Collection")
assert song.genre and song.genre.name == "Pop"
for filename, data in mock_data_files("get_song_details_no_artistid"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
song = adapter.get_song_details("1")
assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
"1",
"Sweet Caroline",
2017,
"544",
timedelta(seconds=203),
)
assert song.path and song.path.endswith("Sweet Caroline.mp3")
assert song.parent_id == "544"
assert song.artist
assert (song.artist.id, song.artist.name) == (None, "Neil Diamond")
assert song.album
assert (song.album.id, song.album.name) == ("88", "50th Anniversary Collection")
assert song.genre and song.genre.name == "Pop"
def test_get_genres(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_genres"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
genres = adapter.get_genres()
assert len(genres) == 2
assert [g.name for g in genres] == ["Country", "Pop"]
def test_get_artists(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_artists"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
artists = adapter.get_artists()
assert len(artists) == 7
assert {a.name for a in artists} == {
"Adele",
"Austin French",
"The Afters",
"The Band Perry",
"Basshunter",
"Zac Brown Band",
"Zach Williams",
}
def test_get_ignored_articles(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_artists"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
ignored_articles = adapter.get_ignored_articles()
assert ignored_articles == {"The", "El", "La", "Los", "Las", "Le", "Les"}
def test_get_ignored_articles_from_cached_get_artists(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_artists"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
adapter.get_artists()
ignored_articles = adapter.get_ignored_articles()
assert ignored_articles == {"The", "El", "La", "Los", "Las", "Le", "Les"}
def test_get_artist(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_artist"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
artist = adapter.get_artist("3")
assert artist.album_count == 1
assert artist.albums and len(artist.albums) == 1
assert ("3", "Kane Brown") == (artist.albums[0].id, artist.albums[0].name)
assert artist.artist_image_url == "ar-3"
assert artist.biography and len(artist.biography) > 0
assert artist.name == "Kane Brown"
assert artist.similar_artists
assert len(artist.similar_artists) == 20
assert (first_similar := artist.similar_artists[0])
assert first_similar
assert first_similar.name == "Luke Combs"
assert first_similar.artist_image_url == "ar-158"
def test_get_artist_with_good_image_url(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_artist_good_image_url"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
artist = adapter.get_artist("3")
assert artist.album_count == 1
assert artist.albums and len(artist.albums) == 1
assert artist.biography and len(artist.biography) > 0
assert artist.name == "Kane Brown"
assert ("3", "Kane Brown") == (artist.albums[0].id, artist.albums[0].name)
assert (
artist.artist_image_url
== "http://entertainermag.com/wp-content/uploads/2017/04/Kane-Brown-Web-Optimized.jpg" # noqa: E501
)
def test_get_play_queue(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_play_queue"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
play_queue = adapter.get_play_queue()
assert play_queue
assert play_queue.current_index and play_queue.current_index == 1
assert play_queue.position == timedelta(milliseconds=98914)
assert play_queue.username == "sumner"
assert play_queue.changed == datetime(
2020, 5, 12, 5, 16, 32, 114000, tzinfo=timezone.utc
)
assert play_queue.songs and len(play_queue.songs) == 5
song = play_queue.songs[0]
assert song.album and song.album.name == "Despacito"
assert song.artist and song.artist.name == "Peter Bence"
assert song.genre and song.genre.name == "Classical"
def test_get_album(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_album"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
album = adapter.get_album("243")
assert (
album.id,
album.name,
album.cover_art,
album.song_count,
album.year,
album.duration,
) == (
"243",
"What You See Is What You Get",
"al-243",
17,
2019,
timedelta(seconds=3576),
)
assert album.artist
assert (album.artist.id, album.artist.name) == ("158", "Luke Combs")
assert album.genre and album.genre.name == "Country"
assert album.songs
assert len(album.songs) == 17
assert [s.title for s in album.songs] == [
"Beer Never Broke My Heart",
"Refrigerator Door",
"Even Though I'm Leaving",
"Lovin' On You",
"Moon Over Mexico",
"1, 2 Many",
"Blue Collar Boys",
"New Every Day",
"Reasons",
"Every Little Bit Helps",
"Dear Today",
"What You See Is What You Get",
"Does To Me",
"Angels Workin' Overtime",
"All Over Again",
"Nothing Like You",
"Better Together",
]
def test_get_music_directory(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_music_directory"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
directory = adapter.get_directory("3")
assert directory.id == "60"
assert directory.name == "Luke Bryan"
assert directory.parent_id == "root"
assert directory.children and len(directory.children) == 1
child = directory.children[0]
assert isinstance(child, SubsonicAPI.Directory)
assert child.id == "542"
assert child.name == "Crash My Party"
assert child.parent_id == "60"
for filename, data in mock_data_files("get_indexes"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
directory = adapter.get_directory("root")
assert directory.id == "root"
assert directory.parent_id is None
assert len(directory.children) == 7
child = directory.children[0]
assert isinstance(child, SubsonicAPI.Directory)
assert child.id == "73"
assert child.name == "The Afters"
assert child.parent_id == "root"
def test_search(adapter: SubsonicAdapter):
for filename, data in mock_data_files("search3"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
search_results = adapter.search("3")
assert len(search_results._songs) == 7
assert len(search_results._artists) == 2
assert len(search_results._albums) == 4