Newer
Older
import arrow
from django.utils import timezone
from rest_framework import exceptions
Eliot Berriot
committed
from funkwhale_api.federation import activity
from funkwhale_api.federation import actors
from funkwhale_api.federation import models
from funkwhale_api.federation import serializers
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",
r_mock.get(actor_url, json=payload)
r = actors.get_actor_data(actor_url)
assert r == payload
def test_get_actor(factories, r_mock):
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
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
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"),
)
"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"),
)
"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 = {
Eliot Berriot
committed
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
Eliot Berriot
committed
],
"id": utils.full_url(
reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
),
Eliot Berriot
committed
"type": "OrderedCollection",
"totalItems": 0,
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)
"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(),
cc=[],
summary=None,
sensitive=False,
attributedTo=test_actor.url,
attachment=[],
to=[actor.url],
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(
"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,
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)
Eliot Berriot
committed
@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):
Eliot Berriot
committed
if not domain:
domain = settings.FEDERATION_HOSTNAME
actor = nodb_factories["federation.Actor"](
preferred_username=username, domain=domain
Eliot Berriot
committed
)
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"]
Eliot Berriot
committed
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)
Eliot Berriot
committed
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()
"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)
Eliot Berriot
committed
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,
Eliot Berriot
committed
)
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)
"@context": serializers.AP_CONTEXT,
"type": "Undo",
"id": follow_serializer.data["id"] + "/undo",
"actor": follow.actor.url,
"object": follow_serializer.data,
"@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)
expected_undo, to=[follow.actor.url], on_behalf_of=test_actor
)
Eliot Berriot
committed
def test_library_actor_handles_follow_manual_approval(preferences, mocker, factories):
preferences["federation__music_needs_approval"] = True
actor = factories["federation.Actor"]()
Eliot Berriot
committed
now = timezone.now()
mocker.patch("django.utils.timezone.now", return_value=now)
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
Eliot Berriot
committed
data = {
"actor": actor.url,
"type": "Follow",
"id": "http://test.federation/user#follows/267",
"object": library_actor.url,
Eliot Berriot
committed
}
library_actor.system_conf.post_inbox(data, actor=actor)
Eliot Berriot
committed
follow = library_actor.received_follows.first()
Eliot Berriot
committed
Eliot Berriot
committed
assert follow.actor == actor
assert follow.approved is None
Eliot Berriot
committed
def test_library_actor_handles_follow_auto_approval(preferences, mocker, factories):
preferences["federation__music_needs_approval"] = False
actor = factories["federation.Actor"]()
accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow")
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
Eliot Berriot
committed
data = {
"actor": actor.url,
"type": "Follow",
"id": "http://test.federation/user#follows/267",
"object": library_actor.url,
Eliot Berriot
committed
}
library_actor.system_conf.post_inbox(data, actor=actor)
Eliot Berriot
committed
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
Eliot Berriot
committed
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()
"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)
assert len(lts) == 2
lt = lts[i]
assert lt.pk is not None
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)
assert len(lts) == 2
lt = lts[i]
assert lt.pk is not None
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"])
assert batch.jobs.count() == len(lts)
assert batch.submitted_by is None
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