Fix #1038: Federated reports
This commit is contained in:
parent
40720328d7
commit
d9afed5067
34 changed files with 985 additions and 76 deletions
|
|
@ -456,6 +456,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
shared_inbox_url=remote_actor1.shared_inbox_url
|
||||
)
|
||||
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
|
||||
remote_actor4 = factories["federation.Actor"]()
|
||||
|
||||
library = factories["music.Library"]()
|
||||
library_follower_local = factories["federation.LibraryFollow"](
|
||||
|
|
@ -491,6 +492,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
activity.PUBLIC_ADDRESS,
|
||||
{"type": "followers", "target": library},
|
||||
{"type": "followers", "target": followed_actor},
|
||||
{"type": "actor_inbox", "actor": remote_actor4},
|
||||
]
|
||||
|
||||
inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items(
|
||||
|
|
@ -511,6 +513,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
[
|
||||
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
|
||||
models.Delivery(inbox_url=remote_actor3.inbox_url),
|
||||
models.Delivery(inbox_url=remote_actor4.inbox_url),
|
||||
models.Delivery(inbox_url=library_follower_remote.inbox_url),
|
||||
models.Delivery(inbox_url=actor_follower_remote.inbox_url),
|
||||
],
|
||||
|
|
@ -527,6 +530,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
activity.PUBLIC_ADDRESS,
|
||||
library.followers_url,
|
||||
followed_actor.followers_url,
|
||||
remote_actor4.fid,
|
||||
]
|
||||
|
||||
assert urls == expected_urls
|
||||
|
|
|
|||
|
|
@ -67,6 +67,95 @@ def test_expand_no_external_request():
|
|||
assert doc == expected
|
||||
|
||||
|
||||
def test_expand_no_external_request_pleroma():
|
||||
payload = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://pleroma.example/schemas/litepub-0.1.jsonld",
|
||||
{"@language": "und"},
|
||||
],
|
||||
"endpoints": {
|
||||
"oauthAuthorizationEndpoint": "https://pleroma.example/oauth/authorize",
|
||||
"oauthRegistrationEndpoint": "https://pleroma.example/api/v1/apps",
|
||||
"oauthTokenEndpoint": "https://pleroma.example/oauth/token",
|
||||
"sharedInbox": "https://pleroma.example/inbox",
|
||||
"uploadMedia": "https://pleroma.example/api/ap/upload_media",
|
||||
},
|
||||
"followers": "https://pleroma.example/internal/fetch/followers",
|
||||
"following": "https://pleroma.example/internal/fetch/following",
|
||||
"id": "https://pleroma.example/internal/fetch",
|
||||
"inbox": "https://pleroma.example/internal/fetch/inbox",
|
||||
"invisible": True,
|
||||
"manuallyApprovesFollowers": False,
|
||||
"name": "Pleroma",
|
||||
"preferredUsername": "internal.fetch",
|
||||
"publicKey": {
|
||||
"id": "https://pleroma.example/internal/fetch#main-key",
|
||||
"owner": "https://pleroma.example/internal/fetch",
|
||||
"publicKeyPem": "PEM",
|
||||
},
|
||||
"summary": "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
|
||||
"type": "Application",
|
||||
"url": "https://pleroma.example/internal/fetch",
|
||||
}
|
||||
|
||||
expected = {
|
||||
contexts.AS.endpoints: [
|
||||
{
|
||||
contexts.AS.sharedInbox: [{"@id": "https://pleroma.example/inbox"}],
|
||||
contexts.AS.oauthAuthorizationEndpoint: [
|
||||
{"@id": "https://pleroma.example/oauth/authorize"}
|
||||
],
|
||||
contexts.LITEPUB.oauthRegistrationEndpoint: [
|
||||
{"@id": "https://pleroma.example/api/v1/apps"}
|
||||
],
|
||||
contexts.AS.oauthTokenEndpoint: [
|
||||
{"@id": "https://pleroma.example/oauth/token"}
|
||||
],
|
||||
contexts.AS.uploadMedia: [
|
||||
{"@id": "https://pleroma.example/api/ap/upload_media"}
|
||||
],
|
||||
},
|
||||
],
|
||||
contexts.AS.followers: [
|
||||
{"@id": "https://pleroma.example/internal/fetch/followers"}
|
||||
],
|
||||
contexts.AS.following: [
|
||||
{"@id": "https://pleroma.example/internal/fetch/following"}
|
||||
],
|
||||
"@id": "https://pleroma.example/internal/fetch",
|
||||
"http://www.w3.org/ns/ldp#inbox": [
|
||||
{"@id": "https://pleroma.example/internal/fetch/inbox"}
|
||||
],
|
||||
contexts.LITEPUB.invisible: [{"@value": True}],
|
||||
contexts.AS.manuallyApprovesFollowers: [{"@value": False}],
|
||||
contexts.AS.name: [{"@language": "und", "@value": "Pleroma"}],
|
||||
contexts.AS.summary: [
|
||||
{
|
||||
"@language": "und",
|
||||
"@value": "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
|
||||
}
|
||||
],
|
||||
contexts.AS.url: [{"@id": "https://pleroma.example/internal/fetch"}],
|
||||
contexts.AS.preferredUsername: [
|
||||
{"@language": "und", "@value": "internal.fetch"}
|
||||
],
|
||||
contexts.SEC.publicKey: [
|
||||
{
|
||||
"@id": "https://pleroma.example/internal/fetch#main-key",
|
||||
contexts.SEC.owner: [{"@id": "https://pleroma.example/internal/fetch"}],
|
||||
contexts.SEC.publicKeyPem: [{"@language": "und", "@value": "PEM"}],
|
||||
}
|
||||
],
|
||||
"@type": [contexts.AS.Application],
|
||||
}
|
||||
|
||||
doc = jsonld.expand(payload)
|
||||
|
||||
assert doc[contexts.AS.endpoints] == expected[contexts.AS.endpoints]
|
||||
assert doc == expected
|
||||
|
||||
|
||||
def test_expand_remote_doc(r_mock):
|
||||
url = "https://noop/federation/actors/demo"
|
||||
payload = {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from funkwhale_api.federation import (
|
|||
routes,
|
||||
serializers,
|
||||
)
|
||||
from funkwhale_api.moderation import serializers as moderation_serializers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -30,6 +31,7 @@ from funkwhale_api.federation import (
|
|||
({"type": "Update", "object": {"type": "Album"}}, routes.inbox_update_album),
|
||||
({"type": "Update", "object": {"type": "Track"}}, routes.inbox_update_track),
|
||||
({"type": "Delete", "object": {"type": "Person"}}, routes.inbox_delete_actor),
|
||||
({"type": "Flag"}, routes.inbox_flag),
|
||||
],
|
||||
)
|
||||
def test_inbox_routes(route, handler):
|
||||
|
|
@ -44,6 +46,7 @@ def test_inbox_routes(route, handler):
|
|||
"route,handler",
|
||||
[
|
||||
({"type": "Accept"}, routes.outbox_accept),
|
||||
({"type": "Flag"}, routes.outbox_flag),
|
||||
({"type": "Follow"}, routes.outbox_follow),
|
||||
({"type": "Create", "object": {"type": "Audio"}}, routes.outbox_create_audio),
|
||||
(
|
||||
|
|
@ -718,3 +721,69 @@ def test_inbox_delete_actor_doesnt_delete_local_actor(factories):
|
|||
)
|
||||
# actor should still be here!
|
||||
local_actor.refresh_from_db()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"factory_name, factory_kwargs",
|
||||
[
|
||||
("federation.Actor", {"local": True}),
|
||||
("music.Artist", {"local": True}),
|
||||
("music.Album", {"local": True}),
|
||||
("music.Track", {"local": True}),
|
||||
("music.Library", {"local": True}),
|
||||
],
|
||||
)
|
||||
def test_inbox_flag(factory_name, factory_kwargs, factories, mocker):
|
||||
report_created_send = mocker.patch(
|
||||
"funkwhale_api.moderation.signals.report_created.send"
|
||||
)
|
||||
actor = factories["federation.Actor"]()
|
||||
target = factories[factory_name](**factory_kwargs)
|
||||
payload = {
|
||||
"type": "Flag",
|
||||
"object": [target.fid],
|
||||
"content": "Test report",
|
||||
"id": "https://" + actor.domain_id + "/testid",
|
||||
"actor": actor.fid,
|
||||
}
|
||||
serializer = serializers.ActivitySerializer(payload)
|
||||
|
||||
result = routes.inbox_flag(
|
||||
serializer.data, context={"actor": actor, "raise_exception": True}
|
||||
)
|
||||
|
||||
report = actor.reports.latest("id")
|
||||
|
||||
assert result == {"object": target, "related_object": report}
|
||||
assert report.fid == payload["id"]
|
||||
assert report.type == "other"
|
||||
assert report.target == target
|
||||
assert report.target_owner == moderation_serializers.get_target_owner(target)
|
||||
assert report.target_state == moderation_serializers.get_target_state(target)
|
||||
|
||||
report_created_send.assert_called_once_with(sender=None, report=report)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"factory_name, factory_kwargs",
|
||||
[
|
||||
("federation.Actor", {"local": True}),
|
||||
("music.Artist", {"local": True}),
|
||||
("music.Album", {"local": True}),
|
||||
("music.Track", {"local": True}),
|
||||
("music.Library", {"local": True}),
|
||||
],
|
||||
)
|
||||
def test_outbox_flag(factory_name, factory_kwargs, factories, mocker):
|
||||
target = factories[factory_name](**factory_kwargs)
|
||||
report = factories["moderation.Report"](
|
||||
target=target, local=True, target_owner=factories["federation.Actor"]()
|
||||
)
|
||||
|
||||
activity = list(routes.outbox_flag({"report": report}))[0]
|
||||
|
||||
serializer = serializers.FlagSerializer(report)
|
||||
expected = serializer.data
|
||||
expected["to"] = [{"type": "actor_inbox", "actor": report.target_owner}]
|
||||
assert activity["payload"] == expected
|
||||
assert activity["actor"] == actors.get_service_actor()
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ 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 actors
|
||||
from funkwhale_api.federation import contexts
|
||||
from funkwhale_api.federation import keys
|
||||
from funkwhale_api.federation import jsonld
|
||||
from funkwhale_api.federation import models
|
||||
from funkwhale_api.federation import serializers
|
||||
from funkwhale_api.federation import utils
|
||||
from funkwhale_api.moderation import serializers as moderation_serializers
|
||||
from funkwhale_api.music import licenses
|
||||
|
||||
|
||||
|
|
@ -70,6 +72,36 @@ def test_actor_serializer_from_ap(db):
|
|||
assert actor.attachment_icon.mimetype == payload["icon"]["mediaType"]
|
||||
|
||||
|
||||
def test_actor_serializer_from_ap_no_icon_mediaType(db):
|
||||
private, public = keys.get_key_pair()
|
||||
actor_url = "https://test.federation/actor"
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context_fw(),
|
||||
"id": actor_url,
|
||||
"type": "Person",
|
||||
"inbox": "https://test.com/inbox",
|
||||
"following": "https://test.com/following",
|
||||
"followers": "https://test.com/followers",
|
||||
"preferredUsername": "test",
|
||||
"manuallyApprovesFollowers": True,
|
||||
"url": "http://hello.world/path",
|
||||
"publicKey": {
|
||||
"publicKeyPem": public.decode("utf-8"),
|
||||
"owner": actor_url,
|
||||
"id": actor_url + "#main-key",
|
||||
},
|
||||
"endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"},
|
||||
"icon": {"type": "Image", "url": "https://image.example/image.png"},
|
||||
}
|
||||
|
||||
serializer = serializers.ActorSerializer(data=payload)
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
actor = serializer.save()
|
||||
|
||||
assert actor.attachment_icon.url == payload["icon"]["url"]
|
||||
assert actor.attachment_icon.mimetype is None
|
||||
|
||||
|
||||
def test_actor_serializer_only_mandatory_field_from_ap(db):
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
|
|
@ -1477,3 +1509,44 @@ def test_channel_create_upload_serializer(factories):
|
|||
serializer = serializers.ChannelCreateUploadSerializer(upload)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_report_serializer_from_ap_create(factories, faker, now, mocker):
|
||||
actor = factories["federation.Actor"]()
|
||||
obj = factories["music.Artist"](local=True)
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"type": "Flag",
|
||||
"id": "https://test.report",
|
||||
"actor": actor.fid,
|
||||
"content": "hello world",
|
||||
"object": [obj.fid],
|
||||
"tag": [{"type": "Hashtag", "name": "#offensive_content"}],
|
||||
}
|
||||
serializer = serializers.FlagSerializer(data=payload, context={"actor": actor})
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
|
||||
report = serializer.save()
|
||||
|
||||
assert report.fid == payload["id"]
|
||||
assert report.summary == payload["content"]
|
||||
assert report.submitter == actor
|
||||
assert report.target == obj
|
||||
assert report.target_state == moderation_serializers.get_target_state(obj)
|
||||
assert report.target_owner == moderation_serializers.get_target_owner(obj)
|
||||
assert report.type == "offensive_content"
|
||||
|
||||
|
||||
def test_report_serializer_to_ap(factories):
|
||||
report = factories["moderation.Report"](local=True)
|
||||
expected = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"type": "Flag",
|
||||
"id": report.fid,
|
||||
"actor": actors.get_service_actor().fid,
|
||||
"content": report.summary,
|
||||
"object": [report.target.fid],
|
||||
"tag": [{"type": "Hashtag", "name": "#{}".format(report.type)}],
|
||||
}
|
||||
serializer = serializers.FlagSerializer(report)
|
||||
assert serializer.data == expected
|
||||
|
|
|
|||
36
api/tests/federation/test_spa_views.py
Normal file
36
api/tests/federation/test_spa_views.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from funkwhale_api.common import utils
|
||||
|
||||
|
||||
def test_channel_detail(spa_html, no_api_auth, client, factories, settings):
|
||||
icon = factories["common.Attachment"]()
|
||||
actor = factories["federation.Actor"](local=True, attachment_icon=icon)
|
||||
url = "/@{}".format(actor.preferred_username)
|
||||
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
expected_metas = [
|
||||
{
|
||||
"tag": "meta",
|
||||
"property": "og:url",
|
||||
"content": utils.join_url(settings.FUNKWHALE_URL, url),
|
||||
},
|
||||
{"tag": "meta", "property": "og:title", "content": actor.display_name},
|
||||
{"tag": "meta", "property": "og:type", "content": "profile"},
|
||||
{
|
||||
"tag": "meta",
|
||||
"property": "og:image",
|
||||
"content": actor.attachment_icon.download_url_medium_square_crop,
|
||||
},
|
||||
{
|
||||
"tag": "link",
|
||||
"rel": "alternate",
|
||||
"type": "application/activity+json",
|
||||
"href": actor.fid,
|
||||
},
|
||||
]
|
||||
|
||||
metas = utils.parse_meta(response.content.decode())
|
||||
|
||||
# we only test our custom metas, not the default ones
|
||||
assert metas[: len(expected_metas)] == expected_metas
|
||||
58
api/tests/federation/test_third_party_activitypub.py
Normal file
58
api/tests/federation/test_third_party_activitypub.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from funkwhale_api.federation import serializers
|
||||
|
||||
|
||||
def test_pleroma_actor_from_ap(factories):
|
||||
|
||||
payload = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://test.federation/schemas/litepub-0.1.jsonld",
|
||||
{"@language": "und"},
|
||||
],
|
||||
"endpoints": {
|
||||
"oauthAuthorizationEndpoint": "https://test.federation/oauth/authorize",
|
||||
"oauthRegistrationEndpoint": "https://test.federation/api/v1/apps",
|
||||
"oauthTokenEndpoint": "https://test.federation/oauth/token",
|
||||
"sharedInbox": "https://test.federation/inbox",
|
||||
"uploadMedia": "https://test.federation/api/ap/upload_media",
|
||||
},
|
||||
"followers": "https://test.federation/internal/fetch/followers",
|
||||
"following": "https://test.federation/internal/fetch/following",
|
||||
"id": "https://test.federation/internal/fetch",
|
||||
"inbox": "https://test.federation/internal/fetch/inbox",
|
||||
"invisible": True,
|
||||
"manuallyApprovesFollowers": False,
|
||||
"name": "Pleroma",
|
||||
"preferredUsername": "internal.fetch",
|
||||
"publicKey": {
|
||||
"id": "https://test.federation/internal/fetch#main-key",
|
||||
"owner": "https://test.federation/internal/fetch",
|
||||
"publicKeyPem": "PEM",
|
||||
},
|
||||
"summary": "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
|
||||
"type": "Application",
|
||||
"url": "https://test.federation/internal/fetch",
|
||||
}
|
||||
|
||||
serializer = serializers.ActorSerializer(data=payload)
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
actor = serializer.save()
|
||||
|
||||
assert actor.fid == payload["id"]
|
||||
assert actor.url == payload["url"]
|
||||
assert actor.inbox_url == payload["inbox"]
|
||||
assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"]
|
||||
assert actor.outbox_url is None
|
||||
assert actor.following_url == payload["following"]
|
||||
assert actor.followers_url == payload["followers"]
|
||||
assert actor.followers_url == payload["followers"]
|
||||
assert actor.type == payload["type"]
|
||||
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 == payload["url"]
|
||||
assert actor.manually_approves_followers is payload["manuallyApprovesFollowers"]
|
||||
assert actor.private_key is None
|
||||
assert actor.public_key == payload["publicKey"]["publicKeyPem"]
|
||||
assert actor.domain_id == "test.federation"
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
import pytest
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from funkwhale_api.federation import exceptions, utils
|
||||
|
||||
|
||||
|
|
@ -172,3 +174,36 @@ def test_local_qs(factory_name, fids, kwargs, expected_indexes, factories, setti
|
|||
|
||||
expected_objs = [obj for i, obj in enumerate(objs) if i in expected_indexes]
|
||||
assert list(result) == expected_objs
|
||||
|
||||
|
||||
def test_get_obj_by_fid_not_found():
|
||||
with pytest.raises(ObjectDoesNotExist):
|
||||
utils.get_object_by_fid("http://test")
|
||||
|
||||
|
||||
def test_get_obj_by_fid_local_not_found(factories):
|
||||
obj = factories["federation.Actor"](local=False)
|
||||
with pytest.raises(ObjectDoesNotExist):
|
||||
utils.get_object_by_fid(obj.fid, local=True)
|
||||
|
||||
|
||||
def test_get_obj_by_fid_local(factories):
|
||||
obj = factories["federation.Actor"](local=True)
|
||||
assert utils.get_object_by_fid(obj.fid, local=True) == obj
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"factory_name",
|
||||
[
|
||||
"federation.Actor",
|
||||
"music.Artist",
|
||||
"music.Album",
|
||||
"music.Track",
|
||||
"music.Upload",
|
||||
"music.Library",
|
||||
],
|
||||
)
|
||||
def test_get_obj_by_fid(factory_name, factories):
|
||||
obj = factories[factory_name]()
|
||||
factories[factory_name]()
|
||||
assert utils.get_object_by_fid(obj.fid) == obj
|
||||
|
|
|
|||
|
|
@ -354,20 +354,24 @@ def test_music_upload_detail_private_approved_follow(
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"accept_header,expected",
|
||||
"accept_header,default,expected",
|
||||
[
|
||||
("text/html,application/xhtml+xml", True),
|
||||
("text/html,application/json", True),
|
||||
("", False),
|
||||
(None, False),
|
||||
("application/json", False),
|
||||
("application/activity+json", False),
|
||||
("application/json,text/html", False),
|
||||
("application/activity+json,text/html", False),
|
||||
("text/html,application/xhtml+xml", True, True),
|
||||
("text/html,application/json", True, True),
|
||||
("", True, False),
|
||||
(None, True, False),
|
||||
("application/json", True, False),
|
||||
("application/activity+json", True, False),
|
||||
("application/json,text/html", True, False),
|
||||
("application/activity+json,text/html", True, False),
|
||||
("unrelated/ct", True, True),
|
||||
("unrelated/ct", False, False),
|
||||
],
|
||||
)
|
||||
def test_should_redirect_ap_to_html(accept_header, expected):
|
||||
assert federation_utils.should_redirect_ap_to_html(accept_header) is expected
|
||||
def test_should_redirect_ap_to_html(accept_header, default, expected):
|
||||
assert (
|
||||
federation_utils.should_redirect_ap_to_html(accept_header, default) is expected
|
||||
)
|
||||
|
||||
|
||||
def test_music_library_retrieve_redirects_to_html_if_header_set(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue