test_views.py 14.9 KB
Newer Older
1
from django.core.paginator import Paginator
2 3
from django.urls import reverse
from django.utils import timezone
4 5 6

import pytest

Agate's avatar
Agate committed
7
from funkwhale_api.federation import actors
8
from funkwhale_api.federation import activity
9
from funkwhale_api.federation import models
Agate's avatar
Agate committed
10
from funkwhale_api.federation import serializers
11
from funkwhale_api.federation import utils
12
from funkwhale_api.federation import views
13 14
from funkwhale_api.federation import webfinger

Agate's avatar
Agate committed
15

Agate's avatar
Agate committed
16 17 18 19 20 21 22
@pytest.mark.parametrize(
    "view,permissions",
    [
        (views.LibraryViewSet, ["federation"]),
        (views.LibraryTrackViewSet, ["federation"]),
    ],
)
23 24 25 26
def test_permissions(assert_user_permission, view, permissions):
    assert_user_permission(view, permissions)


Agate's avatar
Agate committed
27
@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
28
def test_instance_actors(system_actor, db, api_client):
29
    actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
Agate's avatar
Agate committed
30
    url = reverse("federation:instance-actors-detail", kwargs={"actor": system_actor})
31
    response = api_client.get(url)
Agate's avatar
Agate committed
32
    serializer = serializers.ActorSerializer(actor)
Agate's avatar
Agate committed
33

Agate's avatar
Agate committed
34 35
    if system_actor == "library":
        response.data.pop("url")
Agate's avatar
Agate committed
36
    assert response.status_code == 200
Agate's avatar
Agate committed
37
    assert response.data == serializer.data
38

Agate's avatar
Agate committed
39

Agate's avatar
Agate committed
40 41 42 43 44 45 46 47 48
@pytest.mark.parametrize(
    "route,kwargs",
    [
        ("instance-actors-outbox", {"actor": "library"}),
        ("instance-actors-inbox", {"actor": "library"}),
        ("instance-actors-detail", {"actor": "library"}),
        ("well-known-webfinger", {}),
    ],
)
49
def test_instance_endpoints_405_if_federation_disabled(
Agate's avatar
Agate committed
50 51 52 53
    authenticated_actor, db, preferences, api_client, route, kwargs
):
    preferences["federation__enabled"] = False
    url = reverse("federation:{}".format(route), kwargs=kwargs)
Agate's avatar
Agate committed
54 55 56
    response = api_client.get(url)

    assert response.status_code == 405
57 58


Agate's avatar
Agate committed
59 60 61 62
def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker):
    clean = mocker.spy(webfinger, "clean_resource")
    url = reverse("federation:well-known-webfinger")
    response = api_client.get(url, data={"resource": "something"})
63

Agate's avatar
Agate committed
64 65
    clean.assert_called_once_with("something")
    assert url == "/.well-known/webfinger"
66
    assert response.status_code == 400
Agate's avatar
Agate committed
67
    assert response.data["errors"]["resource"] == ("Missing webfinger resource type")
68 69


Agate's avatar
Agate committed
70 71
@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
def test_wellknown_webfinger_system(system_actor, db, api_client, settings, mocker):
72
    actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
Agate's avatar
Agate committed
73
    url = reverse("federation:well-known-webfinger")
74
    response = api_client.get(
75
        url,
Agate's avatar
Agate committed
76 77
        data={"resource": "acct:{}".format(actor.webfinger_subject)},
        HTTP_ACCEPT="application/jrd+json",
78
    )
Agate's avatar
Agate committed
79
    serializer = serializers.ActorWebfingerSerializer(actor)
80 81

    assert response.status_code == 200
Agate's avatar
Agate committed
82
    assert response["Content-Type"] == "application/jrd+json"
Agate's avatar
Agate committed
83
    assert response.data == serializer.data
84 85


86 87
def test_wellknown_nodeinfo(db, preferences, api_client, settings):
    expected = {
Agate's avatar
Agate committed
88
        "links": [
89
            {
Agate's avatar
Agate committed
90 91 92 93
                "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
                "href": "{}{}".format(
                    settings.FUNKWHALE_URL, reverse("api:v1:instance:nodeinfo-2.0")
                ),
94 95 96
            }
        ]
    }
Agate's avatar
Agate committed
97 98
    url = reverse("federation:well-known-nodeinfo")
    response = api_client.get(url, HTTP_ACCEPT="application/jrd+json")
99
    assert response.status_code == 200
Agate's avatar
Agate committed
100
    assert response["Content-Type"] == "application/jrd+json"
101 102 103 104
    assert response.data == expected


def test_wellknown_nodeinfo_disabled(db, preferences, api_client):
Agate's avatar
Agate committed
105 106
    preferences["instance__nodeinfo_enabled"] = False
    url = reverse("federation:well-known-nodeinfo")
107 108 109 110
    response = api_client.get(url)
    assert response.status_code == 404


Agate's avatar
Agate committed
111 112 113
def test_audio_file_list_requires_authenticated_actor(db, preferences, api_client):
    preferences["federation__music_needs_approval"] = True
    url = reverse("federation:music:files-list")
114 115 116 117 118
    response = api_client.get(url)

    assert response.status_code == 403


Agate's avatar
Agate committed
119 120 121 122 123
def test_audio_file_list_actor_no_page(db, preferences, api_client, factories):
    preferences["federation__music_needs_approval"] = False
    preferences["federation__collection_page_size"] = 2
    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    tfs = factories["music.TrackFile"].create_batch(size=5)
124
    conf = {
Agate's avatar
Agate committed
125 126 127 128 129
        "id": utils.full_url(reverse("federation:music:files-list")),
        "page_size": 2,
        "items": list(reversed(tfs)),  # we order by -creation_date
        "item_serializer": serializers.AudioSerializer,
        "actor": library,
130 131
    }
    expected = serializers.PaginatedCollectionSerializer(conf).data
Agate's avatar
Agate committed
132
    url = reverse("federation:music:files-list")
133 134 135 136 137 138
    response = api_client.get(url)

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


Agate's avatar
Agate committed
139 140 141 142 143
def test_audio_file_list_actor_page(db, preferences, api_client, factories):
    preferences["federation__music_needs_approval"] = False
    preferences["federation__collection_page_size"] = 2
    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    tfs = factories["music.TrackFile"].create_batch(size=5)
144
    conf = {
Agate's avatar
Agate committed
145 146 147 148
        "id": utils.full_url(reverse("federation:music:files-list")),
        "page": Paginator(list(reversed(tfs)), 2).page(2),
        "item_serializer": serializers.AudioSerializer,
        "actor": library,
149 150
    }
    expected = serializers.CollectionPageSerializer(conf).data
Agate's avatar
Agate committed
151 152
    url = reverse("federation:music:files-list")
    response = api_client.get(url, data={"page": 2})
153 154 155 156

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

157

158
def test_audio_file_list_actor_page_exclude_federated_files(
Agate's avatar
Agate committed
159 160 161 162 163
    db, preferences, api_client, factories
):
    preferences["federation__music_needs_approval"] = False
    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    tfs = factories["music.TrackFile"].create_batch(size=5, federation=True)
164

Agate's avatar
Agate committed
165
    url = reverse("federation:music:files-list")
166 167 168
    response = api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
169
    assert response.data["totalItems"] == 0
170

171

Agate's avatar
Agate committed
172 173 174 175
def test_audio_file_list_actor_page_error(db, preferences, api_client, factories):
    preferences["federation__music_needs_approval"] = False
    url = reverse("federation:music:files-list")
    response = api_client.get(url, data={"page": "nope"})
176 177 178 179 180

    assert response.status_code == 400


def test_audio_file_list_actor_page_error_too_far(
Agate's avatar
Agate committed
181 182 183 184 185
    db, preferences, api_client, factories
):
    preferences["federation__music_needs_approval"] = False
    url = reverse("federation:music:files-list")
    response = api_client.get(url, data={"page": 5000})
186 187 188 189

    assert response.status_code == 404


190
def test_library_actor_includes_library_link(db, preferences, api_client):
Agate's avatar
Agate committed
191 192
    actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
193 194 195
    response = api_client.get(url)
    expected_links = [
        {
Agate's avatar
Agate committed
196 197 198 199
            "type": "Link",
            "name": "library",
            "mediaType": "application/activity+json",
            "href": utils.full_url(reverse("federation:music:files-list")),
200 201 202
        }
    ]
    assert response.status_code == 200
Agate's avatar
Agate committed
203
    assert response.data["url"] == expected_links
204 205


206
def test_can_fetch_library(superuser_api_client, mocker):
Agate's avatar
Agate committed
207
    result = {"test": "test"}
208
    scan = mocker.patch(
Agate's avatar
Agate committed
209 210
        "funkwhale_api.federation.library.scan_from_account_name", return_value=result
    )
211

Agate's avatar
Agate committed
212 213
    url = reverse("api:v1:federation:libraries-fetch")
    response = superuser_api_client.get(url, data={"account": "test@test.library"})
214 215 216

    assert response.status_code == 200
    assert response.data == result
Agate's avatar
Agate committed
217
    scan.assert_called_once_with("test@test.library")
218 219


220
def test_follow_library(superuser_api_client, mocker, factories, r_mock):
Agate's avatar
Agate committed
221 222 223 224
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    actor = factories["federation.Actor"]()
    follow = {"test": "follow"}
    on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
225
    actor_data = serializers.ActorSerializer(actor).data
Agate's avatar
Agate committed
226 227 228
    actor_data["url"] = [
        {"href": "https://test.library", "name": "library", "type": "Link"}
    ]
229
    library_conf = {
Agate's avatar
Agate committed
230 231 232 233
        "id": "https://test.library",
        "items": range(10),
        "actor": actor,
        "page_size": 5,
234 235 236
    }
    library_data = serializers.PaginatedCollectionSerializer(library_conf).data
    r_mock.get(actor.url, json=actor_data)
Agate's avatar
Agate committed
237
    r_mock.get("https://test.library", json=library_data)
238
    data = {
Agate's avatar
Agate committed
239 240 241 242
        "actor": actor.url,
        "autoimport": False,
        "federation_enabled": True,
        "download_files": False,
243
    }
244

Agate's avatar
Agate committed
245 246
    url = reverse("api:v1:federation:libraries-list")
    response = superuser_api_client.post(url, data)
247 248 249

    assert response.status_code == 201

Agate's avatar
Agate committed
250
    follow = models.Follow.objects.get(actor=library_actor, target=actor, approved=None)
251 252
    library = follow.library

Agate's avatar
Agate committed
253
    assert response.data == serializers.APILibraryCreateSerializer(library).data
254

255 256
    on_commit.assert_called_once_with(
        activity.deliver,
257 258
        serializers.FollowSerializer(follow).data,
        on_behalf_of=library_actor,
Agate's avatar
Agate committed
259
        to=[actor.url],
260
    )
261 262 263


def test_can_list_system_actor_following(factories, superuser_api_client):
Agate's avatar
Agate committed
264 265 266
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow1 = factories["federation.Follow"](actor=library_actor)
    follow2 = factories["federation.Follow"]()
267

Agate's avatar
Agate committed
268
    url = reverse("api:v1:federation:libraries-following")
269 270 271
    response = superuser_api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
272
    assert response.data["results"] == [serializers.APIFollowSerializer(follow1).data]
273 274 275


def test_can_list_system_actor_followers(factories, superuser_api_client):
Agate's avatar
Agate committed
276 277 278
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow1 = factories["federation.Follow"](actor=library_actor)
    follow2 = factories["federation.Follow"](target=library_actor)
279

Agate's avatar
Agate committed
280
    url = reverse("api:v1:federation:libraries-followers")
281 282 283
    response = superuser_api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
284
    assert response.data["results"] == [serializers.APIFollowSerializer(follow2).data]
Agate's avatar
Agate committed
285 286 287


def test_can_list_libraries(factories, superuser_api_client):
Agate's avatar
Agate committed
288 289
    library1 = factories["federation.Library"]()
    library2 = factories["federation.Library"]()
Agate's avatar
Agate committed
290

Agate's avatar
Agate committed
291
    url = reverse("api:v1:federation:libraries-list")
Agate's avatar
Agate committed
292 293 294
    response = superuser_api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
295
    assert response.data["results"] == [
Agate's avatar
Agate committed
296 297 298
        serializers.APILibrarySerializer(library1).data,
        serializers.APILibrarySerializer(library2).data,
    ]
299 300 301


def test_can_detail_library(factories, superuser_api_client):
Agate's avatar
Agate committed
302
    library = factories["federation.Library"]()
303 304

    url = reverse(
Agate's avatar
Agate committed
305 306
        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
    )
307 308 309 310 311 312 313
    response = superuser_api_client.get(url)

    assert response.status_code == 200
    assert response.data == serializers.APILibrarySerializer(library).data


def test_can_patch_library(factories, superuser_api_client):
Agate's avatar
Agate committed
314
    library = factories["federation.Library"]()
315
    data = {
Agate's avatar
Agate committed
316 317 318
        "federation_enabled": not library.federation_enabled,
        "download_files": not library.download_files,
        "autoimport": not library.autoimport,
319 320
    }
    url = reverse(
Agate's avatar
Agate committed
321 322
        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
    )
323 324 325 326 327 328 329
    response = superuser_api_client.patch(url, data)

    assert response.status_code == 200
    library.refresh_from_db()

    for k, v in data.items():
        assert getattr(library, k) == v
330 331 332 333


def test_scan_library(factories, mocker, superuser_api_client):
    scan = mocker.patch(
Agate's avatar
Agate committed
334 335 336 337
        "funkwhale_api.federation.tasks.scan_library.delay",
        return_value=mocker.Mock(id="id"),
    )
    library = factories["federation.Library"]()
338
    now = timezone.now()
Agate's avatar
Agate committed
339
    data = {"until": now}
340
    url = reverse(
Agate's avatar
Agate committed
341 342
        "api:v1:federation:libraries-scan", kwargs={"uuid": str(library.uuid)}
    )
343 344 345
    response = superuser_api_client.post(url, data)

    assert response.status_code == 200
Agate's avatar
Agate committed
346 347
    assert response.data == {"task": "id"}
    scan.assert_called_once_with(library_id=library.pk, until=now)
Agate's avatar
Agate committed
348 349 350


def test_list_library_tracks(factories, superuser_api_client):
Agate's avatar
Agate committed
351 352 353 354 355 356 357 358 359
    library = factories["federation.Library"]()
    lts = list(
        reversed(
            factories["federation.LibraryTrack"].create_batch(size=5, library=library)
        )
    )
    factories["federation.LibraryTrack"].create_batch(size=5)
    url = reverse("api:v1:federation:library-tracks-list")
    response = superuser_api_client.get(url, {"library": library.uuid})
Agate's avatar
Agate committed
360 361 362

    assert response.status_code == 200
    assert response.data == {
Agate's avatar
Agate committed
363 364 365 366
        "results": serializers.APILibraryTrackSerializer(lts, many=True).data,
        "count": 5,
        "previous": None,
        "next": None,
Agate's avatar
Agate committed
367
    }
Agate's avatar
Agate committed
368 369 370


def test_can_update_follow_status(factories, superuser_api_client, mocker):
Agate's avatar
Agate committed
371 372 373
    patched_accept = mocker.patch("funkwhale_api.federation.activity.accept_follow")
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow = factories["federation.Follow"](target=library_actor)
Agate's avatar
Agate committed
374

Agate's avatar
Agate committed
375 376
    payload = {"follow": follow.pk, "approved": True}
    url = reverse("api:v1:federation:libraries-followers")
Agate's avatar
Agate committed
377 378 379 380 381 382 383 384 385
    response = superuser_api_client.patch(url, payload)
    follow.refresh_from_db()

    assert response.status_code == 200
    assert follow.approved is True
    patched_accept.assert_called_once_with(follow)


def test_can_filter_pending_follows(factories, superuser_api_client):
Agate's avatar
Agate committed
386 387
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow = factories["federation.Follow"](target=library_actor, approved=True)
Agate's avatar
Agate committed
388

Agate's avatar
Agate committed
389 390
    params = {"pending": True}
    url = reverse("api:v1:federation:libraries-followers")
Agate's avatar
Agate committed
391 392 393
    response = superuser_api_client.get(url, params)

    assert response.status_code == 200
Agate's avatar
Agate committed
394
    assert len(response.data["results"]) == 0
395 396


Agate's avatar
Agate committed
397 398 399 400 401 402
def test_library_track_action_import(factories, superuser_api_client, mocker):
    lt1 = factories["federation.LibraryTrack"]()
    lt2 = factories["federation.LibraryTrack"](library=lt1.library)
    lt3 = factories["federation.LibraryTrack"]()
    lt4 = factories["federation.LibraryTrack"](library=lt3.library)
    mocked_run = mocker.patch("funkwhale_api.music.tasks.import_batch_run.delay")
403 404

    payload = {
Agate's avatar
Agate committed
405 406 407
        "objects": "all",
        "action": "import",
        "filters": {"library": lt1.library.uuid},
408
    }
Agate's avatar
Agate committed
409 410 411 412
    url = reverse("api:v1:federation:library-tracks-action")
    response = superuser_api_client.post(url, payload, format="json")
    batch = superuser_api_client.user.imports.latest("id")
    expected = {"updated": 2, "action": "import", "result": {"batch": {"id": batch.pk}}}
413 414 415 416 417 418 419

    imported_lts = [lt1, lt2]
    assert response.status_code == 200
    assert response.data == expected
    assert batch.jobs.count() == 2
    for i, job in enumerate(batch.jobs.all()):
        assert job.library_track == imported_lts[i]
420
    mocked_run.assert_called_once_with(import_batch_id=batch.pk)