def test_dereference(): followers_doc = { "@context": jsonld.get_default_context(), "id": "https://noop/federation/actors/demo/followers", "type": "Collection", } actor_doc = { "@context": jsonld.get_default_context(), "id": "https://noop/federation/actors/demo", "type": "Person", "followers": "https://noop/federation/actors/demo/followers", } store = {followers_doc["id"]: followers_doc, actor_doc["id"]: actor_doc} payload = { "followers": {"@id": followers_doc["id"]}, "actor": [ {"@id": actor_doc["id"], "hello": "world"}, {"somethingElse": [{"@id": actor_doc["id"]}]}, ], } expected = { "followers": followers_doc, "actor": [actor_doc, {"somethingElse": [actor_doc]}], } assert jsonld.dereference(payload, store) == expected
def test_jsonld_serializer_dereference(a_responses): class TestSerializer(jsonld.JsonLdSerializer): id = serializers.URLField() type = serializers.CharField() followers = serializers.JSONField() class Meta: jsonld_mapping = { "followers": {"property": contexts.AS.followers, "dereference": True} } payload = { "@context": jsonld.get_default_context(), "id": "https://noop.url/federation/actors/demo", "type": "Person", "followers": "https://noop.url/federation/actors/demo/followers", } followers_doc = { "@context": jsonld.get_default_context(), "id": "https://noop.url/federation/actors/demo/followers", "type": "Collection", } a_responses.get(followers_doc["id"], payload=followers_doc) serializer = TestSerializer(data=payload) assert serializer.is_valid(raise_exception=True) assert serializer.validated_data == { "type": contexts.AS.Person, "id": payload["id"], "followers": [followers_doc], }
def test_inbox_create_audio(factories, mocker): activity = factories["federation.Activity"]() upload = factories["music.Upload"](bitrate=42, duration=55, track__album__with_cover=True) payload = { "@context": jsonld.get_default_context(), "type": "Create", "actor": upload.library.actor.fid, "object": serializers.UploadSerializer(upload).data, } library = upload.library upload.delete() init = mocker.spy(serializers.UploadSerializer, "__init__") save = mocker.spy(serializers.UploadSerializer, "save") assert library.uploads.count() == 0 result = routes.inbox_create_audio( payload, context={ "actor": library.actor, "raise_exception": True, "activity": activity }, ) assert library.uploads.count() == 1 assert result == { "object": library.uploads.latest("id"), "target": library } assert init.call_count == 1 args = init.call_args assert args[1]["data"] == payload["object"] assert args[1]["context"] == {"activity": activity, "actor": library.actor} assert save.call_count == 1
async def test_fetch_many(a_responses): doc = { "@context": jsonld.get_default_context(), "id": "https://noop/federation/actors/demo", "type": "Person", "followers": "https://noop/federation/actors/demo/followers", } followers_doc = { "@context": jsonld.get_default_context(), "id": "https://noop/federation/actors/demo/followers", "type": "Collection", } a_responses.get(doc["id"], payload=doc) a_responses.get(followers_doc["id"], payload=followers_doc) fetched = await jsonld.fetch_many(doc["id"], followers_doc["id"]) assert fetched == {followers_doc["id"]: followers_doc, doc["id"]: doc}
def test_fetch_skipped(factories, r_mock): url = "https://fetch.object" fetch = factories["federation.Fetch"](url=url) payload = {"@context": jsonld.get_default_context(), "type": "Unhandled"} r_mock.get(url, json=payload) tasks.fetch(fetch_id=fetch.pk) fetch.refresh_from_db() assert fetch.status == "skipped" assert fetch.detail["reason"] == "unhandled_type"
def test_jsonld_serializer_fallback(): class TestSerializer(jsonld.JsonLdSerializer): id = serializers.URLField() type = serializers.CharField() name = serializers.CharField() username = serializers.CharField() total = serializers.IntegerField() class Meta: jsonld_fallbacks = {"total": ["total_fallback"]} jsonld_mapping = { "name": { "property": contexts.AS.name, "keep": "first", "attr": "@value", }, "username": { "property": contexts.AS.preferredUsername, "keep": "first", "attr": "@value", }, "total": { "property": contexts.AS.totalItems, "keep": "first", "attr": "@value", }, "total_fallback": { "property": contexts.NOOP.count, "keep": "first", "attr": "@value", }, } payload = { "@context": jsonld.get_default_context(), "id": "https://noop.url/federation/actors/demo", "type": "Person", "name": "Hello", "preferredUsername": "******", "count": 42, } serializer = TestSerializer(data=payload) assert serializer.is_valid(raise_exception=True) assert serializer.validated_data == { "type": contexts.AS.Person, "id": payload["id"], "name": payload["name"], "username": payload["preferredUsername"], "total": 42, }
def test_authenticate(factories, mocker, api_request): private, public = keys.get_key_pair() factories["federation.Domain"](name="test.federation", nodeinfo_fetch_date=None) actor_url = "https://test.federation/actor" mocker.patch( "funkwhale_api.federation.actors.get_actor_data", return_value={ "@context": jsonld.get_default_context(), "id": actor_url, "type": "Person", "outbox": "https://test.com", "inbox": "https://test.com", "followers": "https://test.com", "preferredUsername": "******", "publicKey": { "publicKeyPem": public.decode("utf-8"), "owner": actor_url, "id": actor_url + "#main-key", }, }, ) update_domain_nodeinfo = mocker.patch( "funkwhale_api.federation.tasks.update_domain_nodeinfo") signed_request = factories["federation.SignedRequest"]( auth__key=private, auth__key_id=actor_url + "#main-key", auth__headers=["date"]) prepared = signed_request.prepare() django_request = api_request.get( "/", **{ "HTTP_DATE": prepared.headers["date"], "HTTP_SIGNATURE": prepared.headers["signature"], }) authenticator = authentication.SignatureAuthentication() user, _ = authenticator.authenticate(django_request) actor = django_request.actor assert user.is_anonymous is True assert actor.public_key == public.decode("utf-8") assert actor.fid == actor_url update_domain_nodeinfo.assert_called_once_with( domain_name="test.federation")
def test_autenthicate_supports_blind_key_rotation(factories, mocker, api_request): actor = factories["federation.Actor"]() actor_url = actor.fid # request is signed with a pair of new keys new_private, new_public = keys.get_key_pair() mocker.patch( "funkwhale_api.federation.actors.get_actor_data", return_value={ "@context": jsonld.get_default_context(), "id": actor_url, "type": "Person", "outbox": "https://test.com", "inbox": "https://test.com", "followers": "https://test.com", "preferredUsername": "******", "publicKey": { "publicKeyPem": new_public.decode("utf-8"), "owner": actor_url, "id": actor_url + "#main-key", }, }, ) signed_request = factories["federation.SignedRequest"]( auth__key=new_private, auth__key_id=actor_url + "#main-key", auth__headers=["date"], ) prepared = signed_request.prepare() django_request = api_request.get( "/", **{ "HTTP_DATE": prepared.headers["date"], "HTTP_SIGNATURE": prepared.headers["signature"], }) authenticator = authentication.SignatureAuthentication() user, _ = authenticator.authenticate(django_request) actor = django_request.actor assert user.is_anonymous is True assert actor.public_key == new_public.decode("utf-8") assert actor.fid == actor_url
def test_authenticate_ignore_inactive_policy(factories, api_request, mocker): policy = factories["moderation.InstancePolicy"](block_all=True, for_domain=True, is_active=False) private, public = keys.get_key_pair() actor_url = "https://{}/actor".format(policy.target_domain.name) signed_request = factories["federation.SignedRequest"]( auth__key=private, auth__key_id=actor_url + "#main-key", auth__headers=["date"]) mocker.patch( "funkwhale_api.federation.actors.get_actor_data", return_value={ "@context": jsonld.get_default_context(), "id": actor_url, "type": "Person", "outbox": "https://test.com", "inbox": "https://test.com", "followers": "https://test.com", "preferredUsername": "******", "publicKey": { "publicKeyPem": public.decode("utf-8"), "owner": actor_url, "id": actor_url + "#main-key", }, }, ) prepared = signed_request.prepare() django_request = api_request.get( "/", **{ "HTTP_DATE": prepared.headers["date"], "HTTP_SIGNATURE": prepared.headers["signature"], }) authenticator = authentication.SignatureAuthentication() authenticator.authenticate(django_request) actor = django_request.actor assert actor.public_key == public.decode("utf-8") assert actor.fid == actor_url
def test_inbox_create_audio_channel(factories, mocker): activity = factories["federation.Activity"]() channel = factories["audio.Channel"]() album = factories["music.Album"](artist=channel.artist) upload = factories["music.Upload"]( track__album=album, library=channel.library, ) payload = { "@context": jsonld.get_default_context(), "type": "Create", "actor": channel.actor.fid, "object": serializers.ChannelUploadSerializer(upload).data, } upload.delete() init = mocker.spy(serializers.ChannelCreateUploadSerializer, "__init__") save = mocker.spy(serializers.ChannelCreateUploadSerializer, "save") result = routes.inbox_create_audio( payload, context={ "actor": channel.actor, "raise_exception": True, "activity": activity }, ) assert channel.library.uploads.count() == 1 assert result == { "object": channel.library.uploads.latest("id"), "target": channel } assert init.call_count == 1 args = init.call_args assert args[1]["data"] == payload assert args[1]["context"] == {"channel": channel} assert save.call_count == 1
def test_fetch_collection(mocker, r_mock): class DummySerializer(serializers.serializers.Serializer): def validate(self, validated_data): validated_data = self.initial_data if "id" not in validated_data["object"]: raise serializers.serializers.ValidationError() return validated_data def save(self): return self.initial_data mocker.patch.object( tasks, "COLLECTION_ACTIVITY_SERIALIZERS", [({ "type": "Create", "object.type": "Audio" }, DummySerializer)], ) payloads = { "outbox": { "id": "https://actor.url/outbox", "@context": jsonld.get_default_context(), "type": "OrderedCollection", "totalItems": 27094, "first": "https://actor.url/outbox?page=1", "last": "https://actor.url/outbox?page=3", }, "page1": { "@context": jsonld.get_default_context(), "type": "OrderedCollectionPage", "next": "https://actor.url/outbox?page=2", "orderedItems": [ { "type": "Unhandled" }, { "type": "Unhandled" }, { "type": "Create", "object": { "type": "Audio", "id": "https://actor.url/audio1" }, }, ], }, "page2": { "@context": jsonld.get_default_context(), "type": "OrderedCollectionPage", "next": "https://actor.url/outbox?page=3", "orderedItems": [ { "type": "Unhandled" }, { "type": "Create", "object": { "type": "Audio", "id": "https://actor.url/audio2" }, }, { "type": "Unhandled" }, { "type": "Create", "object": { "type": "Audio" } }, ], }, } r_mock.get(payloads["outbox"]["id"], json=payloads["outbox"]) r_mock.get(payloads["outbox"]["first"], json=payloads["page1"]) r_mock.get(payloads["page1"]["next"], json=payloads["page2"]) result = tasks.fetch_collection( payloads["outbox"]["id"], max_pages=2, ) assert result["items"] == [ payloads["page1"]["orderedItems"][2], payloads["page2"]["orderedItems"][1], ] assert result["skipped"] == 4 assert result["errored"] == 1 assert result["seen"] == 7 assert result["total"] == 27094 assert result["next_page"] == payloads["page2"]["next"]
assert fetch.status == "skipped" assert fetch.detail["reason"] == "unhandled_type" @pytest.mark.parametrize( "r_mock_args, expected_error_code", [ ({ "json": { "type": "Unhandled" } }, "invalid_jsonld"), ({ "json": { "@context": jsonld.get_default_context() } }, "invalid_jsonld"), ({ "text": "invalidjson" }, "invalid_json"), ({ "status_code": 404 }, "http"), ({ "status_code": 500 }, "http"), ], ) def test_fetch_errored(factories, r_mock_args, expected_error_code, r_mock): url = "https://fetch.object"