Skip to content
Snippets Groups Projects
Verified Commit 47aa209d authored by Eliot Berriot's avatar Eliot Berriot
Browse files

See !368: ensure we filter playable entities in subsonic API

parent 224fa4bf
No related branches found
No related tags found
No related merge requests found
...@@ -14,11 +14,28 @@ SAMPLES_PATH = os.path.join( ...@@ -14,11 +14,28 @@ SAMPLES_PATH = os.path.join(
) )
def playable_factory(field):
@factory.post_generation
def inner(self, create, extracted, **kwargs):
if not create:
return
if extracted:
UploadFactory(
library__privacy_level="everyone",
import_status="finished",
**{field: self}
)
return inner
@registry.register @registry.register
class ArtistFactory(factory.django.DjangoModelFactory): class ArtistFactory(factory.django.DjangoModelFactory):
name = factory.Faker("name") name = factory.Faker("name")
mbid = factory.Faker("uuid4") mbid = factory.Faker("uuid4")
fid = factory.Faker("federation_url") fid = factory.Faker("federation_url")
playable = playable_factory("track__album__artist")
class Meta: class Meta:
model = "music.Artist" model = "music.Artist"
...@@ -33,6 +50,7 @@ class AlbumFactory(factory.django.DjangoModelFactory): ...@@ -33,6 +50,7 @@ class AlbumFactory(factory.django.DjangoModelFactory):
artist = factory.SubFactory(ArtistFactory) artist = factory.SubFactory(ArtistFactory)
release_group_id = factory.Faker("uuid4") release_group_id = factory.Faker("uuid4")
fid = factory.Faker("federation_url") fid = factory.Faker("federation_url")
playable = playable_factory("track__album")
class Meta: class Meta:
model = "music.Album" model = "music.Album"
...@@ -47,6 +65,7 @@ class TrackFactory(factory.django.DjangoModelFactory): ...@@ -47,6 +65,7 @@ class TrackFactory(factory.django.DjangoModelFactory):
artist = factory.SelfAttribute("album.artist") artist = factory.SelfAttribute("album.artist")
position = 1 position = 1
tags = ManyToManyFromList("tags") tags = ManyToManyFromList("tags")
playable = playable_factory("track")
class Meta: class Meta:
model = "music.Track" model = "music.Track"
...@@ -71,6 +90,9 @@ class UploadFactory(factory.django.DjangoModelFactory): ...@@ -71,6 +90,9 @@ class UploadFactory(factory.django.DjangoModelFactory):
class Params: class Params:
in_place = factory.Trait(audio_file=None) in_place = factory.Trait(audio_file=None)
playable = factory.Trait(
import_status="finished", library__privacy_level="everyone"
)
@registry.register @registry.register
......
...@@ -19,7 +19,9 @@ from funkwhale_api.playlists import models as playlists_models ...@@ -19,7 +19,9 @@ from funkwhale_api.playlists import models as playlists_models
from . import authentication, filters, negotiation, serializers from . import authentication, filters, negotiation, serializers
def find_object(queryset, model_field="pk", field="id", cast=int): def find_object(
queryset, model_field="pk", field="id", cast=int, filter_playable=False
):
def decorator(func): def decorator(func):
def inner(self, request, *args, **kwargs): def inner(self, request, *args, **kwargs):
data = request.GET or request.POST data = request.GET or request.POST
...@@ -50,6 +52,11 @@ def find_object(queryset, model_field="pk", field="id", cast=int): ...@@ -50,6 +52,11 @@ def find_object(queryset, model_field="pk", field="id", cast=int):
qs = queryset qs = queryset
if hasattr(qs, "__call__"): if hasattr(qs, "__call__"):
qs = qs(request) qs = qs(request)
if filter_playable:
actor = utils.get_actor_from_request(request)
qs = qs.playable_by(actor).distinct()
try: try:
obj = qs.get(**{model_field: value}) obj = qs.get(**{model_field: value})
except qs.model.DoesNotExist: except qs.model.DoesNotExist:
...@@ -124,7 +131,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -124,7 +131,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
@list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists") @list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists")
def get_artists(self, request, *args, **kwargs): def get_artists(self, request, *args, **kwargs):
artists = music_models.Artist.objects.all() artists = music_models.Artist.objects.all().playable_by(
utils.get_actor_from_request(request)
)
data = serializers.GetArtistsSerializer(artists).data data = serializers.GetArtistsSerializer(artists).data
payload = {"artists": data} payload = {"artists": data}
...@@ -132,14 +141,16 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -132,14 +141,16 @@ class SubsonicViewSet(viewsets.GenericViewSet):
@list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes") @list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes")
def get_indexes(self, request, *args, **kwargs): def get_indexes(self, request, *args, **kwargs):
artists = music_models.Artist.objects.all() artists = music_models.Artist.objects.all().playable_by(
utils.get_actor_from_request(request)
)
data = serializers.GetArtistsSerializer(artists).data data = serializers.GetArtistsSerializer(artists).data
payload = {"indexes": data} payload = {"indexes": data}
return response.Response(payload, status=200) return response.Response(payload, status=200)
@list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist") @list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist")
@find_object(music_models.Artist.objects.all()) @find_object(music_models.Artist.objects.all(), filter_playable=True)
def get_artist(self, request, *args, **kwargs): def get_artist(self, request, *args, **kwargs):
artist = kwargs.pop("obj") artist = kwargs.pop("obj")
data = serializers.GetArtistSerializer(artist).data data = serializers.GetArtistSerializer(artist).data
...@@ -148,7 +159,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -148,7 +159,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
return response.Response(payload, status=200) return response.Response(payload, status=200)
@list_route(methods=["get", "post"], url_name="get_song", url_path="getSong") @list_route(methods=["get", "post"], url_name="get_song", url_path="getSong")
@find_object(music_models.Track.objects.all()) @find_object(music_models.Track.objects.all(), filter_playable=True)
def get_song(self, request, *args, **kwargs): def get_song(self, request, *args, **kwargs):
track = kwargs.pop("obj") track = kwargs.pop("obj")
data = serializers.GetSongSerializer(track).data data = serializers.GetSongSerializer(track).data
...@@ -159,14 +170,16 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -159,14 +170,16 @@ class SubsonicViewSet(viewsets.GenericViewSet):
@list_route( @list_route(
methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2" methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2"
) )
@find_object(music_models.Artist.objects.all()) @find_object(music_models.Artist.objects.all(), filter_playable=True)
def get_artist_info2(self, request, *args, **kwargs): def get_artist_info2(self, request, *args, **kwargs):
payload = {"artist-info2": {}} payload = {"artist-info2": {}}
return response.Response(payload, status=200) return response.Response(payload, status=200)
@list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum") @list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum")
@find_object(music_models.Album.objects.select_related("artist")) @find_object(
music_models.Album.objects.select_related("artist"), filter_playable=True
)
def get_album(self, request, *args, **kwargs): def get_album(self, request, *args, **kwargs):
album = kwargs.pop("obj") album = kwargs.pop("obj")
data = serializers.GetAlbumSerializer(album).data data = serializers.GetAlbumSerializer(album).data
...@@ -174,7 +187,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -174,7 +187,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
return response.Response(payload, status=200) return response.Response(payload, status=200)
@list_route(methods=["get", "post"], url_name="stream", url_path="stream") @list_route(methods=["get", "post"], url_name="stream", url_path="stream")
@find_object(music_models.Track.objects.all()) @find_object(music_models.Track.objects.all(), filter_playable=True)
def stream(self, request, *args, **kwargs): def stream(self, request, *args, **kwargs):
track = kwargs.pop("obj") track = kwargs.pop("obj")
queryset = track.uploads.select_related("track__album__artist", "track__artist") queryset = track.uploads.select_related("track__album__artist", "track__artist")
...@@ -221,6 +234,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -221,6 +234,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
data = request.GET or request.POST data = request.GET or request.POST
filterset = filters.AlbumList2FilterSet(data, queryset=queryset) filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
queryset = filterset.qs queryset = filterset.qs
actor = utils.get_actor_from_request(request)
queryset = queryset.playable_by(actor)
try: try:
offset = int(data["offset"]) offset = int(data["offset"])
except (TypeError, KeyError, ValueError): except (TypeError, KeyError, ValueError):
...@@ -240,6 +256,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -240,6 +256,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
def search3(self, request, *args, **kwargs): def search3(self, request, *args, **kwargs):
data = request.GET or request.POST data = request.GET or request.POST
query = str(data.get("query", "")).replace("*", "") query = str(data.get("query", "")).replace("*", "")
actor = utils.get_actor_from_request(request)
conf = [ conf = [
{ {
"subsonic": "artist", "subsonic": "artist",
...@@ -292,6 +309,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -292,6 +309,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
queryset = c["queryset"].filter( queryset = c["queryset"].filter(
utils.get_query(query, c["search_fields"]) utils.get_query(query, c["search_fields"])
) )
queryset = queryset.playable_by(actor)
queryset = queryset[offset : offset + size] queryset = queryset[offset : offset + size]
payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset) payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
return response.Response(payload) return response.Response(payload)
......
...@@ -74,10 +74,13 @@ def test_ping(f, db, api_client): ...@@ -74,10 +74,13 @@ def test_ping(f, db, api_client):
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_artists(f, db, logged_in_api_client, factories): def test_get_artists(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-artists") url = reverse("api:subsonic-get-artists")
assert url.endswith("getArtists") is True assert url.endswith("getArtists") is True
factories["music.Artist"].create_batch(size=10) factories["music.Artist"].create_batch(size=3, playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = { expected = {
"artists": serializers.GetArtistsSerializer( "artists": serializers.GetArtistsSerializer(
music_models.Artist.objects.all() music_models.Artist.objects.all()
...@@ -87,19 +90,25 @@ def test_get_artists(f, db, logged_in_api_client, factories): ...@@ -87,19 +90,25 @@ def test_get_artists(f, db, logged_in_api_client, factories):
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_artist(f, db, logged_in_api_client, factories): def test_get_artist(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-artist") url = reverse("api:subsonic-get-artist")
assert url.endswith("getArtist") is True assert url.endswith("getArtist") is True
artist = factories["music.Artist"]() artist = factories["music.Artist"](playable=True)
factories["music.Album"].create_batch(size=3, artist=artist) factories["music.Album"].create_batch(size=3, artist=artist, playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = {"artist": serializers.GetArtistSerializer(artist).data} expected = {"artist": serializers.GetArtistSerializer(artist).data}
response = logged_in_api_client.get(url, {"id": artist.pk}) response = logged_in_api_client.get(url, {"id": artist.pk})
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
...@@ -114,10 +123,13 @@ def test_get_invalid_artist(f, db, logged_in_api_client, factories): ...@@ -114,10 +123,13 @@ def test_get_invalid_artist(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_artist_info2(f, db, logged_in_api_client, factories): def test_get_artist_info2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-artist-info2") url = reverse("api:subsonic-get-artist-info2")
assert url.endswith("getArtistInfo2") is True assert url.endswith("getArtistInfo2") is True
artist = factories["music.Artist"]() artist = factories["music.Artist"](playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = {"artist-info2": {}} expected = {"artist-info2": {}}
response = logged_in_api_client.get(url, {"id": artist.pk}) response = logged_in_api_client.get(url, {"id": artist.pk})
...@@ -125,50 +137,62 @@ def test_get_artist_info2(f, db, logged_in_api_client, factories): ...@@ -125,50 +137,62 @@ def test_get_artist_info2(f, db, logged_in_api_client, factories):
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_album(f, db, logged_in_api_client, factories): def test_get_album(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-album") url = reverse("api:subsonic-get-album")
assert url.endswith("getAlbum") is True assert url.endswith("getAlbum") is True
artist = factories["music.Artist"]() artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist) album = factories["music.Album"](artist=artist)
factories["music.Track"].create_batch(size=3, album=album) factories["music.Track"].create_batch(size=3, album=album, playable=True)
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
expected = {"album": serializers.GetAlbumSerializer(album).data} expected = {"album": serializers.GetAlbumSerializer(album).data}
response = logged_in_api_client.get(url, {"f": f, "id": album.pk}) response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
playable_by.assert_called_once_with(
music_models.Album.objects.select_related("artist"), None
)
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_song(f, db, logged_in_api_client, factories): def test_get_song(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-song") url = reverse("api:subsonic-get-song")
assert url.endswith("getSong") is True assert url.endswith("getSong") is True
artist = factories["music.Artist"]() artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist) album = factories["music.Album"](artist=artist)
track = factories["music.Track"](album=album) track = factories["music.Track"](album=album, playable=True)
upload = factories["music.Upload"](track=track) upload = factories["music.Upload"](track=track)
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
response = logged_in_api_client.get(url, {"f": f, "id": track.pk}) response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200 assert response.status_code == 200
assert response.data == { assert response.data == {
"song": serializers.get_track_data(track.album, track, upload) "song": serializers.get_track_data(track.album, track, upload)
} }
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_stream(f, db, logged_in_api_client, factories, mocker): def test_stream(f, db, logged_in_api_client, factories, mocker, queryset_equal_queries):
url = reverse("api:subsonic-stream") url = reverse("api:subsonic-stream")
mocked_serve = mocker.spy(music_views, "handle_serve") mocked_serve = mocker.spy(music_views, "handle_serve")
assert url.endswith("stream") is True assert url.endswith("stream") is True
artist = factories["music.Artist"]() upload = factories["music.Upload"](playable=True)
album = factories["music.Album"](artist=artist) playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
track = factories["music.Track"](album=album) response = logged_in_api_client.get(url, {"f": f, "id": upload.track.pk})
upload = factories["music.Upload"](track=track)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
mocked_serve.assert_called_once_with(upload=upload, user=logged_in_api_client.user) mocked_serve.assert_called_once_with(upload=upload, user=logged_in_api_client.user)
assert response.status_code == 200 assert response.status_code == 200
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
...@@ -231,25 +255,30 @@ def test_get_starred(f, db, logged_in_api_client, factories): ...@@ -231,25 +255,30 @@ def test_get_starred(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_album_list2(f, db, logged_in_api_client, factories): def test_get_album_list2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-album-list2") url = reverse("api:subsonic-get-album-list2")
assert url.endswith("getAlbumList2") is True assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"]() album1 = factories["music.Album"](playable=True)
album2 = factories["music.Album"]() album2 = factories["music.Album"](playable=True)
factories["music.Album"]()
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
response = logged_in_api_client.get(url, {"f": f, "type": "newest"}) response = logged_in_api_client.get(url, {"f": f, "type": "newest"})
assert response.status_code == 200 assert response.status_code == 200
assert response.data == { assert response.data == {
"albumList2": {"album": serializers.get_album_list2_data([album2, album1])} "albumList2": {"album": serializers.get_album_list2_data([album2, album1])}
} }
playable_by.assert_called_once()
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_album_list2_pagination(f, db, logged_in_api_client, factories): def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get-album-list2") url = reverse("api:subsonic-get-album-list2")
assert url.endswith("getAlbumList2") is True assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"]() album1 = factories["music.Album"](playable=True)
factories["music.Album"]() factories["music.Album"](playable=True)
response = logged_in_api_client.get( response = logged_in_api_client.get(
url, {"f": f, "type": "newest", "size": 1, "offset": 1} url, {"f": f, "type": "newest", "size": 1, "offset": 1}
) )
...@@ -264,12 +293,15 @@ def test_get_album_list2_pagination(f, db, logged_in_api_client, factories): ...@@ -264,12 +293,15 @@ def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
def test_search3(f, db, logged_in_api_client, factories): def test_search3(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-search3") url = reverse("api:subsonic-search3")
assert url.endswith("search3") is True assert url.endswith("search3") is True
artist = factories["music.Artist"](name="testvalue") artist = factories["music.Artist"](name="testvalue", playable=True)
factories["music.Artist"](name="nope") factories["music.Artist"](name="nope")
album = factories["music.Album"](title="testvalue") factories["music.Artist"](name="nope2", playable=True)
album = factories["music.Album"](title="testvalue", playable=True)
factories["music.Album"](title="nope") factories["music.Album"](title="nope")
track = factories["music.Track"](title="testvalue") factories["music.Album"](title="nope2", playable=True)
track = factories["music.Track"](title="testvalue", playable=True)
factories["music.Track"](title="nope") factories["music.Track"](title="nope")
factories["music.Track"](title="nope2", playable=True)
response = logged_in_api_client.get(url, {"f": f, "query": "testval"}) response = logged_in_api_client.get(url, {"f": f, "query": "testval"})
...@@ -385,20 +417,25 @@ def test_get_music_folders(f, db, logged_in_api_client, factories): ...@@ -385,20 +417,25 @@ def test_get_music_folders(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["xml", "json"]) @pytest.mark.parametrize("f", ["xml", "json"])
def test_get_indexes(f, db, logged_in_api_client, factories): def test_get_indexes(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get-indexes") url = reverse("api:subsonic-get-indexes")
assert url.endswith("getIndexes") is True assert url.endswith("getIndexes") is True
factories["music.Artist"].create_batch(size=10) factories["music.Artist"].create_batch(size=3, playable=True)
expected = { expected = {
"indexes": serializers.GetArtistsSerializer( "indexes": serializers.GetArtistsSerializer(
music_models.Artist.objects.all() music_models.Artist.objects.all()
).data ).data
} }
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
response = logged_in_api_client.get(url) response = logged_in_api_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
def test_get_cover_art_album(factories, logged_in_api_client): def test_get_cover_art_album(factories, logged_in_api_client):
url = reverse("api:subsonic-get-cover-art") url = reverse("api:subsonic-get-cover-art")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment