import arrow import pytest from django.core.paginator import Paginator from funkwhale_api.federation import actors, models, serializers, utils def test_actor_serializer_from_ap(db): payload = { "id": "https://test.federation/user", "type": "Person", "following": "https://test.federation/user/following", "followers": "https://test.federation/user/followers", "inbox": "https://test.federation/user/inbox", "outbox": "https://test.federation/user/outbox", "preferredUsername": "user", "name": "Real User", "summary": "Hello world", "url": "https://test.federation/@user", "manuallyApprovesFollowers": False, "publicKey": { "id": "https://test.federation/user#main-key", "owner": "https://test.federation/user", "publicKeyPem": "yolo", }, "endpoints": {"sharedInbox": "https://test.federation/inbox"}, } serializer = serializers.ActorSerializer(data=payload) assert serializer.is_valid(raise_exception=True) actor = serializer.build() assert actor.url == payload["id"] assert actor.inbox_url == payload["inbox"] assert actor.outbox_url == payload["outbox"] assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"] assert actor.followers_url == payload["followers"] assert actor.following_url == payload["following"] assert actor.public_key == payload["publicKey"]["publicKeyPem"] assert actor.preferred_username == payload["preferredUsername"] assert actor.name == payload["name"] assert actor.domain == "test.federation" assert actor.summary == payload["summary"] assert actor.type == "Person" assert actor.manually_approves_followers == payload["manuallyApprovesFollowers"] def test_actor_serializer_only_mandatory_field_from_ap(db): payload = { "id": "https://test.federation/user", "type": "Person", "following": "https://test.federation/user/following", "followers": "https://test.federation/user/followers", "inbox": "https://test.federation/user/inbox", "outbox": "https://test.federation/user/outbox", "preferredUsername": "user", } serializer = serializers.ActorSerializer(data=payload) assert serializer.is_valid(raise_exception=True) actor = serializer.build() assert actor.url == payload["id"] assert actor.inbox_url == payload["inbox"] assert actor.outbox_url == payload["outbox"] assert actor.followers_url == payload["followers"] assert actor.following_url == payload["following"] assert actor.preferred_username == payload["preferredUsername"] assert actor.domain == "test.federation" assert actor.type == "Person" assert actor.manually_approves_followers is None def test_actor_serializer_to_ap(): expected = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": "https://test.federation/user", "type": "Person", "following": "https://test.federation/user/following", "followers": "https://test.federation/user/followers", "inbox": "https://test.federation/user/inbox", "outbox": "https://test.federation/user/outbox", "preferredUsername": "user", "name": "Real User", "summary": "Hello world", "manuallyApprovesFollowers": False, "publicKey": { "id": "https://test.federation/user#main-key", "owner": "https://test.federation/user", "publicKeyPem": "yolo", }, "endpoints": {"sharedInbox": "https://test.federation/inbox"}, } ac = models.Actor( url=expected["id"], inbox_url=expected["inbox"], outbox_url=expected["outbox"], shared_inbox_url=expected["endpoints"]["sharedInbox"], followers_url=expected["followers"], following_url=expected["following"], public_key=expected["publicKey"]["publicKeyPem"], preferred_username=expected["preferredUsername"], name=expected["name"], domain="test.federation", summary=expected["summary"], type="Person", manually_approves_followers=False, ) serializer = serializers.ActorSerializer(ac) assert serializer.data == expected def test_webfinger_serializer(): expected = { "subject": "acct:service@test.federation", "links": [ { "rel": "self", "href": "https://test.federation/federation/instance/actor", "type": "application/activity+json", } ], "aliases": ["https://test.federation/federation/instance/actor"], } actor = models.Actor( url=expected["links"][0]["href"], preferred_username="service", domain="test.federation", ) serializer = serializers.ActorWebfingerSerializer(actor) assert serializer.data == expected def test_follow_serializer_to_ap(factories): follow = factories["federation.Follow"](local=True) serializer = serializers.FollowSerializer(follow) expected = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url(), "type": "Follow", "actor": follow.actor.url, "object": follow.target.url, } assert serializer.data == expected def test_follow_serializer_save(factories): actor = factories["federation.Actor"]() target = factories["federation.Actor"]() data = { "id": "https://test.follow", "type": "Follow", "actor": actor.url, "object": target.url, } serializer = serializers.FollowSerializer(data=data) assert serializer.is_valid(raise_exception=True) follow = serializer.save() assert follow.pk is not None assert follow.actor == actor assert follow.target == target assert follow.approved is None def test_follow_serializer_save_validates_on_context(factories): actor = factories["federation.Actor"]() target = factories["federation.Actor"]() impostor = factories["federation.Actor"]() data = { "id": "https://test.follow", "type": "Follow", "actor": actor.url, "object": target.url, } serializer = serializers.FollowSerializer( data=data, context={"follow_actor": impostor, "follow_target": impostor} ) assert serializer.is_valid() is False assert "actor" in serializer.errors assert "object" in serializer.errors def test_accept_follow_serializer_representation(factories): follow = factories["federation.Follow"](approved=None) expected = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url() + "/accept", "type": "Accept", "actor": follow.target.url, "object": serializers.FollowSerializer(follow).data, } serializer = serializers.AcceptFollowSerializer(follow) assert serializer.data == expected def test_accept_follow_serializer_save(factories): follow = factories["federation.Follow"](approved=None) data = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url() + "/accept", "type": "Accept", "actor": follow.target.url, "object": serializers.FollowSerializer(follow).data, } serializer = serializers.AcceptFollowSerializer(data=data) assert serializer.is_valid(raise_exception=True) serializer.save() follow.refresh_from_db() assert follow.approved is True def test_accept_follow_serializer_validates_on_context(factories): follow = factories["federation.Follow"](approved=None) impostor = factories["federation.Actor"]() data = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url() + "/accept", "type": "Accept", "actor": impostor.url, "object": serializers.FollowSerializer(follow).data, } serializer = serializers.AcceptFollowSerializer( data=data, context={"follow_actor": impostor, "follow_target": impostor} ) assert serializer.is_valid() is False assert "actor" in serializer.errors["object"] assert "object" in serializer.errors["object"] def test_undo_follow_serializer_representation(factories): follow = factories["federation.Follow"](approved=True) expected = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url() + "/undo", "type": "Undo", "actor": follow.actor.url, "object": serializers.FollowSerializer(follow).data, } serializer = serializers.UndoFollowSerializer(follow) assert serializer.data == expected def test_undo_follow_serializer_save(factories): follow = factories["federation.Follow"](approved=True) data = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url() + "/undo", "type": "Undo", "actor": follow.actor.url, "object": serializers.FollowSerializer(follow).data, } serializer = serializers.UndoFollowSerializer(data=data) assert serializer.is_valid(raise_exception=True) serializer.save() with pytest.raises(models.Follow.DoesNotExist): follow.refresh_from_db() def test_undo_follow_serializer_validates_on_context(factories): follow = factories["federation.Follow"](approved=True) impostor = factories["federation.Actor"]() data = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "id": follow.get_federation_url() + "/undo", "type": "Undo", "actor": impostor.url, "object": serializers.FollowSerializer(follow).data, } serializer = serializers.UndoFollowSerializer( data=data, context={"follow_actor": impostor, "follow_target": impostor} ) assert serializer.is_valid() is False assert "actor" in serializer.errors["object"] assert "object" in serializer.errors["object"] def test_paginated_collection_serializer(factories): tfs = factories["music.TrackFile"].create_batch(size=5) actor = factories["federation.Actor"](local=True) conf = { "id": "https://test.federation/test", "items": tfs, "item_serializer": serializers.AudioSerializer, "actor": actor, "page_size": 2, } expected = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "type": "Collection", "id": conf["id"], "actor": actor.url, "totalItems": len(tfs), "current": conf["id"] + "?page=1", "last": conf["id"] + "?page=3", "first": conf["id"] + "?page=1", } serializer = serializers.PaginatedCollectionSerializer(conf) assert serializer.data == expected def test_paginated_collection_serializer_validation(): data = { "type": "Collection", "id": "https://test.federation/test", "totalItems": 5, "actor": "http://test.actor", "first": "https://test.federation/test?page=1", "last": "https://test.federation/test?page=1", "items": [], } serializer = serializers.PaginatedCollectionSerializer(data=data) assert serializer.is_valid(raise_exception=True) is True assert serializer.validated_data["totalItems"] == 5 assert serializer.validated_data["id"] == data["id"] assert serializer.validated_data["actor"] == data["actor"] def test_collection_page_serializer_validation(): base = "https://test.federation/test" data = { "type": "CollectionPage", "id": base + "?page=2", "totalItems": 5, "actor": "https://test.actor", "items": [], "first": "https://test.federation/test?page=1", "last": "https://test.federation/test?page=3", "prev": base + "?page=1", "next": base + "?page=3", "partOf": base, } serializer = serializers.CollectionPageSerializer(data=data) assert serializer.is_valid(raise_exception=True) is True assert serializer.validated_data["totalItems"] == 5 assert serializer.validated_data["id"] == data["id"] assert serializer.validated_data["actor"] == data["actor"] assert serializer.validated_data["items"] == [] assert serializer.validated_data["prev"] == data["prev"] assert serializer.validated_data["next"] == data["next"] assert serializer.validated_data["partOf"] == data["partOf"] def test_collection_page_serializer_can_validate_child(): data = { "type": "CollectionPage", "id": "https://test.page?page=2", "actor": "https://test.actor", "first": "https://test.page?page=1", "last": "https://test.page?page=3", "partOf": "https://test.page", "totalItems": 1, "items": [{"in": "valid"}], } serializer = serializers.CollectionPageSerializer( data=data, context={"item_serializer": serializers.AudioSerializer} ) # child are validated but not included in data if not valid assert serializer.is_valid(raise_exception=True) is True assert len(serializer.validated_data["items"]) == 0 def test_collection_page_serializer(factories): tfs = factories["music.TrackFile"].create_batch(size=5) actor = factories["federation.Actor"](local=True) conf = { "id": "https://test.federation/test", "item_serializer": serializers.AudioSerializer, "actor": actor, "page": Paginator(tfs, 2).page(2), } expected = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}, ], "type": "CollectionPage", "id": conf["id"] + "?page=2", "actor": actor.url, "totalItems": len(tfs), "partOf": conf["id"], "prev": conf["id"] + "?page=1", "next": conf["id"] + "?page=3", "first": conf["id"] + "?page=1", "last": conf["id"] + "?page=3", "items": [ conf["item_serializer"]( i, context={"actor": actor, "include_ap_context": False} ).data for i in conf["page"].object_list ], } serializer = serializers.CollectionPageSerializer(conf) assert serializer.data == expected def test_activity_pub_audio_serializer_to_library_track(factories): remote_library = factories["federation.Library"]() audio = factories["federation.Audio"]() serializer = serializers.AudioSerializer( data=audio, context={"library": remote_library} ) assert serializer.is_valid(raise_exception=True) lt = serializer.save() assert lt.pk is not None assert lt.url == audio["id"] assert lt.library == remote_library assert lt.audio_url == audio["url"]["href"] assert lt.audio_mimetype == audio["url"]["mediaType"] assert lt.metadata == audio["metadata"] assert lt.title == audio["metadata"]["recording"]["title"] assert lt.artist_name == audio["metadata"]["artist"]["name"] assert lt.album_title == audio["metadata"]["release"]["title"] assert lt.published_date == arrow.get(audio["published"]) def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories): remote_library = factories["federation.Library"]() audio = factories["federation.Audio"]() serializer1 = serializers.AudioSerializer( data=audio, context={"library": remote_library} ) serializer2 = serializers.AudioSerializer( data=audio, context={"library": remote_library} ) assert serializer1.is_valid() is True assert serializer2.is_valid() is True lt1 = serializer1.save() lt2 = serializer2.save() assert lt1 == lt2 assert models.LibraryTrack.objects.count() == 1 def test_activity_pub_audio_serializer_to_ap(factories): tf = factories["music.TrackFile"]( mimetype="audio/mp3", bitrate=42, duration=43, size=44 ) library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { "@context": serializers.AP_CONTEXT, "type": "Audio", "id": tf.get_federation_url(), "name": tf.track.full_name, "published": tf.creation_date.isoformat(), "updated": tf.modification_date.isoformat(), "metadata": { "artist": { "musicbrainz_id": tf.track.artist.mbid, "name": tf.track.artist.name, }, "release": { "musicbrainz_id": tf.track.album.mbid, "title": tf.track.album.title, }, "recording": {"musicbrainz_id": tf.track.mbid, "title": tf.track.title}, "size": tf.size, "length": tf.duration, "bitrate": tf.bitrate, }, "url": { "href": utils.full_url(tf.path), "type": "Link", "mediaType": "audio/mp3", }, "attributedTo": [library.url], } serializer = serializers.AudioSerializer(tf, context={"actor": library}) assert serializer.data == expected def test_activity_pub_audio_serializer_to_ap_no_mbid(factories): tf = factories["music.TrackFile"]( mimetype="audio/mp3", track__mbid=None, track__album__mbid=None, track__album__artist__mbid=None, ) library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { "@context": serializers.AP_CONTEXT, "type": "Audio", "id": tf.get_federation_url(), "name": tf.track.full_name, "published": tf.creation_date.isoformat(), "updated": tf.modification_date.isoformat(), "metadata": { "artist": {"name": tf.track.artist.name, "musicbrainz_id": None}, "release": {"title": tf.track.album.title, "musicbrainz_id": None}, "recording": {"title": tf.track.title, "musicbrainz_id": None}, "size": None, "length": None, "bitrate": None, }, "url": { "href": utils.full_url(tf.path), "type": "Link", "mediaType": "audio/mp3", }, "attributedTo": [library.url], } serializer = serializers.AudioSerializer(tf, context={"actor": library}) assert serializer.data == expected def test_collection_serializer_to_ap(factories): tf1 = factories["music.TrackFile"](mimetype="audio/mp3") tf2 = factories["music.TrackFile"](mimetype="audio/ogg") library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { "@context": serializers.AP_CONTEXT, "id": "https://test.id", "actor": library.url, "totalItems": 2, "type": "Collection", "items": [ serializers.AudioSerializer( tf1, context={"actor": library, "include_ap_context": False} ).data, serializers.AudioSerializer( tf2, context={"actor": library, "include_ap_context": False} ).data, ], } collection = { "id": expected["id"], "actor": library, "items": [tf1, tf2], "item_serializer": serializers.AudioSerializer, } serializer = serializers.CollectionSerializer( collection, context={"actor": library, "id": "https://test.id"} ) assert serializer.data == expected def test_api_library_create_serializer_save(factories, r_mock): library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() actor = factories["federation.Actor"]() follow = factories["federation.Follow"](target=actor, actor=library_actor) actor_data = serializers.ActorSerializer(actor).data actor_data["url"] = [ {"href": "https://test.library", "name": "library", "type": "Link"} ] library_conf = { "id": "https://test.library", "items": range(10), "actor": actor, "page_size": 5, } library_data = serializers.PaginatedCollectionSerializer(library_conf).data r_mock.get(actor.url, json=actor_data) r_mock.get("https://test.library", json=library_data) data = { "actor": actor.url, "autoimport": False, "federation_enabled": True, "download_files": False, } serializer = serializers.APILibraryCreateSerializer(data=data) assert serializer.is_valid(raise_exception=True) is True library = serializer.save() follow = models.Follow.objects.get(target=actor, actor=library_actor, approved=None) assert library.autoimport is data["autoimport"] assert library.federation_enabled is data["federation_enabled"] assert library.download_files is data["download_files"] assert library.tracks_count == 10 assert library.actor == actor assert library.follow == follow def test_tapi_library_track_serializer_not_imported(factories): lt = factories["federation.LibraryTrack"]() serializer = serializers.APILibraryTrackSerializer(lt) assert serializer.get_status(lt) == "not_imported" def test_tapi_library_track_serializer_imported(factories): tf = factories["music.TrackFile"](federation=True) lt = tf.library_track serializer = serializers.APILibraryTrackSerializer(lt) assert serializer.get_status(lt) == "imported" def test_tapi_library_track_serializer_import_pending(factories): job = factories["music.ImportJob"](federation=True, status="pending") lt = job.library_track serializer = serializers.APILibraryTrackSerializer(lt) assert serializer.get_status(lt) == "import_pending"