[tests] Add tests for koreader and views
This commit is contained in:
@ -23,7 +23,7 @@ def boardgame_scrobble():
|
|||||||
log={
|
log={
|
||||||
"players": [
|
"players": [
|
||||||
{"person_id": first.id, "win": True, "score": 30, "color": "Blue"},
|
{"person_id": first.id, "win": True, "score": 30, "color": "Blue"},
|
||||||
{"person_id": second.id, "win": False, "score": 28, "color": "Red"}
|
{"person_id": second.id, "win": False, "score": 28, "color": "Red"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -58,9 +58,7 @@ class MopidyRequest:
|
|||||||
"artist": kwargs.get("artist", self.artist),
|
"artist": kwargs.get("artist", self.artist),
|
||||||
"album": kwargs.get("album", self.album),
|
"album": kwargs.get("album", self.album),
|
||||||
"track_number": int(kwargs.get("track_number", self.track_number)),
|
"track_number": int(kwargs.get("track_number", self.track_number)),
|
||||||
"run_time_ticks": int(
|
"run_time_ticks": int(kwargs.get("run_time_ticks", self.run_time_ticks)),
|
||||||
kwargs.get("run_time_ticks", self.run_time_ticks)
|
|
||||||
),
|
|
||||||
"run_time": int(kwargs.get("run_time", self.run_time)),
|
"run_time": int(kwargs.get("run_time", self.run_time)),
|
||||||
"playback_time_ticks": int(
|
"playback_time_ticks": int(
|
||||||
kwargs.get("playback_time_ticks", self.playback_time_ticks)
|
kwargs.get("playback_time_ticks", self.playback_time_ticks)
|
||||||
@ -103,9 +101,7 @@ def mopidy_track():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mopidy_track_diff_album_request_data(**kwargs):
|
def mopidy_track_diff_album_request_data(**kwargs):
|
||||||
mb_album_id = "0c56c457-afe1-4679-baab-759ba8dd2a58"
|
mb_album_id = "0c56c457-afe1-4679-baab-759ba8dd2a58"
|
||||||
return MopidyRequest(
|
return MopidyRequest(album="Gold", musicbrainz_album_id=mb_album_id).request_json
|
||||||
album="Gold", musicbrainz_album_id=mb_album_id
|
|
||||||
).request_json
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -115,6 +111,7 @@ def mopidy_podcast_request_data():
|
|||||||
mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
|
mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
|
||||||
).request_json
|
).request_json
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mopidy_podcast_https_request_data():
|
def mopidy_podcast_https_request_data():
|
||||||
mopidy_uri = "podcast+https://feeds.npr.org/510318/podcast.xml#85b9c4c4-ae09-43d9-8853-31ccf43f68e6"
|
mopidy_uri = "podcast+https://feeds.npr.org/510318/podcast.xml#85b9c4c4-ae09-43d9-8853-31ccf43f68e6"
|
||||||
@ -122,6 +119,7 @@ def mopidy_podcast_https_request_data():
|
|||||||
mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
|
mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
|
||||||
).request_json
|
).request_json
|
||||||
|
|
||||||
|
|
||||||
class JellyfinTrackRequest:
|
class JellyfinTrackRequest:
|
||||||
name = "Emotion"
|
name = "Emotion"
|
||||||
artist = "Carly Rae Jepsen"
|
artist = "Carly Rae Jepsen"
|
||||||
@ -136,6 +134,7 @@ class JellyfinTrackRequest:
|
|||||||
musicbrainz_album_id = "03b864cd-7761-314c-a892-05a89ddff00d"
|
musicbrainz_album_id = "03b864cd-7761-314c-a892-05a89ddff00d"
|
||||||
musicbrainz_artist_id = "95f5b748-d370-47fe-85bd-0af2dc450bc0"
|
musicbrainz_artist_id = "95f5b748-d370-47fe-85bd-0af2dc450bc0"
|
||||||
status = "resumed"
|
status = "resumed"
|
||||||
|
client_name = "Jellyfin"
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.request_data = {
|
self.request_data = {
|
||||||
@ -159,6 +158,7 @@ class JellyfinTrackRequest:
|
|||||||
"musicbrainz_artist_id", self.musicbrainz_artist_id
|
"musicbrainz_artist_id", self.musicbrainz_artist_id
|
||||||
),
|
),
|
||||||
"Status": kwargs.get("status", self.status),
|
"Status": kwargs.get("status", self.status),
|
||||||
|
"ClientName": kwargs.get("client_name", self.client_name),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, MagicMock
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import time_machine
|
import time_machine
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from music.models import Track
|
from music.models import Track, Artist, Album
|
||||||
from podcasts.models import PodcastEpisode
|
from podcasts.models import PodcastEpisode
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
@ -18,6 +18,14 @@ def test_get_not_allowed_from_mopidy(client, valid_auth_token):
|
|||||||
assert response.status_code == 405
|
assert response.status_code == 405
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_not_allowed_from_jellyfin(client, valid_auth_token):
|
||||||
|
url = reverse("scrobbles:jellyfin-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
response = client.get(url, headers=headers)
|
||||||
|
assert response.status_code == 405
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_bad_mopidy_request_data(client, valid_auth_token):
|
def test_bad_mopidy_request_data(client, valid_auth_token):
|
||||||
url = reverse("scrobbles:mopidy-webhook")
|
url = reverse("scrobbles:mopidy-webhook")
|
||||||
@ -30,6 +38,172 @@ def test_bad_mopidy_request_data(client, valid_auth_token):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_bad_jellyfin_request_data(client, valid_auth_token):
|
||||||
|
url = reverse("scrobbles:jellyfin-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
response = client.post(url, headers)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert (
|
||||||
|
response.data["detail"]
|
||||||
|
== "JSON parse error - Expecting value: line 1 column 1 (char 0)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("music.models.Artist.find_or_create")
|
||||||
|
@patch("music.models.Track.find_or_create")
|
||||||
|
def test_create_scrobble_from_mopidy_track_webhook(
|
||||||
|
mock_track_fc,
|
||||||
|
mock_artist_fc,
|
||||||
|
client,
|
||||||
|
valid_auth_token,
|
||||||
|
mopidy_track,
|
||||||
|
):
|
||||||
|
mock_artist = MagicMock(spec=Artist)
|
||||||
|
mock_artist.id = 1
|
||||||
|
mock_artist_fc.return_value = mock_artist
|
||||||
|
|
||||||
|
mock_track = MagicMock(spec=Track)
|
||||||
|
mock_track.id = 1
|
||||||
|
mock_track.scrobble_for_user.return_value = Scrobble(
|
||||||
|
id=1, track_id=1, user_id=1, in_progress=True
|
||||||
|
)
|
||||||
|
mock_track_fc.return_value = mock_track
|
||||||
|
|
||||||
|
url = reverse("scrobbles:mopidy-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
mopidy_track.request_data,
|
||||||
|
content_type="application/json",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"scrobble_id": 1}
|
||||||
|
mock_track.scrobble_for_user.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("music.models.Artist.find_or_create")
|
||||||
|
@patch("music.models.Track.find_or_create")
|
||||||
|
def test_create_scrobble_from_jellyfin_track_webhook(
|
||||||
|
mock_track_fc,
|
||||||
|
mock_artist_fc,
|
||||||
|
client,
|
||||||
|
valid_auth_token,
|
||||||
|
jellyfin_track,
|
||||||
|
):
|
||||||
|
mock_artist = MagicMock(spec=Artist)
|
||||||
|
mock_artist.id = 1
|
||||||
|
mock_artist_fc.return_value = mock_artist
|
||||||
|
|
||||||
|
mock_track = MagicMock(spec=Track)
|
||||||
|
mock_track.id = 1
|
||||||
|
mock_track.scrobble_for_user.return_value = Scrobble(
|
||||||
|
id=1, track_id=1, user_id=1, in_progress=True
|
||||||
|
)
|
||||||
|
mock_track_fc.return_value = mock_track
|
||||||
|
|
||||||
|
url = reverse("scrobbles:jellyfin-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
|
||||||
|
with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
|
||||||
|
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
jellyfin_track.request_json,
|
||||||
|
content_type="application/json",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"scrobble_id": 1}
|
||||||
|
mock_track.scrobble_for_user.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("music.models.Artist.find_or_create")
|
||||||
|
@patch("music.models.Track.find_or_create")
|
||||||
|
def test_mopidy_track_webhook_creates_track_and_scrobble(
|
||||||
|
mock_track_fc,
|
||||||
|
mock_artist_fc,
|
||||||
|
client,
|
||||||
|
valid_auth_token,
|
||||||
|
mopidy_track,
|
||||||
|
):
|
||||||
|
artist = Artist.objects.create(name="Sublime")
|
||||||
|
album = Album.objects.create(name="Sublime", album_artist=artist)
|
||||||
|
track = Track.objects.create(
|
||||||
|
title="Same in the End",
|
||||||
|
artist=artist,
|
||||||
|
base_run_time_seconds=60,
|
||||||
|
)
|
||||||
|
track.albums.add(album)
|
||||||
|
|
||||||
|
mock_artist_fc.return_value = artist
|
||||||
|
mock_track_fc.return_value = track
|
||||||
|
|
||||||
|
url = reverse("scrobbles:mopidy-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
mopidy_track.request_data,
|
||||||
|
content_type="application/json",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"scrobble_id": 1}
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.get(id=1)
|
||||||
|
assert scrobble.track == track
|
||||||
|
assert scrobble.source == "Mopidy"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("music.models.Artist.find_or_create")
|
||||||
|
@patch("music.models.Track.find_or_create")
|
||||||
|
def test_jellyfin_track_webhook_creates_track_and_scrobble(
|
||||||
|
mock_track_fc,
|
||||||
|
mock_artist_fc,
|
||||||
|
client,
|
||||||
|
valid_auth_token,
|
||||||
|
jellyfin_track,
|
||||||
|
):
|
||||||
|
artist = Artist.objects.create(name="Carly Rae Jepsen")
|
||||||
|
album = Album.objects.create(name="Emotion", album_artist=artist)
|
||||||
|
track = Track.objects.create(
|
||||||
|
title="Emotion",
|
||||||
|
artist=artist,
|
||||||
|
base_run_time_seconds=60,
|
||||||
|
)
|
||||||
|
track.albums.add(album)
|
||||||
|
|
||||||
|
mock_artist_fc.return_value = artist
|
||||||
|
mock_track_fc.return_value = track
|
||||||
|
|
||||||
|
url = reverse("scrobbles:jellyfin-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
|
||||||
|
with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
|
||||||
|
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
jellyfin_track.request_json,
|
||||||
|
content_type="application/json",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"scrobble_id": 1}
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.get(id=1)
|
||||||
|
assert scrobble.track == track
|
||||||
|
assert scrobble.source == "Jellyfin"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip("Need to refactor")
|
@pytest.mark.skip("Need to refactor")
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from books.koreader import (
|
from books.koreader import (
|
||||||
KoReaderBookColumn,
|
KoReaderBookColumn,
|
||||||
build_book_map,
|
build_book_map,
|
||||||
build_page_data,
|
build_page_data,
|
||||||
build_scrobbles_from_book_map,
|
build_scrobbles_from_book_map,
|
||||||
|
get_author_str_from_row,
|
||||||
|
lookup_or_create_authors_from_author_str,
|
||||||
|
create_book_from_row,
|
||||||
|
process_koreader_sqlite_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -52,3 +57,157 @@ def test_build_scrobbles_from_pages(
|
|||||||
assert len(scrobbles[3].logdata.page_data.keys()) == 20
|
assert len(scrobbles[3].logdata.page_data.keys()) == 20
|
||||||
assert len(scrobbles[4].logdata.page_data.keys()) == 20
|
assert len(scrobbles[4].logdata.page_data.keys()) == 20
|
||||||
assert len(scrobbles[5].logdata.page_data.keys()) == 18
|
assert len(scrobbles[5].logdata.page_data.keys()) == 18
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_author_str_from_row():
|
||||||
|
row = [
|
||||||
|
1,
|
||||||
|
"Test Book",
|
||||||
|
"John Smith\nJane Doe",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
300,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"abc123",
|
||||||
|
0,
|
||||||
|
120,
|
||||||
|
]
|
||||||
|
result = get_author_str_from_row(row)
|
||||||
|
assert result == "John Smith, Jane Doe"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_author_str_from_row_strips_middle_initials():
|
||||||
|
row = [
|
||||||
|
1,
|
||||||
|
"Test Book",
|
||||||
|
"John A. Smith",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
300,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"abc123",
|
||||||
|
0,
|
||||||
|
120,
|
||||||
|
]
|
||||||
|
result = get_author_str_from_row(row)
|
||||||
|
assert result == "John Smith"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_lookup_or_create_authors_from_author_str_creates_new_author():
|
||||||
|
author = lookup_or_create_authors_from_author_str("New Author")
|
||||||
|
assert len(author) == 1
|
||||||
|
assert author[0].name == "New Author"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_lookup_or_create_authors_from_author_str_finds_existing():
|
||||||
|
from books.models import Author
|
||||||
|
|
||||||
|
Author.objects.create(name="Existing Author")
|
||||||
|
author = lookup_or_create_authors_from_author_str("Existing Author")
|
||||||
|
assert len(author) == 1
|
||||||
|
assert author[0].name == "Existing Author"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_lookup_or_create_authors_from_author_str_ignores_na():
|
||||||
|
author = lookup_or_create_authors_from_author_str("N/A")
|
||||||
|
assert len(author) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_lookup_or_create_authors_from_author_str_handles_multiple():
|
||||||
|
author = lookup_or_create_authors_from_author_str("Author One, Author Two")
|
||||||
|
assert len(author) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_book_from_row():
|
||||||
|
row = [
|
||||||
|
1,
|
||||||
|
"Test Book - Author Name",
|
||||||
|
"Test Author",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
300,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"abc123def456",
|
||||||
|
3600,
|
||||||
|
120,
|
||||||
|
]
|
||||||
|
book = create_book_from_row(row)
|
||||||
|
assert book.title == "Test Book"
|
||||||
|
assert book.pages == 300
|
||||||
|
assert "abc123def456" in book.koreader_data_by_hash
|
||||||
|
assert book.authors.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_book_from_row_handles_title_with_author_suffix():
|
||||||
|
row = [
|
||||||
|
1,
|
||||||
|
"Test Book - Author_Name",
|
||||||
|
"N/A",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
300,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"abc123def456",
|
||||||
|
3600,
|
||||||
|
120,
|
||||||
|
]
|
||||||
|
book = create_book_from_row(row)
|
||||||
|
assert book.title == "Test Book"
|
||||||
|
assert book.authors.count() == 1
|
||||||
|
assert book.authors.first().name == "Author"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_book_from_row_handles_null_bytes():
|
||||||
|
row = [
|
||||||
|
1,
|
||||||
|
"Test\x00Book",
|
||||||
|
"Test\x00Author",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
300,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"abc123def456",
|
||||||
|
3600,
|
||||||
|
120,
|
||||||
|
]
|
||||||
|
book = create_book_from_row(row)
|
||||||
|
assert "TestBook" in book.title
|
||||||
|
assert "TestAuthor" in book.authors.first().name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_build_book_map_ignores_garbage_titles(koreader_rows):
|
||||||
|
garbage_row = [
|
||||||
|
999,
|
||||||
|
"KOReader Quickstart Guide",
|
||||||
|
"N/A",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"garbage123",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
book_map = build_book_map(koreader_rows.BOOK_ROWS + [garbage_row])
|
||||||
|
assert len(book_map) == 1
|
||||||
|
assert 999 not in book_map
|
||||||
|
|||||||
Reference in New Issue
Block a user