Skip to content
Snippets Groups Projects
test_serializers.py 44.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • Eliot Berriot's avatar
    Eliot Berriot committed
    import io
    import pytest
    import uuid
    
    from django.core.paginator import Paginator
    from django.utils import timezone
    
    
    from funkwhale_api.common import utils as common_utils
    
    from funkwhale_api.federation import contexts
    
    from funkwhale_api.federation import keys
    from funkwhale_api.federation import jsonld
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from funkwhale_api.federation import models
    
    from funkwhale_api.federation import serializers
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from funkwhale_api.federation import utils
    from funkwhale_api.music import licenses
    
    def test_actor_serializer_from_ap(db):
    
        private, public = keys.get_key_pair()
        actor_url = "https://test.federation/actor"
    
        payload = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "@context": jsonld.get_default_context_fw(),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "type": "Person",
    
            "outbox": "https://test.com/outbox",
            "inbox": "https://test.com/inbox",
            "following": "https://test.com/following",
            "followers": "https://test.com/followers",
            "preferredUsername": "test",
            "name": "Test",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "summary": "Hello world",
    
            "manuallyApprovesFollowers": True,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "publicKey": {
    
                "publicKeyPem": public.decode("utf-8"),
                "owner": actor_url,
                "id": actor_url + "#main-key",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            },
    
            "endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"},
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "icon": {
                "type": "Image",
                "mediaType": "image/jpeg",
                "href": "https://image.example/image.png",
            },
    
        }
    
        serializer = serializers.ActorSerializer(data=payload)
    
        assert serializer.is_valid(raise_exception=True)
    
        assert actor.fid == actor_url
        assert actor.url is None
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        assert actor.inbox_url == payload["inbox"]
        assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"]
    
        assert actor.outbox_url == payload["outbox"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        assert actor.following_url == payload["following"]
    
        assert actor.followers_url == payload["followers"]
        assert actor.followers_url == payload["followers"]
        assert actor.type == "Person"
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        assert actor.preferred_username == payload["preferredUsername"]
        assert actor.name == payload["name"]
    
        assert actor.summary_obj.text == payload["summary"]
        assert actor.summary_obj.content_type == "text/html"
    
        assert actor.fid == actor_url
        assert actor.manually_approves_followers is True
        assert actor.private_key is None
        assert actor.public_key == payload["publicKey"]["publicKeyPem"]
        assert actor.domain_id == "test.federation"
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        assert actor.attachment_icon.url == payload["icon"]["href"]
        assert actor.attachment_icon.mimetype == payload["icon"]["mediaType"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    def test_actor_serializer_only_mandatory_field_from_ap(db):
        payload = {
            "@context": jsonld.get_default_context(),
            "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.fid == 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.pk == "test.federation"
        assert actor.type == "Person"
        assert actor.manually_approves_followers is None
    
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    def test_actor_serializer_to_ap(factories):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        expected = {
            "@context": jsonld.get_default_context(),
            "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",
            "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.objects.create(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            fid=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=models.Domain.objects.create(pk="test.federation"),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            type="Person",
            manually_approves_followers=False,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            attachment_icon=factories["common.Attachment"](),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        )
    
    
        content = common_utils.attach_content(
            ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
        )
        expected["summary"] = content.rendered
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        expected["icon"] = {
            "type": "Image",
            "mediaType": "image/jpeg",
            "href": utils.full_url(ac.attachment_icon.file.url),
        }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        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(
            fid=expected["links"][0]["href"],
            preferred_username="service",
            domain=models.Domain(pk="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": jsonld.get_default_context(),
            "id": follow.get_federation_id(),
            "type": "Follow",
            "actor": follow.actor.fid,
            "object": follow.target.fid,
        }
    
        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.fid,
            "object": target.fid,
        }
        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.fid,
            "object": target.fid,
        }
        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": jsonld.get_default_context(),
            "id": follow.get_federation_id() + "/accept",
            "type": "Accept",
            "actor": follow.target.fid,
            "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": jsonld.get_default_context_fw(),
            "id": follow.get_federation_id() + "/accept",
            "type": "Accept",
            "actor": follow.target.fid,
            "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": jsonld.get_default_context_fw(),
            "id": follow.get_federation_id() + "/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": jsonld.get_default_context(),
            "id": follow.get_federation_id() + "/undo",
            "type": "Undo",
            "actor": follow.actor.fid,
            "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": jsonld.get_default_context_fw(),
            "id": follow.get_federation_id() + "/undo",
            "type": "Undo",
            "actor": follow.actor.fid,
            "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": jsonld.get_default_context_fw(),
            "id": follow.get_federation_id() + "/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):
        uploads = factories["music.Upload"].create_batch(size=5)
        actor = factories["federation.Actor"](local=True)
    
        conf = {
            "id": "https://test.federation/test",
            "items": uploads,
            "item_serializer": serializers.UploadSerializer,
            "actor": actor,
            "page_size": 2,
        }
        expected = {
            "@context": jsonld.get_default_context(),
            "type": "Collection",
            "id": conf["id"],
            "actor": actor.fid,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "totalItems": len(uploads),
            "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 = {
            "@context": jsonld.get_default_context_fw(),
            "type": "Collection",
            "id": "https://test.federation/test",
            "totalItems": 5,
            "actor": "http://test.actor",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "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["attributedTo"] == data["actor"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    def test_collection_page_serializer_validation():
        base = "https://test.federation/test"
        data = {
            "@context": jsonld.get_default_context(),
            "type": "CollectionPage",
            "id": base + "?page=2",
            "totalItems": 5,
            "actor": "https://test.actor",
    
            "attributedTo": "https://test.actor",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "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["attributedTo"] == data["actor"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        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 = {
            "@context": jsonld.get_default_context(),
            "type": "CollectionPage",
            "id": "https://test.page?page=2",
    
            "attributedTo": "https://test.actor",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "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.UploadSerializer}
        )
    
        # 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):
        uploads = factories["music.Upload"].create_batch(size=5)
        actor = factories["federation.Actor"](local=True)
    
        conf = {
            "id": "https://test.federation/test",
            "item_serializer": serializers.UploadSerializer,
            "actor": actor,
            "page": Paginator(uploads, 2).page(2),
        }
        expected = {
            "@context": jsonld.get_default_context(),
            "type": "CollectionPage",
            "id": conf["id"] + "?page=2",
            "actor": actor.fid,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "totalItems": len(uploads),
            "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_music_library_serializer_to_ap(factories):
        library = factories["music.Library"](privacy_level="everyone")
        # pending, errored and skippednot included
        factories["music.Upload"](import_status="pending")
        factories["music.Upload"](import_status="errored")
        factories["music.Upload"](import_status="finished")
        serializer = serializers.LibrarySerializer(library)
        expected = {
            "@context": jsonld.get_default_context(),
            "audience": "https://www.w3.org/ns/activitystreams#Public",
            "type": "Library",
            "id": library.fid,
            "name": library.name,
            "summary": library.description,
            "actor": library.actor.fid,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "totalItems": 0,
            "current": library.fid + "?page=1",
            "last": library.fid + "?page=1",
            "first": library.fid + "?page=1",
            "followers": library.followers_url,
        }
    
        assert serializer.data == expected
    
    
    def test_music_library_serializer_from_public(factories, mocker):
        actor = factories["federation.Actor"]()
        retrieve = mocker.patch(
            "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor
        )
        data = {
            "@context": jsonld.get_default_context(),
            "audience": "https://www.w3.org/ns/activitystreams#Public",
            "name": "Hello",
            "summary": "World",
            "type": "Library",
            "id": "https://library.id",
            "followers": "https://library.id/followers",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "totalItems": 12,
            "first": "https://library.id?page=1",
            "last": "https://library.id?page=2",
        }
        serializer = serializers.LibrarySerializer(data=data)
    
        assert serializer.is_valid(raise_exception=True)
    
        library = serializer.save()
    
        assert library.actor == actor
        assert library.fid == data["id"]
        assert library.uploads_count == data["totalItems"]
        assert library.privacy_level == "everyone"
        assert library.name == "Hello"
        assert library.description == "World"
        assert library.followers_url == data["followers"]
    
        retrieve.assert_called_once_with(
            actor.fid,
            actor=None,
            queryset=actor.__class__,
            serializer_class=serializers.ActorSerializer,
        )
    
    
    def test_music_library_serializer_from_private(factories, mocker):
        actor = factories["federation.Actor"]()
        retrieve = mocker.patch(
            "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor
        )
        data = {
            "@context": jsonld.get_default_context_fw(),
            "audience": "",
            "name": "Hello",
            "summary": "World",
            "type": "Library",
            "id": "https://library.id",
            "followers": "https://library.id/followers",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "totalItems": 12,
            "first": "https://library.id?page=1",
            "last": "https://library.id?page=2",
        }
        serializer = serializers.LibrarySerializer(data=data)
    
        assert serializer.is_valid(raise_exception=True)
    
        library = serializer.save()
    
        assert library.actor == actor
        assert library.fid == data["id"]
        assert library.uploads_count == data["totalItems"]
        assert library.privacy_level == "me"
        assert library.name == "Hello"
        assert library.description == "World"
        assert library.followers_url == data["followers"]
        retrieve.assert_called_once_with(
            actor.fid,
            actor=None,
            queryset=actor.__class__,
            serializer_class=serializers.ActorSerializer,
        )
    
    
    def test_activity_pub_artist_serializer_to_ap(factories):
    
        content = factories["common.Content"]()
        artist = factories["music.Artist"](
            description=content, attributed=True, set_tags=["Punk", "Rock"]
        )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        expected = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "@context": jsonld.get_default_context(),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "type": "Artist",
            "id": artist.fid,
            "name": artist.name,
            "musicbrainzId": artist.mbid,
            "published": artist.creation_date.isoformat(),
            "attributedTo": artist.attributed_to.fid,
    
            "mediaType": "text/html",
            "content": common_utils.render_html(content.text, content.content_type),
    
            "image": {
                "type": "Image",
                "mediaType": "image/jpeg",
                "href": utils.full_url(artist.attachment_cover.file.url),
            },
    
            "tag": [
                {"type": "Hashtag", "name": "#Punk"},
                {"type": "Hashtag", "name": "#Rock"},
            ],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        }
        serializer = serializers.ArtistSerializer(artist)
    
        assert serializer.data == expected
    
    
    def test_activity_pub_album_serializer_to_ap(factories):
    
        content = factories["common.Content"]()
        album = factories["music.Album"](
            description=content, attributed=True, set_tags=["Punk", "Rock"]
        )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        expected = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "@context": jsonld.get_default_context(),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "type": "Album",
            "id": album.fid,
            "name": album.title,
            "cover": {
                "type": "Link",
                "mediaType": "image/jpeg",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "href": utils.full_url(album.attachment_cover.file.url),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            },
    
            "image": {
                "type": "Image",
                "mediaType": "image/jpeg",
                "href": utils.full_url(album.attachment_cover.file.url),
            },
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "musicbrainzId": album.mbid,
            "published": album.creation_date.isoformat(),
            "released": album.release_date.isoformat(),
            "artists": [
                serializers.ArtistSerializer(
                    album.artist, context={"include_ap_context": False}
                ).data
            ],
            "attributedTo": album.attributed_to.fid,
    
            "mediaType": "text/html",
            "content": common_utils.render_html(content.text, content.content_type),
    
            "tag": [
                {"type": "Hashtag", "name": "#Punk"},
                {"type": "Hashtag", "name": "#Rock"},
            ],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        }
        serializer = serializers.AlbumSerializer(album)
    
        assert serializer.data == expected
    
    
    
    def test_activity_pub_artist_serializer_from_ap_update(factories, faker):
        artist = factories["music.Artist"](attributed=True)
        payload = {
            "@context": jsonld.get_default_context(),
            "type": "Artist",
            "id": artist.fid,
            "name": faker.sentence(),
            "musicbrainzId": faker.uuid4(),
            "published": artist.creation_date.isoformat(),
            "attributedTo": artist.attributed_to.fid,
            "mediaType": "text/html",
            "content": common_utils.render_html(faker.sentence(), "text/html"),
            "image": {"type": "Image", "mediaType": "image/jpeg", "href": faker.url()},
            "tag": [
                {"type": "Hashtag", "name": "#Punk"},
                {"type": "Hashtag", "name": "#Rock"},
            ],
        }
    
        serializer = serializers.ArtistSerializer(artist, data=payload)
        assert serializer.is_valid(raise_exception=True) is True
    
        serializer.save()
    
        artist.refresh_from_db()
    
        assert artist.name == payload["name"]
        assert str(artist.mbid) == payload["musicbrainzId"]
        assert artist.attachment_cover.url == payload["image"]["href"]
        assert artist.attachment_cover.mimetype == payload["image"]["mediaType"]
        assert artist.description.text == payload["content"]
        assert artist.description.content_type == "text/html"
        assert sorted(artist.tagged_items.values_list("tag__name", flat=True)) == [
            "Punk",
            "Rock",
        ]
    
    
    
    def test_activity_pub_album_serializer_from_ap_update(factories, faker):
        album = factories["music.Album"](attributed=True)
        released = faker.date_object()
        payload = {
            "@context": jsonld.get_default_context(),
            "type": "Album",
            "id": album.fid,
            "name": faker.sentence(),
            "cover": {"type": "Link", "mediaType": "image/jpeg", "href": faker.url()},
            "musicbrainzId": faker.uuid4(),
            "published": album.creation_date.isoformat(),
            "released": released.isoformat(),
            "artists": [
                serializers.ArtistSerializer(
                    album.artist, context={"include_ap_context": False}
                ).data
            ],
            "attributedTo": album.attributed_to.fid,
            "tag": [
                {"type": "Hashtag", "name": "#Punk"},
                {"type": "Hashtag", "name": "#Rock"},
            ],
        }
        serializer = serializers.AlbumSerializer(album, data=payload)
        assert serializer.is_valid(raise_exception=True) is True
    
        serializer.save()
    
        album.refresh_from_db()
    
        assert album.title == payload["name"]
        assert str(album.mbid) == payload["musicbrainzId"]
        assert album.release_date == released
        assert album.attachment_cover.url == payload["cover"]["href"]
        assert album.attachment_cover.mimetype == payload["cover"]["mediaType"]
        assert sorted(album.tagged_items.values_list("tag__name", flat=True)) == [
            "Punk",
            "Rock",
        ]
    
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    def test_activity_pub_track_serializer_to_ap(factories):
    
        content = factories["common.Content"]()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        track = factories["music.Track"](
    
            license="cc-by-4.0",
            copyright="test",
            disc_number=3,
            attributed=True,
            set_tags=["Punk", "Rock"],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        )
        expected = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "@context": jsonld.get_default_context(),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "published": track.creation_date.isoformat(),
            "type": "Track",
            "musicbrainzId": track.mbid,
            "id": track.fid,
            "name": track.title,
            "position": track.position,
            "disc": track.disc_number,
            "license": track.license.conf["identifiers"][0],
            "copyright": "test",
            "artists": [
                serializers.ArtistSerializer(
                    track.artist, context={"include_ap_context": False}
                ).data
            ],
            "album": serializers.AlbumSerializer(
                track.album, context={"include_ap_context": False}
            ).data,
            "attributedTo": track.attributed_to.fid,
    
            "mediaType": "text/html",
            "content": common_utils.render_html(content.text, content.content_type),
    
            "tag": [
                {"type": "Hashtag", "name": "#Punk"},
                {"type": "Hashtag", "name": "#Rock"},
            ],
    
            "image": {
                "type": "Image",
                "mediaType": "image/jpeg",
                "href": utils.full_url(track.attachment_cover.file.url),
            },
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        }
        serializer = serializers.TrackSerializer(track)
    
        assert serializer.data == expected
    
    
    def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
    
        add_tags = mocker.patch("funkwhale_api.tags.models.add_tags")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        track_attributed_to = factories["federation.Actor"]()
        album_attributed_to = factories["federation.Actor"]()
        album_artist_attributed_to = factories["federation.Actor"]()
        artist_attributed_to = factories["federation.Actor"]()
    
        activity = factories["federation.Activity"]()
        published = timezone.now()
        released = timezone.now().date()
        data = {
            "@context": jsonld.get_default_context(),
            "type": "Track",
            "id": "http://hello.track",
            "published": published.isoformat(),
            "musicbrainzId": str(uuid.uuid4()),
            "name": "Black in back",
            "position": 5,
            "disc": 1,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "attributedTo": track_attributed_to.fid,
    
            "image": {
                "type": "Link",
                "href": "https://cover.image/track.png",
                "mediaType": "image/png",
            },
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "album": {
                "type": "Album",
                "id": "http://hello.album",
                "name": "Purple album",
                "musicbrainzId": str(uuid.uuid4()),
                "published": published.isoformat(),
                "released": released.isoformat(),
    
                "content": "Album summary",
                "mediaType": "text/markdown",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "attributedTo": album_attributed_to.fid,
                "cover": {
                    "type": "Link",
                    "href": "https://cover.image/test.png",
                    "mediaType": "image/png",
                },
    
                "tag": [{"type": "Hashtag", "name": "AlbumTag"}],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "artists": [
                    {
                        "type": "Artist",
    
                        "mediaType": "text/plain",
                        "content": "Artist summary",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                        "id": "http://hello.artist",
                        "name": "John Smith",
                        "musicbrainzId": str(uuid.uuid4()),
                        "published": published.isoformat(),
                        "attributedTo": album_artist_attributed_to.fid,
    
                        "tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
    
                        "image": {
                            "type": "Link",
                            "href": "https://cover.image/album-artist.png",
                            "mediaType": "image/png",
                        },
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    }
                ],
            },
            "artists": [
                {
                    "type": "Artist",
                    "id": "http://hello.trackartist",
                    "name": "Bob Smith",
    
                    "mediaType": "text/plain",
                    "content": "Other artist summary",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "musicbrainzId": str(uuid.uuid4()),
                    "attributedTo": artist_attributed_to.fid,
                    "published": published.isoformat(),
    
                    "tag": [{"type": "Hashtag", "name": "ArtistTag"}],
    
                    "image": {
                        "type": "Link",
                        "href": "https://cover.image/artist.png",
                        "mediaType": "image/png",
                    },
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                }
            ],
    
            "tag": [
                {"type": "Hashtag", "name": "#Hello"},
                {"type": "Hashtag", "name": "World"},
            ],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        }
        serializer = serializers.TrackSerializer(data=data, context={"activity": activity})
        assert serializer.is_valid(raise_exception=True)
    
        track = serializer.save()
        album = track.album
        artist = track.artist
        album_artist = track.album.artist
    
        assert track.from_activity == activity
        assert track.fid == data["id"]
        assert track.title == data["name"]
        assert track.position == data["position"]
        assert track.disc_number == data["disc"]
        assert track.creation_date == published
        assert track.attributed_to == track_attributed_to
        assert str(track.mbid) == data["musicbrainzId"]
    
        assert track.description.text == data["content"]
        assert track.description.content_type == "text/html"
    
        assert track.attachment_cover.url == data["image"]["href"]
        assert track.attachment_cover.mimetype == data["image"]["mediaType"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        assert album.from_activity == activity
    
        assert album.attachment_cover.url == data["album"]["cover"]["href"]
        assert album.attachment_cover.mimetype == data["album"]["cover"]["mediaType"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        assert album.title == data["album"]["name"]
        assert album.fid == data["album"]["id"]
        assert str(album.mbid) == data["album"]["musicbrainzId"]
        assert album.creation_date == published
        assert album.release_date == released
        assert album.attributed_to == album_attributed_to
    
        assert album.description.text == data["album"]["content"]
        assert album.description.content_type == data["album"]["mediaType"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        assert artist.from_activity == activity
        assert artist.name == data["artists"][0]["name"]
        assert artist.fid == data["artists"][0]["id"]
        assert str(artist.mbid) == data["artists"][0]["musicbrainzId"]
        assert artist.creation_date == published
        assert artist.attributed_to == artist_attributed_to
    
        assert artist.description.text == data["artists"][0]["content"]
        assert artist.description.content_type == data["artists"][0]["mediaType"]
    
        assert artist.attachment_cover.url == data["artists"][0]["image"]["href"]
        assert artist.attachment_cover.mimetype == data["artists"][0]["image"]["mediaType"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        assert album_artist.from_activity == activity
        assert album_artist.name == data["album"]["artists"][0]["name"]
        assert album_artist.fid == data["album"]["artists"][0]["id"]
        assert str(album_artist.mbid) == data["album"]["artists"][0]["musicbrainzId"]
        assert album_artist.creation_date == published
        assert album_artist.attributed_to == album_artist_attributed_to
    
        assert album_artist.description.text == data["album"]["artists"][0]["content"]
        assert (
            album_artist.description.content_type
            == data["album"]["artists"][0]["mediaType"]
        )
    
        assert (
            album_artist.attachment_cover.url
            == data["album"]["artists"][0]["image"]["href"]
        )
        assert (
            album_artist.attachment_cover.mimetype
            == data["album"]["artists"][0]["image"]["mediaType"]
        )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
        add_tags.assert_any_call(track, *["Hello", "World"])
        add_tags.assert_any_call(album, *["AlbumTag"])
        add_tags.assert_any_call(album_artist, *["AlbumArtistTag"])
        add_tags.assert_any_call(artist, *["ArtistTag"])
    
    def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker, faker):
    
        set_tags = mocker.patch("funkwhale_api.tags.models.set_tags")
    
        content = factories["common.Content"]()
    
        track_attributed_to = factories["federation.Actor"]()
    
        track = factories["music.Track"](description=content)
    
    
        published = timezone.now()
        data = {
            "@context": jsonld.get_default_context(),
            "type": "Track",
            "id": track.fid,
            "published": published.isoformat(),
            "musicbrainzId": str(uuid.uuid4()),
            "name": "Black in back",
            "position": 5,
            "disc": 2,
    
            "attributedTo": track_attributed_to.fid,
            "album": serializers.AlbumSerializer(track.album).data,
            "artists": [serializers.ArtistSerializer(track.artist).data],
    
            "image": {"type": "Link", "mediaType": "image/jpeg", "href": faker.url()},
    
            "tag": [
                {"type": "Hashtag", "name": "#Hello"},
                # Ensure we can handle tags without a leading #
                {"type": "Hashtag", "name": "World"},
            ],
        }
        serializer = serializers.TrackSerializer(track, data=data)
        assert serializer.is_valid(raise_exception=True)
    
        serializer.save()
        track.refresh_from_db()
    
        assert track.fid == data["id"]
        assert track.title == data["name"]
        assert track.position == data["position"]
        assert track.disc_number == data["disc"]
        assert track.attributed_to == track_attributed_to
    
        assert track.description.content_type == "text/html"
        assert track.description.text == "hello there"
    
        assert str(track.mbid) == data["musicbrainzId"]
    
        assert track.attachment_cover.url == data["image"]["href"]
        assert track.attachment_cover.mimetype == data["image"]["mediaType"]
    
    
        set_tags.assert_called_once_with(track, *["Hello", "World"])
    
    
        with pytest.raises(content.DoesNotExist):
            content.refresh_from_db()
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
        activity = factories["federation.Activity"]()
        library = factories["music.Library"]()
    
        published = timezone.now()
        updated = timezone.now()
        released = timezone.now().date()
        data = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "@context": jsonld.get_default_context(),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "type": "Audio",
            "id": "https://track.file",
            "name": "Ignored",
            "published": published.isoformat(),
            "updated": updated.isoformat(),
            "duration": 43,
            "bitrate": 42,
            "size": 66,
            "url": {"href": "https://audio.file", "type": "Link", "mediaType": "audio/mp3"},
            "library": library.fid,