test_views.py 15.5 KB
Newer Older
Agate's avatar
Agate committed
1
import pytest
2
from django.core.paginator import Paginator
3 4
from django.urls import reverse
from django.utils import timezone
5

6 7 8 9 10 11 12 13 14
from funkwhale_api.federation import (
    activity,
    actors,
    models,
    serializers,
    utils,
    views,
    webfinger,
)
15
from funkwhale_api.music import tasks as music_tasks
16

Agate's avatar
Agate committed
17

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


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

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

Agate's avatar
Agate committed
41

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

    assert response.status_code == 405
59 60


Agate's avatar
Agate committed
61 62 63 64
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"})
65

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


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

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


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


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


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

    assert response.status_code == 403


Agate's avatar
Agate committed
121 122 123 124 125
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)
126
    conf = {
Agate's avatar
Agate committed
127 128 129 130 131
        "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,
132 133
    }
    expected = serializers.PaginatedCollectionSerializer(conf).data
Agate's avatar
Agate committed
134
    url = reverse("federation:music:files-list")
135 136 137 138 139 140
    response = api_client.get(url)

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


Agate's avatar
Agate committed
141 142 143 144 145
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)
146
    conf = {
Agate's avatar
Agate committed
147 148 149 150
        "id": utils.full_url(reverse("federation:music:files-list")),
        "page": Paginator(list(reversed(tfs)), 2).page(2),
        "item_serializer": serializers.AudioSerializer,
        "actor": library,
151 152
    }
    expected = serializers.CollectionPageSerializer(conf).data
Agate's avatar
Agate committed
153 154
    url = reverse("federation:music:files-list")
    response = api_client.get(url, data={"page": 2})
155 156 157 158

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

159

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

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

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

172

Agate's avatar
Agate committed
173 174 175 176
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"})
177 178 179 180 181

    assert response.status_code == 400


def test_audio_file_list_actor_page_error_too_far(
Agate's avatar
Agate committed
182 183 184 185 186
    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})
187 188 189 190

    assert response.status_code == 404


191
def test_library_actor_includes_library_link(db, preferences, api_client):
Agate's avatar
Agate committed
192
    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
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow1 = factories["federation.Follow"](actor=library_actor)
Agate's avatar
Agate committed
266
    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
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
277
    factories["federation.Follow"](actor=library_actor)
Agate's avatar
Agate committed
278
    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
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
387
    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
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"]()
401
    factories["federation.LibraryTrack"](library=lt3.library)
402
    mocked_run = mocker.patch("funkwhale_api.common.utils.on_commit")
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 421 422
    mocked_run.assert_called_once_with(
        music_tasks.import_batch_run.delay, import_batch_id=batch.pk
    )
Agate's avatar
Agate committed
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447


def test_local_actor_detail(factories, api_client):
    user = factories["users.User"](with_actor=True)
    url = reverse("federation:actors-detail", kwargs={"user__username": user.username})
    serializer = serializers.ActorSerializer(user.actor)
    response = api_client.get(url)

    assert response.status_code == 200
    assert response.data == serializer.data


def test_wellknown_webfinger_local(factories, api_client, settings, mocker):
    user = factories["users.User"](with_actor=True)
    url = reverse("federation:well-known-webfinger")
    response = api_client.get(
        url,
        data={"resource": "acct:{}".format(user.actor.webfinger_subject)},
        HTTP_ACCEPT="application/jrd+json",
    )
    serializer = serializers.ActorWebfingerSerializer(user.actor)

    assert response.status_code == 200
    assert response["Content-Type"] == "application/jrd+json"
    assert response.data == serializer.data