test_views.py 23.5 KB
Newer Older
1
import datetime
2
3
import json

Eliot Berriot's avatar
Eliot Berriot committed
4
import pytest
5
from django.urls import reverse
Eliot Berriot's avatar
Eliot Berriot committed
6
from django.utils import timezone
7
from rest_framework.response import Response
8

9
import funkwhale_api
10
from funkwhale_api.moderation import filters as moderation_filters
11
12
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
Eliot Berriot's avatar
Eliot Berriot committed
13
from funkwhale_api.subsonic import renderers, serializers
14
15
16
17
18
19
20


def render_json(data):
    return json.loads(renderers.SubsonicJSONRenderer().render(data))


def test_render_content_json(db, api_client):
Eliot Berriot's avatar
Eliot Berriot committed
21
22
    url = reverse("api:subsonic-ping")
    response = api_client.get(url, {"f": "json"})
23

24
25
26
27
    expected = {
        "status": "ok",
        "version": "1.16.0",
        "type": "funkwhale",
28
        "funkwhaleVersion": funkwhale_api.__version__,
29
    }
30
31
32
33
    assert response.status_code == 200
    assert json.loads(response.content) == render_json(expected)


Eliot Berriot's avatar
Eliot Berriot committed
34
@pytest.mark.parametrize("f", ["xml", "json"])
35
def test_exception_wrong_credentials(f, db, api_client):
Eliot Berriot's avatar
Eliot Berriot committed
36
37
    url = reverse("api:subsonic-ping")
    response = api_client.get(url, {"f": f, "u": "yolo"})
38
39

    expected = {
Eliot Berriot's avatar
Eliot Berriot committed
40
41
        "status": "failed",
        "error": {"code": 40, "message": "Wrong username or password."},
42
43
44
45
46
    }
    assert response.status_code == 200
    assert response.data == expected


47
48
@pytest.mark.parametrize("f", ["json"])
def test_exception_missing_credentials(f, db, api_client):
Eliot Berriot's avatar
Eliot Berriot committed
49
    url = reverse("api:subsonic-get_artists")
50
51
52
53
54
55
56
57
58
59
60
    response = api_client.get(url)

    expected = {
        "status": "failed",
        "error": {"code": 10, "message": "Required parameter is missing."},
    }

    assert response.status_code == 200
    assert response.data == expected


61
def test_disabled_subsonic(preferences, api_client):
Eliot Berriot's avatar
Eliot Berriot committed
62
63
    preferences["subsonic__enabled"] = False
    url = reverse("api:subsonic-ping")
64
65
66
67
    response = api_client.get(url)
    assert response.status_code == 405


Eliot Berriot's avatar
Eliot Berriot committed
68
@pytest.mark.parametrize("f", ["xml", "json"])
69
def test_get_license(f, db, logged_in_api_client, mocker):
Eliot Berriot's avatar
Eliot Berriot committed
70
    url = reverse("api:subsonic-get_license")
Eliot Berriot's avatar
Eliot Berriot committed
71
    assert url.endswith("getLicense") is True
72
    now = timezone.now()
Eliot Berriot's avatar
Eliot Berriot committed
73
74
    mocker.patch("django.utils.timezone.now", return_value=now)
    response = logged_in_api_client.get(url, {"f": f})
75
    expected = {
Eliot Berriot's avatar
Eliot Berriot committed
76
77
        "status": "ok",
        "version": "1.16.0",
78
79
        "type": "funkwhale",
        "funkwhaleVersion": funkwhale_api.__version__,
Eliot Berriot's avatar
Eliot Berriot committed
80
81
82
83
84
        "license": {
            "valid": "true",
            "email": "valid@valid.license",
            "licenseExpires": now + datetime.timedelta(days=365),
        },
85
86
87
88
89
    }
    assert response.status_code == 200
    assert response.data == expected


Eliot Berriot's avatar
Eliot Berriot committed
90
@pytest.mark.parametrize("f", ["xml", "json"])
91
def test_ping(f, db, api_client):
Eliot Berriot's avatar
Eliot Berriot committed
92
93
    url = reverse("api:subsonic-ping")
    response = api_client.get(url, {"f": f})
94

Eliot Berriot's avatar
Eliot Berriot committed
95
    expected = {"status": "ok", "version": "1.16.0"}
96
97
98
99
    assert response.status_code == 200
    assert response.data == expected


100
@pytest.mark.parametrize("f", ["json"])
101
102
103
def test_get_artists(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
104
105
106
107
    factories["moderation.UserFilter"](
        user=logged_in_api_client.user,
        target_artist=factories["music.Artist"](playable=True),
    )
Eliot Berriot's avatar
Eliot Berriot committed
108
    url = reverse("api:subsonic-get_artists")
Eliot Berriot's avatar
Eliot Berriot committed
109
    assert url.endswith("getArtists") is True
110
111
    factories["music.Artist"].create_batch(size=3, playable=True)
    playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
112
113
114
115
    exclude_query = moderation_filters.get_filtered_content_query(
        moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
    )
    assert exclude_query is not None
116
    expected = {
Eliot Berriot's avatar
Eliot Berriot committed
117
        "artists": serializers.GetArtistsSerializer(
118
            music_models.Artist.objects.all().exclude(exclude_query)
119
120
        ).data
    }
Eliot Berriot's avatar
Eliot Berriot committed
121
    response = logged_in_api_client.get(url, {"f": f})
122
123
124

    assert response.status_code == 200
    assert response.data == expected
125
126
127
128
    playable_by.assert_called_once_with(
        music_models.Artist.objects.all().exclude(exclude_query),
        logged_in_api_client.user.actor,
    )
129
130


131
@pytest.mark.parametrize("f", ["json"])
132
133
134
def test_get_artist(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
Eliot Berriot's avatar
Eliot Berriot committed
135
    url = reverse("api:subsonic-get_artist")
Eliot Berriot's avatar
Eliot Berriot committed
136
    assert url.endswith("getArtist") is True
137
138
139
140
    artist = factories["music.Artist"](playable=True)
    factories["music.Album"].create_batch(size=3, artist=artist, playable=True)
    playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")

Eliot Berriot's avatar
Eliot Berriot committed
141
142
    expected = {"artist": serializers.GetArtistSerializer(artist).data}
    response = logged_in_api_client.get(url, {"id": artist.pk})
143
144
145

    assert response.status_code == 200
    assert response.data == expected
146
    playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
147
148


149
@pytest.mark.parametrize("f", ["json"])
150
def test_get_invalid_artist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
151
    url = reverse("api:subsonic-get_artist")
152
153
154
155
156
157
158
159
    assert url.endswith("getArtist") is True
    expected = {"error": {"code": 0, "message": 'For input string "asdf"'}}
    response = logged_in_api_client.get(url, {"id": "asdf"})

    assert response.status_code == 200
    assert response.data == expected


160
@pytest.mark.parametrize("f", ["json"])
161
162
163
def test_get_artist_info2(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
Eliot Berriot's avatar
Eliot Berriot committed
164
    url = reverse("api:subsonic-get_artist_info2")
Eliot Berriot's avatar
Eliot Berriot committed
165
    assert url.endswith("getArtistInfo2") is True
166
167
    artist = factories["music.Artist"](playable=True)
    playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
168

Eliot Berriot's avatar
Eliot Berriot committed
169
170
    expected = {"artist-info2": {}}
    response = logged_in_api_client.get(url, {"id": artist.pk})
171
172
173
174

    assert response.status_code == 200
    assert response.data == expected

175
176
    playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)

177

178
@pytest.mark.parametrize("f", ["json"])
179
180
181
def test_get_album(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
Eliot Berriot's avatar
Eliot Berriot committed
182
    url = reverse("api:subsonic-get_album")
Eliot Berriot's avatar
Eliot Berriot committed
183
184
185
    assert url.endswith("getAlbum") is True
    artist = factories["music.Artist"]()
    album = factories["music.Album"](artist=artist)
186
187
    factories["music.Track"].create_batch(size=3, album=album, playable=True)
    playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
Eliot Berriot's avatar
Eliot Berriot committed
188
189
    expected = {"album": serializers.GetAlbumSerializer(album).data}
    response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
190
191
192
193

    assert response.status_code == 200
    assert response.data == expected

194
195
196
197
    playable_by.assert_called_once_with(
        music_models.Album.objects.select_related("artist"), None
    )

198

199
@pytest.mark.parametrize("f", ["json"])
200
201
202
def test_get_song(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
Eliot Berriot's avatar
Eliot Berriot committed
203
    url = reverse("api:subsonic-get_song")
204
205
206
    assert url.endswith("getSong") is True
    artist = factories["music.Artist"]()
    album = factories["music.Album"](artist=artist)
207
    track = factories["music.Track"](album=album, playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
208
    upload = factories["music.Upload"](track=track)
209
    playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
210
211
212
    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})

    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
213
214
215
    assert response.data == {
        "song": serializers.get_track_data(track.album, track, upload)
    }
216
    playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
217
218


219
@pytest.mark.parametrize("f", ["json"])
220
221
222
223
224
225
def test_stream(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
):
    # Even with this settings set to false, we proxy media in the subsonic API
    # Because clients don't expect a 302 redirect
    settings.PROXY_MEDIA = False
Eliot Berriot's avatar
Eliot Berriot committed
226
227
228
    url = reverse("api:subsonic-stream")
    mocked_serve = mocker.spy(music_views, "handle_serve")
    assert url.endswith("stream") is True
229
230
231
    upload = factories["music.Upload"](playable=True)
    playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
    response = logged_in_api_client.get(url, {"f": f, "id": upload.track.pk})
Eliot Berriot's avatar
Eliot Berriot committed
232

233
    mocked_serve.assert_called_once_with(
234
235
236
237
238
        upload=upload,
        user=logged_in_api_client.user,
        format=None,
        max_bitrate=None,
        proxy_media=True,
239
    )
240
    assert response.status_code == 200
241
    playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
242
243


244
@pytest.mark.parametrize("format,expected", [("mp3", "mp3"), ("raw", None)])
245
246
def test_stream_format(format, expected, logged_in_api_client, factories, mocker):
    url = reverse("api:subsonic-stream")
247
248
249
    mocked_serve = mocker.patch.object(
        music_views, "handle_serve", return_value=Response()
    )
250
251
252
    upload = factories["music.Upload"](playable=True)
    response = logged_in_api_client.get(url, {"id": upload.track.pk, "format": format})

253
    mocked_serve.assert_called_once_with(
254
255
256
257
258
        upload=upload,
        user=logged_in_api_client.user,
        format=expected,
        max_bitrate=None,
        proxy_media=True,
259
260
261
262
263
    )
    assert response.status_code == 200


@pytest.mark.parametrize(
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
    "max_bitrate,format,default_transcoding_format,expected_bitrate,expected_format",
    [
        # no max bitrate, no format, so no transcoding should happen
        (0, "", "ogg", None, None),
        # same using "raw" format
        (0, "raw", "ogg", None, None),
        # specified bitrate, but no format, so fallback to default transcoding format
        (192, "", "ogg", 192000, "ogg"),
        # specified bitrate, but over limit
        (2000, "", "ogg", 320000, "ogg"),
        # specified format, we use that one
        (192, "opus", "ogg", 192000, "opus"),
        # No default transcoding format set and no format requested
        (192, "", None, 192000, None),
    ],
279
)
280
281
282
283
284
285
286
287
288
289
290
291
def test_stream_transcode(
    max_bitrate,
    format,
    default_transcoding_format,
    expected_bitrate,
    expected_format,
    logged_in_api_client,
    factories,
    mocker,
    settings,
):
    settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
292
293
294
295
296
297
    url = reverse("api:subsonic-stream")
    mocked_serve = mocker.patch.object(
        music_views, "handle_serve", return_value=Response()
    )
    upload = factories["music.Upload"](playable=True)
    response = logged_in_api_client.get(
298
        url, {"id": upload.track.pk, "maxBitRate": max_bitrate, "format": format}
299
300
301
    )

    mocked_serve.assert_called_once_with(
302
303
        upload=upload,
        user=logged_in_api_client.user,
304
305
        format=expected_format,
        max_bitrate=expected_bitrate,
306
        proxy_media=True,
307
    )
308
309
310
    assert response.status_code == 200


311
@pytest.mark.parametrize("f", ["json"])
312
def test_star(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
313
314
315
316
    url = reverse("api:subsonic-star")
    assert url.endswith("star") is True
    track = factories["music.Track"]()
    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
317
318

    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
319
    assert response.data == {"status": "ok"}
320

Eliot Berriot's avatar
Eliot Berriot committed
321
    favorite = logged_in_api_client.user.track_favorites.latest("id")
322
323
324
    assert favorite.track == track


325
@pytest.mark.parametrize("f", ["json"])
326
def test_unstar(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
327
328
329
    url = reverse("api:subsonic-unstar")
    assert url.endswith("unstar") is True
    track = factories["music.Track"]()
330
    factories["favorites.TrackFavorite"](track=track, user=logged_in_api_client.user)
Eliot Berriot's avatar
Eliot Berriot committed
331
    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
332
333

    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
334
    assert response.data == {"status": "ok"}
335
336
337
    assert logged_in_api_client.user.track_favorites.count() == 0


338
@pytest.mark.parametrize("f", ["json"])
339
def test_get_starred2(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
340
    url = reverse("api:subsonic-get_starred2")
Eliot Berriot's avatar
Eliot Berriot committed
341
342
343
344
345
346
    assert url.endswith("getStarred2") is True
    track = factories["music.Track"]()
    favorite = factories["favorites.TrackFavorite"](
        track=track, user=logged_in_api_client.user
    )
    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
347
348
349

    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
350
        "starred2": {"song": serializers.get_starred_tracks_data([favorite])}
351
352
353
    }


354
355
@pytest.mark.parametrize("f", ["json"])
def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
Eliot Berriot's avatar
Eliot Berriot committed
356
    url = reverse("api:subsonic-get_random_songs")
357
358
359
360
361
362
    assert url.endswith("getRandomSongs") is True
    track1 = factories["music.Track"]()
    track2 = factories["music.Track"]()
    factories["music.Track"]()

    order_by = mocker.patch.object(
Eliot Berriot's avatar
Eliot Berriot committed
363
        music_models.TrackQuerySet, "order_by", return_value=[track1, track2]
364
365
366
367
368
    )
    response = logged_in_api_client.get(url, {"f": f, "size": 2})

    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
369
370
371
        "randomSongs": {
            "song": serializers.GetSongSerializer([track1, track2], many=True).data
        }
372
373
374
375
376
    }

    order_by.assert_called_once_with("?")


377
@pytest.mark.parametrize("f", ["json"])
378
def test_get_starred(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
379
    url = reverse("api:subsonic-get_starred")
Eliot Berriot's avatar
Eliot Berriot committed
380
381
382
383
384
385
    assert url.endswith("getStarred") is True
    track = factories["music.Track"]()
    favorite = factories["favorites.TrackFavorite"](
        track=track, user=logged_in_api_client.user
    )
    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
386
387
388

    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
389
        "starred": {"song": serializers.get_starred_tracks_data([favorite])}
390
391
392
    }


393
@pytest.mark.parametrize("f", ["json"])
394
395
396
def test_get_album_list2(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
Eliot Berriot's avatar
Eliot Berriot committed
397
    url = reverse("api:subsonic-get_album_list2")
Eliot Berriot's avatar
Eliot Berriot committed
398
    assert url.endswith("getAlbumList2") is True
399
400
401
402
    album1 = factories["music.Album"](playable=True)
    album2 = factories["music.Album"](playable=True)
    factories["music.Album"]()
    playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
Eliot Berriot's avatar
Eliot Berriot committed
403
    response = logged_in_api_client.get(url, {"f": f, "type": "newest"})
404
405
406

    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
407
        "albumList2": {"album": serializers.get_album_list2_data([album2, album1])}
408
    }
409
    playable_by.assert_called_once()
410
411


412
@pytest.mark.parametrize("f", ["json"])
413
def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
414
    url = reverse("api:subsonic-get_album_list2")
415
    assert url.endswith("getAlbumList2") is True
416
417
    album1 = factories["music.Album"](playable=True)
    factories["music.Album"](playable=True)
418
419
420
421
422
423
424
425
426
427
    response = logged_in_api_client.get(
        url, {"f": f, "type": "newest", "size": 1, "offset": 1}
    )

    assert response.status_code == 200
    assert response.data == {
        "albumList2": {"album": serializers.get_album_list2_data([album1])}
    }


428
@pytest.mark.parametrize("f", ["json"])
429
def test_search3(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
430
431
    url = reverse("api:subsonic-search3")
    assert url.endswith("search3") is True
432
    artist = factories["music.Artist"](name="testvalue", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
433
    factories["music.Artist"](name="nope")
434
435
    factories["music.Artist"](name="nope2", playable=True)
    album = factories["music.Album"](title="testvalue", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
436
    factories["music.Album"](title="nope")
437
438
    factories["music.Album"](title="nope2", playable=True)
    track = factories["music.Track"](title="testvalue", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
439
    factories["music.Track"](title="nope")
440
    factories["music.Track"](title="nope2", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
441
442
443
444
445
446
447
448

    response = logged_in_api_client.get(url, {"f": f, "query": "testval"})

    artist_qs = (
        music_models.Artist.objects.with_albums_count()
        .filter(pk=artist.pk)
        .values("_albums_count", "id", "name")
    )
449
450
    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
451
452
453
454
        "searchResult3": {
            "artist": [serializers.get_artist_data(a) for a in artist_qs],
            "album": serializers.get_album_list2_data([album]),
            "song": serializers.get_song_list_data([track]),
455
456
        }
    }
457
458


459
@pytest.mark.parametrize("f", ["json"])
460
def test_get_playlists(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
461
    url = reverse("api:subsonic-get_playlists")
Eliot Berriot's avatar
Eliot Berriot committed
462
    assert url.endswith("getPlaylists") is True
463
464
465
466
467
468
469
470
471
472
473
474
475
    playlist1 = factories["playlists.PlaylistTrack"](
        playlist__user=logged_in_api_client.user
    ).playlist
    playlist2 = factories["playlists.PlaylistTrack"](
        playlist__privacy_level="everyone"
    ).playlist
    playlist3 = factories["playlists.PlaylistTrack"](
        playlist__privacy_level="instance"
    ).playlist
    # private
    factories["playlists.PlaylistTrack"](playlist__privacy_level="me")
    # no track
    factories["playlists.Playlist"](privacy_level="everyone")
Eliot Berriot's avatar
Eliot Berriot committed
476
    response = logged_in_api_client.get(url, {"f": f})
477

478
479
480
481
482
483
484
    qs = (
        playlist1.__class__.objects.with_tracks_count()
        .filter(pk__in=[playlist1.pk, playlist2.pk, playlist3.pk])
        .order_by("-creation_date")
    )
    expected = {
        "playlists": {"playlist": [serializers.get_playlist_data(p) for p in qs]}
485
    }
486
487
    assert response.status_code == 200
    assert response.data == expected
488
489


490
@pytest.mark.parametrize("f", ["json"])
491
def test_get_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
492
    url = reverse("api:subsonic-get_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
493
    assert url.endswith("getPlaylist") is True
494
495
496
    playlist = factories["playlists.PlaylistTrack"](
        playlist__user=logged_in_api_client.user
    ).playlist
Eliot Berriot's avatar
Eliot Berriot committed
497
    response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
498
499
500
501

    qs = playlist.__class__.objects.with_tracks_count()
    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
502
        "playlist": serializers.get_playlist_detail_data(qs.first())
503
504
505
    }


506
@pytest.mark.parametrize("f", ["json"])
507
def test_update_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
508
    url = reverse("api:subsonic-update_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
509
510
    assert url.endswith("updatePlaylist") is True
    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
511
    factories["playlists.PlaylistTrack"](index=0, playlist=playlist)
Eliot Berriot's avatar
Eliot Berriot committed
512
    new_track = factories["music.Track"]()
513
    response = logged_in_api_client.get(
Eliot Berriot's avatar
Eliot Berriot committed
514
515
516
517
518
519
520
521
522
        url,
        {
            "f": f,
            "name": "new_name",
            "playlistId": playlist.pk,
            "songIdToAdd": new_track.pk,
            "songIndexToRemove": 0,
        },
    )
523
524
    playlist.refresh_from_db()
    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
525
    assert playlist.name == "new_name"
526
527
528
529
    assert playlist.playlist_tracks.count() == 1
    assert playlist.playlist_tracks.first().track_id == new_track.pk


530
@pytest.mark.parametrize("f", ["json"])
531
def test_delete_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
532
    url = reverse("api:subsonic-delete_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
533
534
535
    assert url.endswith("deletePlaylist") is True
    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
    response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
536
537
538
539
540
    assert response.status_code == 200
    with pytest.raises(playlist.__class__.DoesNotExist):
        playlist.refresh_from_db()


541
@pytest.mark.parametrize("f", ["json"])
542
def test_create_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
543
    url = reverse("api:subsonic-create_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
544
545
546
    assert url.endswith("createPlaylist") is True
    track1 = factories["music.Track"]()
    track2 = factories["music.Track"]()
547
    response = logged_in_api_client.get(
Eliot Berriot's avatar
Eliot Berriot committed
548
549
        url, {"f": f, "name": "hello", "songId": [track1.pk, track2.pk]}
    )
550
    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
551
    playlist = logged_in_api_client.user.playlists.latest("id")
552
553
554
555
    assert playlist.playlist_tracks.count() == 2
    for i, t in enumerate([track1, track2]):
        plt = playlist.playlist_tracks.get(track=t)
        assert plt.index == i
Eliot Berriot's avatar
Eliot Berriot committed
556
    assert playlist.name == "hello"
557
558
    qs = playlist.__class__.objects.with_tracks_count()
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
559
        "playlist": serializers.get_playlist_detail_data(qs.first())
560
    }
561
562


563
@pytest.mark.parametrize("f", ["json"])
564
def test_get_music_folders(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
565
    url = reverse("api:subsonic-get_music_folders")
Eliot Berriot's avatar
Eliot Berriot committed
566
567
    assert url.endswith("getMusicFolders") is True
    response = logged_in_api_client.get(url, {"f": f})
568
569
    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
570
        "musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}
571
572
573
    }


574
@pytest.mark.parametrize("f", ["json"])
575
576
577
def test_get_indexes(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
578
579
580
581
582
583
584
585
    factories["moderation.UserFilter"](
        user=logged_in_api_client.user,
        target_artist=factories["music.Artist"](playable=True),
    )
    exclude_query = moderation_filters.get_filtered_content_query(
        moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
    )

Eliot Berriot's avatar
Eliot Berriot committed
586
    url = reverse("api:subsonic-get_indexes")
Eliot Berriot's avatar
Eliot Berriot committed
587
    assert url.endswith("getIndexes") is True
588
    factories["music.Artist"].create_batch(size=3, playable=True)
589
    expected = {
Eliot Berriot's avatar
Eliot Berriot committed
590
        "indexes": serializers.GetArtistsSerializer(
591
            music_models.Artist.objects.all().exclude(exclude_query)
592
593
        ).data
    }
594
    playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
595
596
597
598
    response = logged_in_api_client.get(url)

    assert response.status_code == 200
    assert response.data == expected
599

600
601
602
603
    playable_by.assert_called_once_with(
        music_models.Artist.objects.all().exclude(exclude_query),
        logged_in_api_client.user.actor,
    )
604

605
606

def test_get_cover_art_album(factories, logged_in_api_client):
Eliot Berriot's avatar
Eliot Berriot committed
607
    url = reverse("api:subsonic-get_cover_art")
Eliot Berriot's avatar
Eliot Berriot committed
608
609
610
    assert url.endswith("getCoverArt") is True
    album = factories["music.Album"]()
    response = logged_in_api_client.get(url, {"id": "al-{}".format(album.pk)})
611
612

    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
613
614
    assert response["Content-Type"] == ""
    assert response["X-Accel-Redirect"] == music_views.get_file_path(
615
        album.cover
Eliot Berriot's avatar
Eliot Berriot committed
616
    ).decode("utf-8")
617
618


619
620
def test_get_avatar(factories, logged_in_api_client):
    user = factories["users.User"]()
Eliot Berriot's avatar
Eliot Berriot committed
621
    url = reverse("api:subsonic-get_avatar")
622
623
624
625
626
627
628
629
630
631
    assert url.endswith("getAvatar") is True
    response = logged_in_api_client.get(url, {"username": user.username})

    assert response.status_code == 200
    assert response["Content-Type"] == ""
    assert response["X-Accel-Redirect"] == music_views.get_file_path(
        user.avatar
    ).decode("utf-8")


632
def test_scrobble(factories, logged_in_api_client):
Eliot Berriot's avatar
Eliot Berriot committed
633
634
    upload = factories["music.Upload"]()
    track = upload.track
Eliot Berriot's avatar
Eliot Berriot committed
635
636
637
    url = reverse("api:subsonic-scrobble")
    assert url.endswith("scrobble") is True
    response = logged_in_api_client.get(url, {"id": track.pk, "submission": True})
638
639
640

    assert response.status_code == 200

641
642
    listening = logged_in_api_client.user.listenings.latest("id")
    assert listening.track == track
643
644
645
646


@pytest.mark.parametrize("f", ["json"])
def test_get_user(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
647
    url = reverse("api:subsonic-get_user")
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
    assert url.endswith("getUser") is True
    response = logged_in_api_client.get(
        url, {"f": f, "username": logged_in_api_client.user.username}
    )
    assert response.status_code == 200
    assert response.data == {
        "user": {
            "username": logged_in_api_client.user.username,
            "email": logged_in_api_client.user.email,
            "scrobblingEnabled": "true",
            "adminRole": "false",
            "downloadRole": "true",
            "uploadRole": "true",
            "settingsRole": "false",
            "playlistRole": "true",
            "commentRole": "false",
            "podcastRole": "false",
            "streamRole": "true",
            "jukeboxRole": "true",
            "coverArtRole": "false",
            "shareRole": "false",
            "folder": [
                f["id"] for f in serializers.get_folders(logged_in_api_client.user)
            ],
        }
    }