test_views.py 26.9 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
def test_stream_transcode(
    max_bitrate,
    format,
    default_transcoding_format,
    expected_bitrate,
    expected_format,
    logged_in_api_client,
    factories,
    mocker,
    settings,
):
291
292
293
294
    upload = factories["music.Upload"](playable=True)
    params = {"id": upload.track.pk, "maxBitRate": max_bitrate}
    if format:
        params["format"] = format
295
    settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
296
297
298
299
    url = reverse("api:subsonic-stream")
    mocked_serve = mocker.patch.object(
        music_views, "handle_serve", return_value=Response()
    )
300
    response = logged_in_api_client.get(url, params)
301
302

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


312
@pytest.mark.parametrize("f", ["json"])
313
def test_star(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
314
315
316
317
    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})
318
319

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

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


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

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


339
@pytest.mark.parametrize("f", ["json"])
340
def test_get_starred2(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
341
    url = reverse("api:subsonic-get_starred2")
Eliot Berriot's avatar
Eliot Berriot committed
342
343
344
345
346
347
    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})
348
349
350

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


355
356
@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
357
    url = reverse("api:subsonic-get_random_songs")
358
359
360
361
362
363
    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
364
        music_models.TrackQuerySet, "order_by", return_value=[track1, track2]
365
366
367
368
369
    )
    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
370
371
372
        "randomSongs": {
            "song": serializers.GetSongSerializer([track1, track2], many=True).data
        }
373
374
375
376
377
    }

    order_by.assert_called_once_with("?")


378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
@pytest.mark.parametrize("f", ["json"])
def test_get_genres(f, db, logged_in_api_client, factories, mocker):
    url = reverse("api:subsonic-get_genres")
    assert url.endswith("getGenres") is True
    tag1 = factories["tags.Tag"](name="Pop")
    tag2 = factories["tags.Tag"](name="Rock")

    factories["music.Album"](set_tags=[tag1.name, tag2.name])
    factories["music.Track"](set_tags=[tag1.name])
    response = logged_in_api_client.get(url)

    assert response.status_code == 200
    assert response.data == {
        "genres": {
            "genre": [
                {"songCount": 1, "albumCount": 1, "value": tag1.name},
                {"songCount": 0, "albumCount": 1, "value": tag2.name},
            ]
        }
    }


400
@pytest.mark.parametrize("f", ["json"])
401
def test_get_starred(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
402
    url = reverse("api:subsonic-get_starred")
Eliot Berriot's avatar
Eliot Berriot committed
403
404
405
406
407
408
    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})
409
410
411

    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
412
        "starred": {"song": serializers.get_starred_tracks_data([favorite])}
413
414
415
    }


416
@pytest.mark.parametrize("f", ["json"])
417
418
419
def test_get_album_list2(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
Eliot Berriot's avatar
Eliot Berriot committed
420
    url = reverse("api:subsonic-get_album_list2")
Eliot Berriot's avatar
Eliot Berriot committed
421
    assert url.endswith("getAlbumList2") is True
422
423
424
425
    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
426
    response = logged_in_api_client.get(url, {"f": f, "type": "newest"})
427
428
429

    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
430
        "albumList2": {"album": serializers.get_album_list2_data([album2, album1])}
431
    }
432
    playable_by.assert_called_once()
433
434


435
@pytest.mark.parametrize("f", ["json"])
436
def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
437
    url = reverse("api:subsonic-get_album_list2")
438
    assert url.endswith("getAlbumList2") is True
439
440
    album1 = factories["music.Album"](playable=True)
    factories["music.Album"](playable=True)
441
442
443
444
445
446
447
448
449
450
    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])}
    }


451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
@pytest.mark.parametrize("f", ["json"])
def test_get_album_list2_by_genre(f, db, logged_in_api_client, factories):
    url = reverse("api:subsonic-get_album_list2")
    assert url.endswith("getAlbumList2") is True
    album1 = factories["music.Album"](
        artist__name="Artist1", playable=True, set_tags=["Rock"]
    )
    album2 = factories["music.Album"](
        artist__name="Artist2", playable=True, artist__set_tags=["Rock"]
    )
    factories["music.Album"](playable=True, set_tags=["Pop"])
    response = logged_in_api_client.get(
        url, {"f": f, "type": "byGenre", "size": 5, "offset": 0, "genre": "rock"}
    )

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


472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
@pytest.mark.parametrize(
    "params, expected",
    [
        ({"type": "byYear", "fromYear": 1902, "toYear": 1903}, [2, 3]),
        # Because why not, it's supported in Subsonic API…
        # http://www.subsonic.org/pages/api.jsp#getAlbumList2
        ({"type": "byYear", "fromYear": 1903, "toYear": 1902}, [3, 2]),
    ],
)
def test_get_album_list2_by_year(params, expected, db, logged_in_api_client, factories):
    albums = [
        factories["music.Album"](
            playable=True, release_date=datetime.date(1900 + i, 1, 1)
        )
        for i in range(5)
    ]
    url = reverse("api:subsonic-get_album_list2")
    base_params = {"f": "json"}
    base_params.update(params)
    response = logged_in_api_client.get(url, base_params)

    assert response.status_code == 200
    assert response.data == {
        "albumList2": {
            "album": serializers.get_album_list2_data([albums[i] for i in expected])
        }
    }


501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
@pytest.mark.parametrize("f", ["json"])
@pytest.mark.parametrize(
    "tags_field",
    ["set_tags", "artist__set_tags", "album__set_tags", "album__artist__set_tags"],
)
def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
    url = reverse("api:subsonic-get_songs_by_genre")
    assert url.endswith("getSongsByGenre") is True
    track1 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
    track2 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
    factories["music.Track"](playable=True, **{tags_field: ["Pop"]})
    expected = {
        "songsByGenre": {"song": serializers.get_song_list_data([track2, track1])}
    }

    response = logged_in_api_client.get(
        url, {"f": f, "count": 5, "offset": 0, "genre": "rock"}
    )
    assert response.status_code == 200
    assert response.data == expected


523
@pytest.mark.parametrize("f", ["json"])
524
def test_search3(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
525
526
    url = reverse("api:subsonic-search3")
    assert url.endswith("search3") is True
527
    artist = factories["music.Artist"](name="testvalue", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
528
    factories["music.Artist"](name="nope")
529
530
    factories["music.Artist"](name="nope2", playable=True)
    album = factories["music.Album"](title="testvalue", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
531
    factories["music.Album"](title="nope")
532
533
    factories["music.Album"](title="nope2", playable=True)
    track = factories["music.Track"](title="testvalue", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
534
    factories["music.Track"](title="nope")
535
    factories["music.Track"](title="nope2", playable=True)
Eliot Berriot's avatar
Eliot Berriot committed
536
537
538
539
540
541
542
543

    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")
    )
544
545
    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
546
547
548
549
        "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]),
550
551
        }
    }
552
553


554
@pytest.mark.parametrize("f", ["json"])
555
def test_get_playlists(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
556
    url = reverse("api:subsonic-get_playlists")
Eliot Berriot's avatar
Eliot Berriot committed
557
    assert url.endswith("getPlaylists") is True
558
559
560
561
562
563
564
565
566
567
568
569
570
    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
571
    response = logged_in_api_client.get(url, {"f": f})
572

573
574
575
576
577
578
579
    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]}
580
    }
581
582
    assert response.status_code == 200
    assert response.data == expected
583
584


585
@pytest.mark.parametrize("f", ["json"])
586
def test_get_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
587
    url = reverse("api:subsonic-get_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
588
    assert url.endswith("getPlaylist") is True
589
590
591
    playlist = factories["playlists.PlaylistTrack"](
        playlist__user=logged_in_api_client.user
    ).playlist
Eliot Berriot's avatar
Eliot Berriot committed
592
    response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
593
594
595
596

    qs = playlist.__class__.objects.with_tracks_count()
    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
597
        "playlist": serializers.get_playlist_detail_data(qs.first())
598
599
600
    }


601
@pytest.mark.parametrize("f", ["json"])
602
def test_update_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
603
    url = reverse("api:subsonic-update_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
604
605
    assert url.endswith("updatePlaylist") is True
    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
606
    factories["playlists.PlaylistTrack"](index=0, playlist=playlist)
Eliot Berriot's avatar
Eliot Berriot committed
607
    new_track = factories["music.Track"]()
608
    response = logged_in_api_client.get(
Eliot Berriot's avatar
Eliot Berriot committed
609
610
611
612
613
614
615
616
617
        url,
        {
            "f": f,
            "name": "new_name",
            "playlistId": playlist.pk,
            "songIdToAdd": new_track.pk,
            "songIndexToRemove": 0,
        },
    )
618
619
    playlist.refresh_from_db()
    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
620
    assert playlist.name == "new_name"
621
622
623
624
    assert playlist.playlist_tracks.count() == 1
    assert playlist.playlist_tracks.first().track_id == new_track.pk


625
@pytest.mark.parametrize("f", ["json"])
626
def test_delete_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
627
    url = reverse("api:subsonic-delete_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
628
629
630
    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})
631
632
633
634
635
    assert response.status_code == 200
    with pytest.raises(playlist.__class__.DoesNotExist):
        playlist.refresh_from_db()


636
@pytest.mark.parametrize("f", ["json"])
637
def test_create_playlist(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
638
    url = reverse("api:subsonic-create_playlist")
Eliot Berriot's avatar
Eliot Berriot committed
639
640
641
    assert url.endswith("createPlaylist") is True
    track1 = factories["music.Track"]()
    track2 = factories["music.Track"]()
642
    response = logged_in_api_client.get(
Eliot Berriot's avatar
Eliot Berriot committed
643
644
        url, {"f": f, "name": "hello", "songId": [track1.pk, track2.pk]}
    )
645
    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
646
    playlist = logged_in_api_client.user.playlists.latest("id")
647
648
649
650
    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
651
    assert playlist.name == "hello"
652
653
    qs = playlist.__class__.objects.with_tracks_count()
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
654
        "playlist": serializers.get_playlist_detail_data(qs.first())
655
    }
656
657


658
@pytest.mark.parametrize("f", ["json"])
659
def test_get_music_folders(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
660
    url = reverse("api:subsonic-get_music_folders")
Eliot Berriot's avatar
Eliot Berriot committed
661
662
    assert url.endswith("getMusicFolders") is True
    response = logged_in_api_client.get(url, {"f": f})
663
664
    assert response.status_code == 200
    assert response.data == {
Eliot Berriot's avatar
Eliot Berriot committed
665
        "musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}
666
667
668
    }


669
@pytest.mark.parametrize("f", ["json"])
670
671
672
def test_get_indexes(
    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
673
674
675
676
677
678
679
680
    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
681
    url = reverse("api:subsonic-get_indexes")
Eliot Berriot's avatar
Eliot Berriot committed
682
    assert url.endswith("getIndexes") is True
683
    factories["music.Artist"].create_batch(size=3, playable=True)
684
    expected = {
Eliot Berriot's avatar
Eliot Berriot committed
685
        "indexes": serializers.GetArtistsSerializer(
686
            music_models.Artist.objects.all().exclude(exclude_query)
687
688
        ).data
    }
689
    playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
690
691
692
693
    response = logged_in_api_client.get(url)

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

695
696
697
698
    playable_by.assert_called_once_with(
        music_models.Artist.objects.all().exclude(exclude_query),
        logged_in_api_client.user.actor,
    )
699

700
701

def test_get_cover_art_album(factories, logged_in_api_client):
Eliot Berriot's avatar
Eliot Berriot committed
702
    url = reverse("api:subsonic-get_cover_art")
Eliot Berriot's avatar
Eliot Berriot committed
703
704
705
    assert url.endswith("getCoverArt") is True
    album = factories["music.Album"]()
    response = logged_in_api_client.get(url, {"id": "al-{}".format(album.pk)})
706
707

    assert response.status_code == 200
Eliot Berriot's avatar
Eliot Berriot committed
708
709
    assert response["Content-Type"] == ""
    assert response["X-Accel-Redirect"] == music_views.get_file_path(
710
        album.cover
Eliot Berriot's avatar
Eliot Berriot committed
711
    ).decode("utf-8")
712
713


714
715
def test_get_avatar(factories, logged_in_api_client):
    user = factories["users.User"]()
Eliot Berriot's avatar
Eliot Berriot committed
716
    url = reverse("api:subsonic-get_avatar")
717
718
719
720
721
722
723
724
725
726
    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")


727
def test_scrobble(factories, logged_in_api_client):
Eliot Berriot's avatar
Eliot Berriot committed
728
729
    upload = factories["music.Upload"]()
    track = upload.track
Eliot Berriot's avatar
Eliot Berriot committed
730
731
732
    url = reverse("api:subsonic-scrobble")
    assert url.endswith("scrobble") is True
    response = logged_in_api_client.get(url, {"id": track.pk, "submission": True})
733
734
735

    assert response.status_code == 200

736
737
    listening = logged_in_api_client.user.listenings.latest("id")
    assert listening.track == track
738
739
740
741


@pytest.mark.parametrize("f", ["json"])
def test_get_user(f, db, logged_in_api_client, factories):
Eliot Berriot's avatar
Eliot Berriot committed
742
    url = reverse("api:subsonic-get_user")
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
    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)
            ],
        }
    }