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