1036 lines
33 KiB
Python
1036 lines
33 KiB
Python
from datetime import datetime, timedelta
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import time_machine
|
|
from django.contrib.auth import get_user_model
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from music.models import Album, Artist, Track
|
|
from podcasts.models import PodcastEpisode
|
|
from scrobbles.models import Scrobble, ShareViewLog
|
|
from scrobbles.sqids import encode_scrobble_share
|
|
from tasks.models import Task
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_get_not_allowed_from_mopidy(client, valid_auth_token):
|
|
url = reverse("scrobbles:mopidy-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
response = client.get(url, headers=headers)
|
|
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
|
|
def test_bad_mopidy_request_data(client, valid_auth_token):
|
|
url = reverse("scrobbles:mopidy-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
response = client.post(
|
|
url, "not valid json", content_type="application/json", headers=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
|
|
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, "not valid json", content_type="application/json", headers=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_fk=artist,
|
|
base_run_time_seconds=60,
|
|
)
|
|
track.artists.add(artist)
|
|
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_fk=artist,
|
|
base_run_time_seconds=60,
|
|
)
|
|
track.artists.add(artist)
|
|
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"
|
|
assert "raw_data" in scrobble.log
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@patch("music.models.Artist.find_or_create")
|
|
@patch("music.models.Track.find_or_create")
|
|
def test_mopidy_track_webhook_stores_raw_data(
|
|
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_fk=artist,
|
|
base_run_time_seconds=60,
|
|
)
|
|
track.artists.add(artist)
|
|
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"
|
|
assert "raw_data" in scrobble.log
|
|
assert scrobble.log["raw_data"]["name"] == "Same in the End"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@patch("music.models.Artist.find_or_create")
|
|
@patch("music.models.Track.find_or_create")
|
|
def test_mopidy_track_webhook_stores_album_id(
|
|
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_fk=artist,
|
|
album=album,
|
|
base_run_time_seconds=60,
|
|
)
|
|
track.artists.add(artist)
|
|
|
|
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
|
|
|
|
scrobble = Scrobble.objects.get(id=1)
|
|
assert "album_id" in scrobble.log
|
|
assert scrobble.log["album_id"] == album.id
|
|
assert "album" not in scrobble.log
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@patch("music.models.Artist.find_or_create")
|
|
@patch("music.models.Track.find_or_create")
|
|
def test_jellyfin_track_webhook_stores_raw_data(
|
|
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_fk=artist,
|
|
base_run_time_seconds=60,
|
|
)
|
|
track.artists.add(artist)
|
|
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
|
|
|
|
scrobble = Scrobble.objects.get(id=1)
|
|
assert scrobble.track == track
|
|
assert scrobble.source == "Jellyfin"
|
|
assert "raw_data" in scrobble.log
|
|
assert scrobble.log["raw_data"]["Name"] == "Emotion"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@patch("music.models.Artist.find_or_create")
|
|
@patch("music.models.Track.find_or_create")
|
|
def test_jellyfin_track_webhook_stores_album_id(
|
|
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_fk=artist,
|
|
album=album,
|
|
base_run_time_seconds=60,
|
|
)
|
|
track.artists.add(artist)
|
|
|
|
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
|
|
|
|
scrobble = Scrobble.objects.get(id=1)
|
|
assert "album_id" in scrobble.log
|
|
assert scrobble.log["album_id"] == album.id
|
|
|
|
|
|
@pytest.mark.skip("Need to refactor")
|
|
@pytest.mark.django_db
|
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
|
@patch(
|
|
"music.utils.lookup_album_dict_from_mb",
|
|
return_value={"year": "1999", "mb_group_id": 1},
|
|
)
|
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
|
def test_scrobble_mopidy_same_track_different_album(
|
|
mock_lookup_artist,
|
|
mock_lookup_album,
|
|
mock_lookup_track,
|
|
mock_lookup_artist_tadb,
|
|
mock_lookup_album_tadb,
|
|
mock_fetch_artwork,
|
|
mock_scrape_allmusic,
|
|
client,
|
|
mopidy_track,
|
|
mopidy_track_diff_album_request_data,
|
|
valid_auth_token,
|
|
):
|
|
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.last()
|
|
assert scrobble.media_obj.album.name == "Sublime"
|
|
|
|
response = client.post(
|
|
url,
|
|
mopidy_track_diff_album_request_data,
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.data == {"scrobble_id": 2}
|
|
scrobble = Scrobble.objects.last()
|
|
assert scrobble.media_obj.__class__ == Track
|
|
assert scrobble.media_obj.album.name == "Sublime"
|
|
assert scrobble.media_obj.title == "Same in the End"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@patch(
|
|
"podcasts.sources.podcastindex.lookup_podcast_from_podcastindex",
|
|
return_value={},
|
|
)
|
|
def test_scrobble_mopidy_podcast(
|
|
mock_lookup_podcast, client, mopidy_podcast_request_data, valid_auth_token
|
|
):
|
|
url = reverse("scrobbles:mopidy-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
response = client.post(
|
|
url,
|
|
mopidy_podcast_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.media_obj.__class__ == PodcastEpisode
|
|
assert scrobble.media_obj.title == "Up First"
|
|
|
|
|
|
@pytest.mark.skip("Need to refactor")
|
|
@pytest.mark.django_db
|
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
|
@patch(
|
|
"music.utils.lookup_album_dict_from_mb",
|
|
return_value={"year": "1999", "mb_group_id": 1},
|
|
)
|
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
|
def test_scrobble_jellyfin_track(
|
|
mock_lookup_artist,
|
|
mock_lookup_album,
|
|
mock_lookup_track,
|
|
mock_lookup_artist_tadb,
|
|
mock_lookup_album_tadb,
|
|
mock_fetch_artwork,
|
|
mock_scrape_allmusic,
|
|
client,
|
|
jellyfin_track,
|
|
valid_auth_token,
|
|
):
|
|
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.media_obj.__class__ == Track
|
|
assert scrobble.media_obj.title == "Emotion"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_scrobble_detail_view_with_notes_as_flat_list(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="testuser", email="test@example.com", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task", description="Test description")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task,
|
|
media_type="Task",
|
|
user=user,
|
|
visibility="public",
|
|
log={
|
|
"notes": ["First note", "Second note"],
|
|
"description": "Test description",
|
|
},
|
|
)
|
|
url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
assert "First note" in response.content.decode()
|
|
assert "Second note" in response.content.decode()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_scrobble_detail_view_with_notes_as_dict_timestamps(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="testuser", email="test@example.com", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task", description="Test description")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task,
|
|
media_type="Task",
|
|
user=user,
|
|
visibility="public",
|
|
log={
|
|
"notes": [
|
|
{"2024-01-01 10:00:00": "Note at first timestamp"},
|
|
{"2024-01-02 11:30:00": "Note at second timestamp"},
|
|
],
|
|
"description": "Test description",
|
|
},
|
|
)
|
|
url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
content = response.content.decode()
|
|
assert "2024-01-01 10:00:00" in content
|
|
assert "Note at first timestamp" in content
|
|
assert "2024-01-02 11:30:00" in content
|
|
assert "Note at second timestamp" in content
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_scrobble_detail_view_with_notes_and_labels(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="testuser", email="test@example.com", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task", description="Test description")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task,
|
|
media_type="Task",
|
|
user=user,
|
|
visibility="public",
|
|
log={
|
|
"notes": [
|
|
{"2024-01-01 10:00:00": "Note with label"},
|
|
],
|
|
"labels": ["work", "urgent"],
|
|
"description": "Test description",
|
|
},
|
|
)
|
|
url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
content = response.content.decode()
|
|
assert "work" in content
|
|
assert "urgent" in content
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_scrobble_detail_view_post_updates_log(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="testuser", email="test@example.com", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task", description="Test description")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task,
|
|
media_type="Task",
|
|
user=user,
|
|
log={
|
|
"notes": ["Original note"],
|
|
"description": "Original description",
|
|
},
|
|
)
|
|
url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
|
|
|
|
client.force_login(user)
|
|
response = client.post(
|
|
url,
|
|
{
|
|
"description": "Updated description",
|
|
"notes": "Updated note",
|
|
},
|
|
)
|
|
assert response.status_code == 302
|
|
|
|
scrobble.refresh_from_db()
|
|
assert scrobble.log["description"] == "Updated description"
|
|
assert isinstance(scrobble.log["notes"], dict)
|
|
assert list(scrobble.log["notes"].values()) == ["Updated note"]
|
|
|
|
|
|
@pytest.mark.skip("Need to refactor")
|
|
@pytest.mark.django_db
|
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
|
@patch(
|
|
"music.utils.lookup_album_dict_from_mb",
|
|
return_value={"year": "1999", "mb_group_id": 1},
|
|
)
|
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
|
def test_scrobble_jellyfin_track_update(
|
|
mock_lookup_artist,
|
|
mock_lookup_album,
|
|
mock_lookup_track,
|
|
mock_lookup_artist_tadb,
|
|
mock_lookup_album_tadb,
|
|
mock_fetch_artwork,
|
|
mock_scrape_allmusic,
|
|
test_track,
|
|
client,
|
|
jellyfin_track,
|
|
valid_auth_token,
|
|
):
|
|
Scrobble.objects.create(
|
|
timestamp=timezone.now() - timedelta(minutes=0.5),
|
|
track=Track.objects.first(),
|
|
user_id=1,
|
|
)
|
|
url = reverse("scrobbles:jellyfin-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
|
|
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.media_obj.__class__ == Track
|
|
assert scrobble.media_obj.title == "Emotion"
|
|
|
|
|
|
@pytest.mark.skip("Need to refactor")
|
|
@pytest.mark.django_db
|
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
|
@patch(
|
|
"music.utils.lookup_album_dict_from_mb",
|
|
return_value={"year": "1999", "mb_group_id": 1},
|
|
)
|
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
|
def test_scrobble_jellyfin_track_create_new(
|
|
mock_lookup_artist,
|
|
mock_lookup_album,
|
|
mock_lookup_track,
|
|
mock_lookup_artist_tadb,
|
|
mock_lookup_album_tadb,
|
|
mock_fetch_artwork,
|
|
mock_scrape_allmusic,
|
|
test_track,
|
|
client,
|
|
jellyfin_track,
|
|
valid_auth_token,
|
|
):
|
|
url = reverse("scrobbles:jellyfin-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
Scrobble.objects.create(
|
|
timestamp=timezone.now() - timedelta(minutes=1),
|
|
track=Track.objects.first(),
|
|
user_id=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": 2}
|
|
|
|
scrobble = Scrobble.objects.get(id=1)
|
|
assert scrobble.media_obj.__class__ == Track
|
|
assert scrobble.media_obj.title == "Emotion"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_get_not_allowed_from_gps(client, valid_auth_token):
|
|
url = reverse("scrobbles:gps-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
response = client.get(url, headers=headers)
|
|
assert response.status_code == 405
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_gps_webhook_creates_location(client, valid_auth_token):
|
|
url = reverse("scrobbles:gps-webhook")
|
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
gps_data = {
|
|
"lat": "40.7128",
|
|
"lon": "-74.0060",
|
|
"alt": "10.5",
|
|
"time": "2024-01-14T12:00:00Z",
|
|
"prov": "gps",
|
|
}
|
|
response = client.post(
|
|
url,
|
|
gps_data,
|
|
content_type="application/json",
|
|
headers=headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert "scrobble_id" in response.data
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_view_shared_visibility(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="shareuser", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
url = reverse(
|
|
"scrobbles:shared-detail",
|
|
kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
|
|
)
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_view_public_visibility(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="shareuser2", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="public",
|
|
timestamp=timezone.now(),
|
|
)
|
|
url = reverse(
|
|
"scrobbles:shared-detail",
|
|
kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
|
|
)
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_view_private_visibility_returns_404(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="shareuser3", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="private",
|
|
timestamp=timezone.now(),
|
|
)
|
|
url = reverse(
|
|
"scrobbles:shared-detail",
|
|
kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
|
|
)
|
|
response = client.get(url)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_view_invalid_sqid_returns_404(client):
|
|
url = reverse("scrobbles:shared-detail", kwargs={"sqid": "InvalidSqid123"})
|
|
response = client.get(url)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_view_expired_token_returns_404(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="shareuser4", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
old_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
|
|
scrobble.regenerate_share_token()
|
|
url = reverse("scrobbles:shared-detail", kwargs={"sqid": old_sqid})
|
|
response = client.get(url)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_view_increments_count_and_logs_view(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="shareuser5", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
assert scrobble.share_view_count == 0
|
|
|
|
url = reverse(
|
|
"scrobbles:shared-detail",
|
|
kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
|
|
)
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
|
|
scrobble.refresh_from_db()
|
|
assert scrobble.share_view_count == 1
|
|
assert ShareViewLog.objects.filter(scrobble=scrobble).count() == 1
|
|
|
|
log_entry = ShareViewLog.objects.filter(scrobble=scrobble).first()
|
|
assert log_entry.ip_address == "127.0.0.1"
|
|
assert log_entry.user_agent == ""
|
|
assert log_entry.referrer == ""
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_explore_view_shows_only_public_scrobbles(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="exploreuser", password="testpass"
|
|
)
|
|
public_task = Task.objects.create(title="Public Task Title")
|
|
shared_task = Task.objects.create(title="Shared Task Title")
|
|
private_task = Task.objects.create(title="Private Task Title")
|
|
ts = timezone.now()
|
|
public_scrobble = Scrobble.objects.create(
|
|
task=public_task, media_type="Task", user=user, visibility="public",
|
|
timestamp=ts,
|
|
)
|
|
Scrobble.objects.create(
|
|
task=shared_task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=ts,
|
|
)
|
|
Scrobble.objects.create(
|
|
task=private_task, media_type="Task", user=user, visibility="private",
|
|
timestamp=ts,
|
|
)
|
|
|
|
url = reverse("scrobbles:explore")
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
|
|
content = response.content.decode()
|
|
assert "Public Task Title" in content
|
|
assert "Shared Task Title" not in content
|
|
assert "Private Task Title" not in content
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_change_visibility_owner_can_change(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="visuser", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="private",
|
|
timestamp=timezone.now(),
|
|
)
|
|
|
|
client.force_login(user)
|
|
url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})
|
|
response = client.post(url, {"visibility": "shared"})
|
|
assert response.status_code == 302
|
|
|
|
scrobble.refresh_from_db()
|
|
assert scrobble.visibility == "shared"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_change_visibility_non_owner_gets_404(client):
|
|
owner = get_user_model().objects.create_user(
|
|
username="owner", password="testpass"
|
|
)
|
|
other = get_user_model().objects.create_user(
|
|
username="other", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=owner, visibility="private",
|
|
timestamp=timezone.now(),
|
|
)
|
|
|
|
client.force_login(other)
|
|
url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})
|
|
response = client.post(url, {"visibility": "shared"})
|
|
assert response.status_code == 404
|
|
|
|
scrobble.refresh_from_db()
|
|
assert scrobble.visibility == "private"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_change_visibility_anonymous_redirects_to_login(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="anontest", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="private",
|
|
timestamp=timezone.now(),
|
|
)
|
|
url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})
|
|
response = client.post(url, {"visibility": "shared"})
|
|
assert response.status_code == 302
|
|
assert "/login/" in response.url
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_regenerate_share_token_invalidates_old_sqid(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="regentest", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
old_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
|
|
|
|
client.force_login(user)
|
|
url = reverse("scrobbles:regenerate-share-token", kwargs={"pk": scrobble.id})
|
|
response = client.post(url)
|
|
assert response.status_code == 302
|
|
|
|
scrobble.refresh_from_db()
|
|
assert scrobble.share_token_version == 1
|
|
|
|
old_url = reverse("scrobbles:shared-detail", kwargs={"sqid": old_sqid})
|
|
old_response = client.get(old_url)
|
|
assert old_response.status_code == 404
|
|
|
|
new_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
|
|
new_url = reverse("scrobbles:shared-detail", kwargs={"sqid": new_sqid})
|
|
new_response = client.get(new_url)
|
|
assert new_response.status_code == 200
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_analytics_owner_can_view(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="analyticsuser", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
|
|
client.force_login(user)
|
|
url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_analytics_non_owner_gets_404(client):
|
|
owner = get_user_model().objects.create_user(
|
|
username="analyticsowner", password="testpass"
|
|
)
|
|
other = get_user_model().objects.create_user(
|
|
username="analyticsother", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=owner, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
|
|
client.force_login(other)
|
|
url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})
|
|
response = client.get(url)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_share_analytics_shows_view_logs(client):
|
|
user = get_user_model().objects.create_user(
|
|
username="analyticsviews", password="testpass"
|
|
)
|
|
task = Task.objects.create(title="Test Task")
|
|
scrobble = Scrobble.objects.create(
|
|
task=task, media_type="Task", user=user, visibility="shared",
|
|
timestamp=timezone.now(),
|
|
)
|
|
|
|
sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
|
|
share_url = reverse("scrobbles:shared-detail", kwargs={"sqid": sqid})
|
|
client.get(share_url)
|
|
client.get(share_url)
|
|
|
|
client.force_login(user)
|
|
url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})
|
|
response = client.get(url)
|
|
assert response.status_code == 200
|
|
content = response.content.decode()
|
|
assert "127.0.0.1" in content
|
|
assert ShareViewLog.objects.filter(scrobble=scrobble).count() == 2
|