funkwhale/api/tests/federation/test_activity.py

346 wiersze
11 KiB
Python

import pytest
import uuid
from django.db.models import Q
from django.urls import reverse
from funkwhale_api.federation import (
activity,
models,
api_serializers,
serializers,
tasks,
)
def test_receive_validates_basic_attributes_and_stores_activity(factories, now, mocker):
mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
local_to_actor = factories["users.User"]().create_actor()
local_cc_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
a = {
"@context": [],
"actor": remote_actor.fid,
"type": "Noop",
"id": "https://test.activity",
"to": [local_to_actor.fid, remote_actor.fid],
"cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS],
}
copy = activity.receive(activity=a, on_behalf_of=remote_actor)
assert copy.payload == a
assert copy.creation_date >= now
assert copy.actor == remote_actor
assert copy.fid == a["id"]
assert copy.type == "Noop"
mocked_dispatch.assert_called_once_with(
tasks.dispatch_inbox.delay, activity_id=copy.pk
)
assert models.InboxItem.objects.count() == 2
for actor, t in [(local_to_actor, "to"), (local_cc_actor, "cc")]:
ii = models.InboxItem.objects.get(actor=actor)
assert ii.type == t
assert ii.activity == copy
assert ii.is_read is False
def test_get_actors_from_audience_urls(settings, db):
settings.FEDERATION_HOSTNAME = "federation.hostname"
library_uuid1 = uuid.uuid4()
library_uuid2 = uuid.uuid4()
urls = [
"https://wrong.url",
"https://federation.hostname"
+ reverse("federation:actors-detail", kwargs={"preferred_username": "kevin"}),
"https://federation.hostname"
+ reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}),
"https://federation.hostname"
+ reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}),
"https://federation.hostname"
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid1}),
"https://federation.hostname"
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid2}),
activity.PUBLIC_ADDRESS,
]
followed_query = Q(target__followers_url=urls[0])
for url in urls[1:-1]:
followed_query |= Q(target__followers_url=url)
actor_follows = models.Follow.objects.filter(followed_query, approved=True)
library_follows = models.LibraryFollow.objects.filter(followed_query, approved=True)
expected = models.Actor.objects.filter(
Q(fid__in=urls[0:-1])
| Q(pk__in=actor_follows.values_list("actor", flat=True))
| Q(pk__in=library_follows.values_list("actor", flat=True))
)
assert str(activity.get_actors_from_audience(urls).query) == str(expected.query)
def test_get_inbox_urls(factories):
a1 = factories["federation.Actor"](
shared_inbox_url=None, inbox_url="https://a1.inbox"
)
a2 = factories["federation.Actor"](
shared_inbox_url="https://shared.inbox", inbox_url="https://a2.inbox"
)
factories["federation.Actor"](
shared_inbox_url="https://shared.inbox", inbox_url="https://a3.inbox"
)
expected = sorted(set([a1.inbox_url, a2.shared_inbox_url]))
assert activity.get_inbox_urls(a1.__class__.objects.all()) == expected
def test_receive_invalid_data(factories):
remote_actor = factories["federation.Actor"]()
a = {"@context": [], "actor": remote_actor.fid, "id": "https://test.activity"}
with pytest.raises(serializers.serializers.ValidationError):
activity.receive(activity=a, on_behalf_of=remote_actor)
def test_receive_actor_mismatch(factories):
remote_actor = factories["federation.Actor"]()
a = {
"@context": [],
"type": "Noop",
"actor": "https://hello",
"id": "https://test.activity",
}
with pytest.raises(serializers.serializers.ValidationError):
activity.receive(activity=a, on_behalf_of=remote_actor)
def test_inbox_routing(factories, mocker):
object = factories["music.Artist"]()
target = factories["music.Artist"]()
router = activity.InboxRouter()
a = factories["federation.Activity"](type="Follow")
handler_payload = {}
handler_context = {}
def handler(payload, context):
handler_payload.update(payload)
handler_context.update(context)
return {"target": target, "object": object}
router.connect({"type": "Follow"}, handler)
good_message = {"type": "Follow"}
router.dispatch(good_message, context={"activity": a})
assert handler_payload == good_message
assert handler_context == {"activity": a}
a.refresh_from_db()
assert a.object == object
assert a.target == target
def test_inbox_routing_send_to_channel(factories, mocker):
group_send = mocker.patch("funkwhale_api.common.channels.group_send")
a = factories["federation.Activity"](type="Follow")
ii = factories["federation.InboxItem"](actor__local=True)
router = activity.InboxRouter()
handler = mocker.stub()
router.connect({"type": "Follow"}, handler)
good_message = {"type": "Follow"}
router.dispatch(
good_message, context={"activity": a, "inbox_items": ii.__class__.objects.all()}
)
ii.refresh_from_db()
group_send.assert_called_once_with(
"user.{}.inbox".format(ii.actor.user.pk),
{
"type": "event.send",
"text": "",
"data": {
"type": "inbox.item_added",
"item": api_serializers.InboxItemSerializer(ii).data,
},
},
)
@pytest.mark.parametrize(
"route,payload,expected",
[
({"type": "Follow"}, {"type": "Follow"}, True),
({"type": "Follow"}, {"type": "Noop"}, False),
({"type": "Follow"}, {"type": "Follow", "id": "https://hello"}, True),
(
{"type": "Create", "object.type": "Audio"},
{"type": "Create", "object": {"type": "Note"}},
False,
),
(
{"type": "Create", "object.type": "Audio"},
{"type": "Create", "object": {"type": "Audio"}},
True,
),
],
)
def test_route_matching(route, payload, expected):
assert activity.match_route(route, payload) is expected
def test_outbox_router_dispatch(mocker, factories, now):
router = activity.OutboxRouter()
actor = factories["federation.Actor"]()
r1 = factories["federation.Actor"]()
r2 = factories["federation.Actor"]()
mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
def handler(context):
yield {
"payload": {
"type": "Noop",
"actor": actor.fid,
"summary": context["summary"],
"to": [r1],
"cc": [r2, activity.PUBLIC_ADDRESS],
},
"actor": actor,
}
expected_deliveries_url = activity.get_inbox_urls(
models.Actor.objects.filter(pk__in=[r1.pk, r2.pk])
)
router.connect({"type": "Noop"}, handler)
activities = router.dispatch({"type": "Noop"}, {"summary": "hello"})
a = activities[0]
mocked_dispatch.assert_called_once_with(
tasks.dispatch_outbox.delay, activity_id=a.pk
)
assert a.payload == {
"type": "Noop",
"actor": actor.fid,
"summary": "hello",
"to": [r1.fid],
"cc": [r2.fid, activity.PUBLIC_ADDRESS],
}
assert a.actor == actor
assert a.creation_date >= now
assert a.uuid is not None
assert a.deliveries.count() == 2
for url in expected_deliveries_url:
delivery = a.deliveries.get(inbox_url=url)
assert delivery.is_delivered is False
def test_prepare_deliveries_and_inbox_items(factories):
local_actor1 = factories["federation.Actor"](
local=True, shared_inbox_url="https://testlocal.inbox"
)
local_actor2 = factories["federation.Actor"](
local=True, shared_inbox_url=local_actor1.shared_inbox_url
)
local_actor3 = factories["federation.Actor"](local=True, shared_inbox_url=None)
remote_actor1 = factories["federation.Actor"](
shared_inbox_url="https://testremote.inbox"
)
remote_actor2 = factories["federation.Actor"](
shared_inbox_url=remote_actor1.shared_inbox_url
)
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
library = factories["music.Library"]()
library_follower_local = factories["federation.LibraryFollow"](
target=library, actor__local=True, approved=True
).actor
library_follower_remote = factories["federation.LibraryFollow"](
target=library, actor__local=False, approved=True
).actor
# follow not approved
factories["federation.LibraryFollow"](
target=library, actor__local=False, approved=False
)
followed_actor = factories["federation.Actor"]()
actor_follower_local = factories["federation.Follow"](
target=followed_actor, actor__local=True, approved=True
).actor
actor_follower_remote = factories["federation.Follow"](
target=followed_actor, actor__local=False, approved=True
).actor
# follow not approved
factories["federation.Follow"](
target=followed_actor, actor__local=False, approved=False
)
recipients = [
local_actor1,
local_actor2,
local_actor3,
remote_actor1,
remote_actor2,
remote_actor3,
activity.PUBLIC_ADDRESS,
{"type": "followers", "target": library},
{"type": "followers", "target": followed_actor},
]
inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items(
recipients, "to"
)
expected_inbox_items = sorted(
[
models.InboxItem(actor=local_actor1, type="to"),
models.InboxItem(actor=local_actor2, type="to"),
models.InboxItem(actor=local_actor3, type="to"),
models.InboxItem(actor=library_follower_local, type="to"),
models.InboxItem(actor=actor_follower_local, type="to"),
],
key=lambda v: v.actor.pk,
)
expected_deliveries = sorted(
[
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
models.Delivery(inbox_url=remote_actor3.inbox_url),
models.Delivery(inbox_url=library_follower_remote.inbox_url),
models.Delivery(inbox_url=actor_follower_remote.inbox_url),
],
key=lambda v: v.inbox_url,
)
expected_urls = [
local_actor1.fid,
local_actor2.fid,
local_actor3.fid,
remote_actor1.fid,
remote_actor2.fid,
remote_actor3.fid,
activity.PUBLIC_ADDRESS,
library.followers_url,
followed_actor.followers_url,
]
assert urls == expected_urls
assert len(expected_inbox_items) == len(inbox_items)
assert len(expected_deliveries) == len(deliveries)
for delivery, expected_delivery in zip(
sorted(deliveries, key=lambda v: v.inbox_url), expected_deliveries
):
assert delivery.inbox_url == expected_delivery.inbox_url
for inbox_item, expected_inbox_item in zip(
sorted(inbox_items, key=lambda v: v.actor.pk), expected_inbox_items
):
assert inbox_item.actor == expected_inbox_item.actor
assert inbox_item.type == "to"