test_views.py 15.4 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

Agate's avatar
Agate committed
16

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


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

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

Agate's avatar
Agate committed
40

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

    assert response.status_code == 405
58 59


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

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


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

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


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


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


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

    assert response.status_code == 403


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

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


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

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

158

159
def test_audio_file_list_actor_page_exclude_federated_files(
Agate's avatar
Agate committed
160 161 162
    db, preferences, api_client, factories
):
    preferences["federation__music_needs_approval"] = False
163
    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
    url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
192 193 194
    response = api_client.get(url)
    expected_links = [
        {
Agate's avatar
Agate committed
195 196 197 198
            "type": "Link",
            "name": "library",
            "mediaType": "application/activity+json",
            "href": utils.full_url(reverse("federation:music:files-list")),
199 200 201
        }
    ]
    assert response.status_code == 200
Agate's avatar
Agate committed
202
    assert response.data["url"] == expected_links
203 204


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

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

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


219
def test_follow_library(superuser_api_client, mocker, factories, r_mock):
Agate's avatar
Agate committed
220 221 222 223
    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")
224
    actor_data = serializers.ActorSerializer(actor).data
Agate's avatar
Agate committed
225 226 227
    actor_data["url"] = [
        {"href": "https://test.library", "name": "library", "type": "Link"}
    ]
228
    library_conf = {
Agate's avatar
Agate committed
229 230 231 232
        "id": "https://test.library",
        "items": range(10),
        "actor": actor,
        "page_size": 5,
233 234 235
    }
    library_data = serializers.PaginatedCollectionSerializer(library_conf).data
    r_mock.get(actor.url, json=actor_data)
Agate's avatar
Agate committed
236
    r_mock.get("https://test.library", json=library_data)
237
    data = {
Agate's avatar
Agate committed
238 239 240 241
        "actor": actor.url,
        "autoimport": False,
        "federation_enabled": True,
        "download_files": False,
242
    }
243

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

    assert response.status_code == 201

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

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

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


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

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

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


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

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

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


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

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

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


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

    url = reverse(
Agate's avatar
Agate committed
304 305
        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
    )
306 307 308 309 310 311 312
    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
313
    library = factories["federation.Library"]()
314
    data = {
Agate's avatar
Agate committed
315 316 317
        "federation_enabled": not library.federation_enabled,
        "download_files": not library.download_files,
        "autoimport": not library.autoimport,
318 319
    }
    url = reverse(
Agate's avatar
Agate committed
320 321
        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
    )
322 323 324 325 326 327 328
    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
329 330 331 332


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

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


def test_list_library_tracks(factories, superuser_api_client):
Agate's avatar
Agate committed
350 351 352 353 354 355 356 357 358
    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
359 360 361

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


def test_can_update_follow_status(factories, superuser_api_client, mocker):
Agate's avatar
Agate committed
370 371 372
    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
373

Agate's avatar
Agate committed
374 375
    payload = {"follow": follow.pk, "approved": True}
    url = reverse("api:v1:federation:libraries-followers")
Agate's avatar
Agate committed
376 377 378 379 380 381 382 383 384
    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
385
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
386
    factories["federation.Follow"](target=library_actor, approved=True)
Agate's avatar
Agate committed
387

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

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


Agate's avatar
Agate committed
396 397 398 399
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"]()
400
    factories["federation.LibraryTrack"](library=lt3.library)
Agate's avatar
Agate committed
401
    mocked_run = mocker.patch("funkwhale_api.music.tasks.import_batch_run.delay")
402 403

    payload = {
Agate's avatar
Agate committed
404 405 406
        "objects": "all",
        "action": "import",
        "filters": {"library": lt1.library.uuid},
407
    }
Agate's avatar
Agate committed
408 409 410 411
    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}}}
412 413 414 415 416 417 418

    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]
419
    mocked_run.assert_called_once_with(import_batch_id=batch.pk)
Agate's avatar
Agate committed
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444


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