[tests] Add tests for koreader and views
All checks were successful
build & deploy / test (push) Successful in 1m44s
build & deploy / deploy (push) Has been skipped

This commit is contained in:
2026-03-08 01:05:17 -05:00
parent 446144bc51
commit 7d8e1ac817
3 changed files with 342 additions and 9 deletions

View File

@ -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):

View File

@ -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={})

View File

@ -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