516 lines
18 KiB
Python
516 lines
18 KiB
Python
import arrow
|
|
import pytest
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from rest_framework import exceptions
|
|
|
|
from funkwhale_api.federation import actors, models, serializers, utils
|
|
from funkwhale_api.music import models as music_models
|
|
from funkwhale_api.music import tasks as music_tasks
|
|
|
|
|
|
def test_actor_fetching(r_mock):
|
|
payload = {
|
|
"id": "https://actor.mock/users/actor#main-key",
|
|
"owner": "test",
|
|
"publicKeyPem": "test_pem",
|
|
}
|
|
actor_url = "https://actor.mock/"
|
|
r_mock.get(actor_url, json=payload)
|
|
r = actors.get_actor_data(actor_url)
|
|
|
|
assert r == payload
|
|
|
|
|
|
def test_get_actor(factories, r_mock):
|
|
actor = factories["federation.Actor"].build()
|
|
payload = serializers.ActorSerializer(actor).data
|
|
r_mock.get(actor.url, json=payload)
|
|
new_actor = actors.get_actor(actor.url)
|
|
|
|
assert new_actor.pk is not None
|
|
assert serializers.ActorSerializer(new_actor).data == payload
|
|
|
|
|
|
def test_get_actor_use_existing(factories, preferences, mocker):
|
|
preferences["federation__actor_fetch_delay"] = 60
|
|
actor = factories["federation.Actor"]()
|
|
get_data = mocker.patch("funkwhale_api.federation.actors.get_actor_data")
|
|
new_actor = actors.get_actor(actor.url)
|
|
|
|
assert new_actor == actor
|
|
get_data.assert_not_called()
|
|
|
|
|
|
def test_get_actor_refresh(factories, preferences, mocker):
|
|
preferences["federation__actor_fetch_delay"] = 0
|
|
actor = factories["federation.Actor"]()
|
|
payload = serializers.ActorSerializer(actor).data
|
|
# actor changed their username in the meantime
|
|
payload["preferredUsername"] = "New me"
|
|
get_data = mocker.patch(
|
|
"funkwhale_api.federation.actors.get_actor_data", return_value=payload
|
|
)
|
|
new_actor = actors.get_actor(actor.url)
|
|
|
|
assert new_actor == actor
|
|
assert new_actor.last_fetch_date > actor.last_fetch_date
|
|
assert new_actor.preferred_username == "New me"
|
|
|
|
|
|
def test_get_library(db, settings, mocker):
|
|
get_key_pair = mocker.patch(
|
|
"funkwhale_api.federation.keys.get_key_pair",
|
|
return_value=(b"private", b"public"),
|
|
)
|
|
expected = {
|
|
"preferred_username": "library",
|
|
"domain": settings.FEDERATION_HOSTNAME,
|
|
"type": "Person",
|
|
"name": "{}'s library".format(settings.FEDERATION_HOSTNAME),
|
|
"manually_approves_followers": True,
|
|
"public_key": "public",
|
|
"url": utils.full_url(
|
|
reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
|
|
),
|
|
"shared_inbox_url": utils.full_url(
|
|
reverse("federation:instance-actors-inbox", kwargs={"actor": "library"})
|
|
),
|
|
"inbox_url": utils.full_url(
|
|
reverse("federation:instance-actors-inbox", kwargs={"actor": "library"})
|
|
),
|
|
"outbox_url": utils.full_url(
|
|
reverse("federation:instance-actors-outbox", kwargs={"actor": "library"})
|
|
),
|
|
"summary": "Bot account to federate with {}'s library".format(
|
|
settings.FEDERATION_HOSTNAME
|
|
),
|
|
}
|
|
actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
for key, value in expected.items():
|
|
assert getattr(actor, key) == value
|
|
|
|
|
|
def test_get_test(db, mocker, settings):
|
|
get_key_pair = mocker.patch(
|
|
"funkwhale_api.federation.keys.get_key_pair",
|
|
return_value=(b"private", b"public"),
|
|
)
|
|
expected = {
|
|
"preferred_username": "test",
|
|
"domain": settings.FEDERATION_HOSTNAME,
|
|
"type": "Person",
|
|
"name": "{}'s test account".format(settings.FEDERATION_HOSTNAME),
|
|
"manually_approves_followers": False,
|
|
"public_key": "public",
|
|
"url": utils.full_url(
|
|
reverse("federation:instance-actors-detail", kwargs={"actor": "test"})
|
|
),
|
|
"shared_inbox_url": utils.full_url(
|
|
reverse("federation:instance-actors-inbox", kwargs={"actor": "test"})
|
|
),
|
|
"inbox_url": utils.full_url(
|
|
reverse("federation:instance-actors-inbox", kwargs={"actor": "test"})
|
|
),
|
|
"outbox_url": utils.full_url(
|
|
reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
|
|
),
|
|
"summary": "Bot account to test federation with {}. Send me /ping and I'll answer you.".format(
|
|
settings.FEDERATION_HOSTNAME
|
|
),
|
|
}
|
|
actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
|
for key, value in expected.items():
|
|
assert getattr(actor, key) == value
|
|
|
|
|
|
def test_test_get_outbox():
|
|
expected = {
|
|
"@context": [
|
|
"https://www.w3.org/ns/activitystreams",
|
|
"https://w3id.org/security/v1",
|
|
{},
|
|
],
|
|
"id": utils.full_url(
|
|
reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
|
|
),
|
|
"type": "OrderedCollection",
|
|
"totalItems": 0,
|
|
"orderedItems": [],
|
|
}
|
|
|
|
data = actors.SYSTEM_ACTORS["test"].get_outbox({}, actor=None)
|
|
|
|
assert data == expected
|
|
|
|
|
|
def test_test_post_inbox_requires_authenticated_actor():
|
|
with pytest.raises(exceptions.PermissionDenied):
|
|
actors.SYSTEM_ACTORS["test"].post_inbox({}, actor=None)
|
|
|
|
|
|
def test_test_post_outbox_validates_actor(nodb_factories):
|
|
actor = nodb_factories["federation.Actor"]()
|
|
data = {"actor": "noop"}
|
|
with pytest.raises(exceptions.ValidationError) as exc_info:
|
|
actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
|
|
msg = "The actor making the request do not match"
|
|
assert msg in exc_info.value
|
|
|
|
|
|
def test_test_post_inbox_handles_create_note(settings, mocker, factories):
|
|
deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
|
|
actor = factories["federation.Actor"]()
|
|
now = timezone.now()
|
|
mocker.patch("django.utils.timezone.now", return_value=now)
|
|
data = {
|
|
"actor": actor.url,
|
|
"type": "Create",
|
|
"id": "http://test.federation/activity",
|
|
"object": {
|
|
"type": "Note",
|
|
"id": "http://test.federation/object",
|
|
"content": "<p><a>@mention</a> /ping</p>",
|
|
},
|
|
}
|
|
test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
|
expected_note = factories["federation.Note"](
|
|
id="https://test.federation/activities/note/{}".format(now.timestamp()),
|
|
content="Pong!",
|
|
published=now.isoformat(),
|
|
inReplyTo=data["object"]["id"],
|
|
cc=[],
|
|
summary=None,
|
|
sensitive=False,
|
|
attributedTo=test_actor.url,
|
|
attachment=[],
|
|
to=[actor.url],
|
|
url="https://{}/activities/note/{}".format(
|
|
settings.FEDERATION_HOSTNAME, now.timestamp()
|
|
),
|
|
tag=[{"href": actor.url, "name": actor.mention_username, "type": "Mention"}],
|
|
)
|
|
expected_activity = {
|
|
"@context": serializers.AP_CONTEXT,
|
|
"actor": test_actor.url,
|
|
"id": "https://{}/activities/note/{}/activity".format(
|
|
settings.FEDERATION_HOSTNAME, now.timestamp()
|
|
),
|
|
"to": actor.url,
|
|
"type": "Create",
|
|
"published": now.isoformat(),
|
|
"object": expected_note,
|
|
"cc": [],
|
|
}
|
|
actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
|
|
deliver.assert_called_once_with(
|
|
expected_activity,
|
|
to=[actor.url],
|
|
on_behalf_of=actors.SYSTEM_ACTORS["test"].get_actor_instance(),
|
|
)
|
|
|
|
|
|
def test_getting_actor_instance_persists_in_db(db):
|
|
test = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
|
from_db = models.Actor.objects.get(url=test.url)
|
|
|
|
for f in test._meta.fields:
|
|
assert getattr(from_db, f.name) == getattr(test, f.name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"username,domain,expected",
|
|
[("test", "wrongdomain.com", False), ("notsystem", "", False), ("test", "", True)],
|
|
)
|
|
def test_actor_is_system(username, domain, expected, nodb_factories, settings):
|
|
if not domain:
|
|
domain = settings.FEDERATION_HOSTNAME
|
|
|
|
actor = nodb_factories["federation.Actor"](
|
|
preferred_username=username, domain=domain
|
|
)
|
|
assert actor.is_system is expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"username,domain,expected",
|
|
[
|
|
("test", "wrongdomain.com", None),
|
|
("notsystem", "", None),
|
|
("test", "", actors.SYSTEM_ACTORS["test"]),
|
|
],
|
|
)
|
|
def test_actor_is_system(username, domain, expected, nodb_factories, settings):
|
|
if not domain:
|
|
domain = settings.FEDERATION_HOSTNAME
|
|
actor = nodb_factories["federation.Actor"](
|
|
preferred_username=username, domain=domain
|
|
)
|
|
assert actor.system_conf == expected
|
|
|
|
|
|
@pytest.mark.parametrize("value", [False, True])
|
|
def test_library_actor_manually_approves_based_on_preference(value, preferences):
|
|
preferences["federation__music_needs_approval"] = value
|
|
library_conf = actors.SYSTEM_ACTORS["library"]
|
|
assert library_conf.manually_approves_followers is value
|
|
|
|
|
|
def test_system_actor_handle(mocker, nodb_factories):
|
|
handler = mocker.patch("funkwhale_api.federation.actors.TestActor.handle_create")
|
|
actor = nodb_factories["federation.Actor"]()
|
|
activity = nodb_factories["federation.Activity"](type="Create", actor=actor.url)
|
|
serializer = serializers.ActivitySerializer(data=activity)
|
|
assert serializer.is_valid()
|
|
actors.SYSTEM_ACTORS["test"].handle(activity, actor)
|
|
handler.assert_called_once_with(activity, actor)
|
|
|
|
|
|
def test_test_actor_handles_follow(settings, mocker, factories):
|
|
deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
|
|
actor = factories["federation.Actor"]()
|
|
accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow")
|
|
test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
|
data = {
|
|
"actor": actor.url,
|
|
"type": "Follow",
|
|
"id": "http://test.federation/user#follows/267",
|
|
"object": test_actor.url,
|
|
}
|
|
actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
|
|
follow = models.Follow.objects.get(target=test_actor, approved=True)
|
|
follow_back = models.Follow.objects.get(actor=test_actor, approved=None)
|
|
accept_follow.assert_called_once_with(follow)
|
|
deliver.assert_called_once_with(
|
|
serializers.FollowSerializer(follow_back).data,
|
|
on_behalf_of=test_actor,
|
|
to=[actor.url],
|
|
)
|
|
|
|
|
|
def test_test_actor_handles_undo_follow(settings, mocker, factories):
|
|
deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
|
|
test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
|
follow = factories["federation.Follow"](target=test_actor)
|
|
reverse_follow = factories["federation.Follow"](
|
|
actor=test_actor, target=follow.actor
|
|
)
|
|
follow_serializer = serializers.FollowSerializer(follow)
|
|
reverse_follow_serializer = serializers.FollowSerializer(reverse_follow)
|
|
undo = {
|
|
"@context": serializers.AP_CONTEXT,
|
|
"type": "Undo",
|
|
"id": follow_serializer.data["id"] + "/undo",
|
|
"actor": follow.actor.url,
|
|
"object": follow_serializer.data,
|
|
}
|
|
expected_undo = {
|
|
"@context": serializers.AP_CONTEXT,
|
|
"type": "Undo",
|
|
"id": reverse_follow_serializer.data["id"] + "/undo",
|
|
"actor": reverse_follow.actor.url,
|
|
"object": reverse_follow_serializer.data,
|
|
}
|
|
|
|
actors.SYSTEM_ACTORS["test"].post_inbox(undo, actor=follow.actor)
|
|
deliver.assert_called_once_with(
|
|
expected_undo, to=[follow.actor.url], on_behalf_of=test_actor
|
|
)
|
|
|
|
assert models.Follow.objects.count() == 0
|
|
|
|
|
|
def test_library_actor_handles_follow_manual_approval(preferences, mocker, factories):
|
|
preferences["federation__music_needs_approval"] = True
|
|
actor = factories["federation.Actor"]()
|
|
now = timezone.now()
|
|
mocker.patch("django.utils.timezone.now", return_value=now)
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
data = {
|
|
"actor": actor.url,
|
|
"type": "Follow",
|
|
"id": "http://test.federation/user#follows/267",
|
|
"object": library_actor.url,
|
|
}
|
|
|
|
library_actor.system_conf.post_inbox(data, actor=actor)
|
|
follow = library_actor.received_follows.first()
|
|
|
|
assert follow.actor == actor
|
|
assert follow.approved is None
|
|
|
|
|
|
def test_library_actor_handles_follow_auto_approval(preferences, mocker, factories):
|
|
preferences["federation__music_needs_approval"] = False
|
|
actor = factories["federation.Actor"]()
|
|
mocker.patch("funkwhale_api.federation.activity.accept_follow")
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
data = {
|
|
"actor": actor.url,
|
|
"type": "Follow",
|
|
"id": "http://test.federation/user#follows/267",
|
|
"object": library_actor.url,
|
|
}
|
|
library_actor.system_conf.post_inbox(data, actor=actor)
|
|
|
|
follow = library_actor.received_follows.first()
|
|
|
|
assert follow.actor == actor
|
|
assert follow.approved is True
|
|
|
|
|
|
def test_library_actor_handles_accept(mocker, factories):
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
actor = factories["federation.Actor"]()
|
|
pending_follow = factories["federation.Follow"](
|
|
actor=library_actor, target=actor, approved=None
|
|
)
|
|
serializer = serializers.AcceptFollowSerializer(pending_follow)
|
|
library_actor.system_conf.post_inbox(serializer.data, actor=actor)
|
|
|
|
pending_follow.refresh_from_db()
|
|
|
|
assert pending_follow.approved is True
|
|
|
|
|
|
def test_library_actor_handle_create_audio_no_library(mocker, factories):
|
|
# when we receive inbox create audio, we should not do anything
|
|
# if we don't have a configured library matching the sender
|
|
mocked_create = mocker.patch(
|
|
"funkwhale_api.federation.serializers.AudioSerializer.create"
|
|
)
|
|
actor = factories["federation.Actor"]()
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
data = {
|
|
"actor": actor.url,
|
|
"type": "Create",
|
|
"id": "http://test.federation/audio/create",
|
|
"object": {
|
|
"id": "https://batch.import",
|
|
"type": "Collection",
|
|
"totalItems": 2,
|
|
"items": factories["federation.Audio"].create_batch(size=2),
|
|
},
|
|
}
|
|
library_actor.system_conf.post_inbox(data, actor=actor)
|
|
|
|
mocked_create.assert_not_called()
|
|
models.LibraryTrack.objects.count() == 0
|
|
|
|
|
|
def test_library_actor_handle_create_audio_no_library_enabled(mocker, factories):
|
|
# when we receive inbox create audio, we should not do anything
|
|
# if we don't have an enabled library
|
|
mocked_create = mocker.patch(
|
|
"funkwhale_api.federation.serializers.AudioSerializer.create"
|
|
)
|
|
disabled_library = factories["federation.Library"](federation_enabled=False)
|
|
actor = disabled_library.actor
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
data = {
|
|
"actor": actor.url,
|
|
"type": "Create",
|
|
"id": "http://test.federation/audio/create",
|
|
"object": {
|
|
"id": "https://batch.import",
|
|
"type": "Collection",
|
|
"totalItems": 2,
|
|
"items": factories["federation.Audio"].create_batch(size=2),
|
|
},
|
|
}
|
|
library_actor.system_conf.post_inbox(data, actor=actor)
|
|
|
|
mocked_create.assert_not_called()
|
|
models.LibraryTrack.objects.count() == 0
|
|
|
|
|
|
def test_library_actor_handle_create_audio(mocker, factories):
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
remote_library = factories["federation.Library"](federation_enabled=True)
|
|
|
|
data = {
|
|
"actor": remote_library.actor.url,
|
|
"type": "Create",
|
|
"id": "http://test.federation/audio/create",
|
|
"object": {
|
|
"id": "https://batch.import",
|
|
"type": "Collection",
|
|
"totalItems": 2,
|
|
"items": factories["federation.Audio"].create_batch(size=2),
|
|
},
|
|
}
|
|
|
|
library_actor.system_conf.post_inbox(data, actor=remote_library.actor)
|
|
|
|
lts = list(remote_library.tracks.order_by("id"))
|
|
|
|
assert len(lts) == 2
|
|
|
|
for i, a in enumerate(data["object"]["items"]):
|
|
lt = lts[i]
|
|
assert lt.pk is not None
|
|
assert lt.url == a["id"]
|
|
assert lt.library == remote_library
|
|
assert lt.audio_url == a["url"]["href"]
|
|
assert lt.audio_mimetype == a["url"]["mediaType"]
|
|
assert lt.metadata == a["metadata"]
|
|
assert lt.title == a["metadata"]["recording"]["title"]
|
|
assert lt.artist_name == a["metadata"]["artist"]["name"]
|
|
assert lt.album_title == a["metadata"]["release"]["title"]
|
|
assert lt.published_date == arrow.get(a["published"])
|
|
|
|
|
|
def test_library_actor_handle_create_audio_autoimport(mocker, factories):
|
|
mocked_import = mocker.patch("funkwhale_api.common.utils.on_commit")
|
|
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
|
remote_library = factories["federation.Library"](
|
|
federation_enabled=True, autoimport=True
|
|
)
|
|
|
|
data = {
|
|
"actor": remote_library.actor.url,
|
|
"type": "Create",
|
|
"id": "http://test.federation/audio/create",
|
|
"object": {
|
|
"id": "https://batch.import",
|
|
"type": "Collection",
|
|
"totalItems": 2,
|
|
"items": factories["federation.Audio"].create_batch(size=2),
|
|
},
|
|
}
|
|
|
|
library_actor.system_conf.post_inbox(data, actor=remote_library.actor)
|
|
|
|
lts = list(remote_library.tracks.order_by("id"))
|
|
|
|
assert len(lts) == 2
|
|
|
|
for i, a in enumerate(data["object"]["items"]):
|
|
lt = lts[i]
|
|
assert lt.pk is not None
|
|
assert lt.url == a["id"]
|
|
assert lt.library == remote_library
|
|
assert lt.audio_url == a["url"]["href"]
|
|
assert lt.audio_mimetype == a["url"]["mediaType"]
|
|
assert lt.metadata == a["metadata"]
|
|
assert lt.title == a["metadata"]["recording"]["title"]
|
|
assert lt.artist_name == a["metadata"]["artist"]["name"]
|
|
assert lt.album_title == a["metadata"]["release"]["title"]
|
|
assert lt.published_date == arrow.get(a["published"])
|
|
|
|
batch = music_models.ImportBatch.objects.latest("id")
|
|
|
|
assert batch.jobs.count() == len(lts)
|
|
assert batch.source == "federation"
|
|
assert batch.submitted_by is None
|
|
|
|
for i, job in enumerate(batch.jobs.order_by("id")):
|
|
lt = lts[i]
|
|
assert job.library_track == lt
|
|
assert job.mbid == lt.mbid
|
|
assert job.source == lt.url
|
|
|
|
mocked_import.assert_any_call(
|
|
music_tasks.import_job_run.delay, import_job_id=job.pk, use_acoustid=False
|
|
)
|