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

Agate's avatar
Agate committed
6
from funkwhale_api.federation import activity, actors, models, serializers, utils, views, webfinger
7

Agate's avatar
Agate committed
8

Agate's avatar
Agate committed
9 10 11 12 13 14 15
@pytest.mark.parametrize(
    "view,permissions",
    [
        (views.LibraryViewSet, ["federation"]),
        (views.LibraryTrackViewSet, ["federation"]),
    ],
)
16 17 18 19
def test_permissions(assert_user_permission, view, permissions):
    assert_user_permission(view, permissions)


Agate's avatar
Agate committed
20
@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
21
def test_instance_actors(system_actor, db, api_client):
22
    actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
Agate's avatar
Agate committed
23
    url = reverse("federation:instance-actors-detail", kwargs={"actor": system_actor})
24
    response = api_client.get(url)
Agate's avatar
Agate committed
25
    serializer = serializers.ActorSerializer(actor)
Agate's avatar
Agate committed
26

Agate's avatar
Agate committed
27 28
    if system_actor == "library":
        response.data.pop("url")
Agate's avatar
Agate committed
29
    assert response.status_code == 200
Agate's avatar
Agate committed
30
    assert response.data == serializer.data
31

Agate's avatar
Agate committed
32

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

    assert response.status_code == 405
50 51


Agate's avatar
Agate committed
52 53 54 55
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"})
56

Agate's avatar
Agate committed
57 58
    clean.assert_called_once_with("something")
    assert url == "/.well-known/webfinger"
59
    assert response.status_code == 400
Agate's avatar
Agate committed
60
    assert response.data["errors"]["resource"] == ("Missing webfinger resource type")
61 62


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

    assert response.status_code == 200
Agate's avatar
Agate committed
75
    assert response["Content-Type"] == "application/jrd+json"
Agate's avatar
Agate committed
76
    assert response.data == serializer.data
77 78


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


def test_wellknown_nodeinfo_disabled(db, preferences, api_client):
Agate's avatar
Agate committed
98 99
    preferences["instance__nodeinfo_enabled"] = False
    url = reverse("federation:well-known-nodeinfo")
100 101 102 103
    response = api_client.get(url)
    assert response.status_code == 404


Agate's avatar
Agate committed
104 105 106
def test_audio_file_list_requires_authenticated_actor(db, preferences, api_client):
    preferences["federation__music_needs_approval"] = True
    url = reverse("federation:music:files-list")
107 108 109 110 111
    response = api_client.get(url)

    assert response.status_code == 403


Agate's avatar
Agate committed
112 113 114 115 116
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)
117
    conf = {
Agate's avatar
Agate committed
118 119 120 121 122
        "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,
123 124
    }
    expected = serializers.PaginatedCollectionSerializer(conf).data
Agate's avatar
Agate committed
125
    url = reverse("federation:music:files-list")
126 127 128 129 130 131
    response = api_client.get(url)

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


Agate's avatar
Agate committed
132 133 134 135 136
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)
137
    conf = {
Agate's avatar
Agate committed
138 139 140 141
        "id": utils.full_url(reverse("federation:music:files-list")),
        "page": Paginator(list(reversed(tfs)), 2).page(2),
        "item_serializer": serializers.AudioSerializer,
        "actor": library,
142 143
    }
    expected = serializers.CollectionPageSerializer(conf).data
Agate's avatar
Agate committed
144 145
    url = reverse("federation:music:files-list")
    response = api_client.get(url, data={"page": 2})
146 147 148 149

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

150

151
def test_audio_file_list_actor_page_exclude_federated_files(
Agate's avatar
Agate committed
152 153 154 155
    db, preferences, api_client, factories
):
    preferences["federation__music_needs_approval"] = False
    tfs = factories["music.TrackFile"].create_batch(size=5, federation=True)
156

Agate's avatar
Agate committed
157
    url = reverse("federation:music:files-list")
158 159 160
    response = api_client.get(url)

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

163

Agate's avatar
Agate committed
164 165 166 167
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"})
168 169 170 171 172

    assert response.status_code == 400


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

    assert response.status_code == 404


182
def test_library_actor_includes_library_link(db, preferences, api_client):
Agate's avatar
Agate committed
183
    url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
184 185 186
    response = api_client.get(url)
    expected_links = [
        {
Agate's avatar
Agate committed
187 188 189 190
            "type": "Link",
            "name": "library",
            "mediaType": "application/activity+json",
            "href": utils.full_url(reverse("federation:music:files-list")),
191 192 193
        }
    ]
    assert response.status_code == 200
Agate's avatar
Agate committed
194
    assert response.data["url"] == expected_links
195 196


197
def test_can_fetch_library(superuser_api_client, mocker):
Agate's avatar
Agate committed
198
    result = {"test": "test"}
199
    scan = mocker.patch(
Agate's avatar
Agate committed
200 201
        "funkwhale_api.federation.library.scan_from_account_name", return_value=result
    )
202

Agate's avatar
Agate committed
203 204
    url = reverse("api:v1:federation:libraries-fetch")
    response = superuser_api_client.get(url, data={"account": "test@test.library"})
205 206 207

    assert response.status_code == 200
    assert response.data == result
Agate's avatar
Agate committed
208
    scan.assert_called_once_with("test@test.library")
209 210


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

Agate's avatar
Agate committed
236 237
    url = reverse("api:v1:federation:libraries-list")
    response = superuser_api_client.post(url, data)
238 239 240

    assert response.status_code == 201

Agate's avatar
Agate committed
241
    follow = models.Follow.objects.get(actor=library_actor, target=actor, approved=None)
242 243
    library = follow.library

Agate's avatar
Agate committed
244
    assert response.data == serializers.APILibraryCreateSerializer(library).data
245

246 247
    on_commit.assert_called_once_with(
        activity.deliver,
248 249
        serializers.FollowSerializer(follow).data,
        on_behalf_of=library_actor,
Agate's avatar
Agate committed
250
        to=[actor.url],
251
    )
252 253 254


def test_can_list_system_actor_following(factories, superuser_api_client):
Agate's avatar
Agate committed
255 256
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow1 = factories["federation.Follow"](actor=library_actor)
Agate's avatar
Agate committed
257
    factories["federation.Follow"]()
258

Agate's avatar
Agate committed
259
    url = reverse("api:v1:federation:libraries-following")
260 261 262
    response = superuser_api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
263
    assert response.data["results"] == [serializers.APIFollowSerializer(follow1).data]
264 265 266


def test_can_list_system_actor_followers(factories, superuser_api_client):
Agate's avatar
Agate committed
267 268 269
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow1 = factories["federation.Follow"](actor=library_actor)
    follow2 = factories["federation.Follow"](target=library_actor)
270

Agate's avatar
Agate committed
271
    url = reverse("api:v1:federation:libraries-followers")
272 273 274
    response = superuser_api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
275
    assert response.data["results"] == [serializers.APIFollowSerializer(follow2).data]
Agate's avatar
Agate committed
276 277 278


def test_can_list_libraries(factories, superuser_api_client):
Agate's avatar
Agate committed
279 280
    library1 = factories["federation.Library"]()
    library2 = factories["federation.Library"]()
Agate's avatar
Agate committed
281

Agate's avatar
Agate committed
282
    url = reverse("api:v1:federation:libraries-list")
Agate's avatar
Agate committed
283 284 285
    response = superuser_api_client.get(url)

    assert response.status_code == 200
Agate's avatar
Agate committed
286
    assert response.data["results"] == [
Agate's avatar
Agate committed
287 288 289
        serializers.APILibrarySerializer(library1).data,
        serializers.APILibrarySerializer(library2).data,
    ]
290 291 292


def test_can_detail_library(factories, superuser_api_client):
Agate's avatar
Agate committed
293
    library = factories["federation.Library"]()
294 295

    url = reverse(
Agate's avatar
Agate committed
296 297
        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
    )
298 299 300 301 302 303 304
    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
305
    library = factories["federation.Library"]()
306
    data = {
Agate's avatar
Agate committed
307 308 309
        "federation_enabled": not library.federation_enabled,
        "download_files": not library.download_files,
        "autoimport": not library.autoimport,
310 311
    }
    url = reverse(
Agate's avatar
Agate committed
312 313
        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
    )
314 315 316 317 318 319 320
    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
321 322 323 324


def test_scan_library(factories, mocker, superuser_api_client):
    scan = mocker.patch(
Agate's avatar
Agate committed
325 326 327 328
        "funkwhale_api.federation.tasks.scan_library.delay",
        return_value=mocker.Mock(id="id"),
    )
    library = factories["federation.Library"]()
329
    now = timezone.now()
Agate's avatar
Agate committed
330
    data = {"until": now}
331
    url = reverse(
Agate's avatar
Agate committed
332 333
        "api:v1:federation:libraries-scan", kwargs={"uuid": str(library.uuid)}
    )
334 335 336
    response = superuser_api_client.post(url, data)

    assert response.status_code == 200
Agate's avatar
Agate committed
337 338
    assert response.data == {"task": "id"}
    scan.assert_called_once_with(library_id=library.pk, until=now)
Agate's avatar
Agate committed
339 340 341


def test_list_library_tracks(factories, superuser_api_client):
Agate's avatar
Agate committed
342 343 344 345 346 347 348 349 350
    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
351 352 353

    assert response.status_code == 200
    assert response.data == {
Agate's avatar
Agate committed
354 355 356 357
        "results": serializers.APILibraryTrackSerializer(lts, many=True).data,
        "count": 5,
        "previous": None,
        "next": None,
Agate's avatar
Agate committed
358
    }
Agate's avatar
Agate committed
359 360 361


def test_can_update_follow_status(factories, superuser_api_client, mocker):
Agate's avatar
Agate committed
362 363 364
    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
365

Agate's avatar
Agate committed
366 367
    payload = {"follow": follow.pk, "approved": True}
    url = reverse("api:v1:federation:libraries-followers")
Agate's avatar
Agate committed
368 369 370 371 372 373 374 375 376
    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
377 378
    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
    follow = factories["federation.Follow"](target=library_actor, approved=True)
Agate's avatar
Agate committed
379

Agate's avatar
Agate committed
380 381
    params = {"pending": True}
    url = reverse("api:v1:federation:libraries-followers")
Agate's avatar
Agate committed
382 383 384
    response = superuser_api_client.get(url, params)

    assert response.status_code == 200
Agate's avatar
Agate committed
385
    assert len(response.data["results"]) == 0
386 387


Agate's avatar
Agate committed
388 389 390 391 392 393
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")
394 395

    payload = {
Agate's avatar
Agate committed
396 397 398
        "objects": "all",
        "action": "import",
        "filters": {"library": lt1.library.uuid},
399
    }
Agate's avatar
Agate committed
400 401 402 403
    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}}}
404 405 406 407 408 409 410

    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]
411
    mocked_run.assert_called_once_with(import_batch_id=batch.pk)