Resolve "Per-user libraries" (use !368 instead)

This commit is contained in:
Eliot Berriot 2018-09-06 18:35:02 +00:00
commit 2ea21994ee
144 changed files with 6709 additions and 5307 deletions

View file

@ -1,32 +1,7 @@
from funkwhale_api.federation import activity, serializers
import pytest
def test_deliver(factories, r_mock, mocker, settings):
settings.CELERY_TASK_ALWAYS_EAGER = True
to = factories["federation.Actor"]()
mocker.patch("funkwhale_api.federation.actors.get_actor", return_value=to)
sender = factories["federation.Actor"]()
ac = {
"id": "http://test.federation/activity",
"type": "Create",
"actor": sender.url,
"object": {
"id": "http://test.federation/note",
"type": "Note",
"content": "Hello",
},
}
r_mock.post(to.inbox_url)
activity.deliver(ac, to=[to.url], on_behalf_of=sender)
request = r_mock.request_history[0]
assert r_mock.called is True
assert r_mock.call_count == 1
assert request.url == to.inbox_url
assert request.headers["content-type"] == "application/activity+json"
from funkwhale_api.federation import activity, serializers, tasks
def test_accept_follow(mocker, factories):
@ -35,5 +10,125 @@ def test_accept_follow(mocker, factories):
expected_accept = serializers.AcceptFollowSerializer(follow).data
activity.accept_follow(follow)
deliver.assert_called_once_with(
expected_accept, to=[follow.actor.url], on_behalf_of=follow.target
expected_accept, to=[follow.actor.fid], on_behalf_of=follow.target
)
def test_receive_validates_basic_attributes_and_stores_activity(factories, now, mocker):
mocked_dispatch = mocker.patch(
"funkwhale_api.federation.tasks.dispatch_inbox.delay"
)
local_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
another_actor = factories["federation.Actor"]()
a = {
"@context": [],
"actor": remote_actor.fid,
"type": "Noop",
"id": "https://test.activity",
"to": [local_actor.fid],
"cc": [another_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"]
mocked_dispatch.assert_called_once_with(activity_id=copy.pk)
inbox_item = copy.inbox_items.get(actor__fid=local_actor.fid)
assert inbox_item.is_delivered is False
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(mocker):
router = activity.InboxRouter()
handler = mocker.stub(name="handler")
router.connect({"type": "Follow"}, handler)
good_message = {"type": "Follow"}
router.dispatch(good_message, context={})
handler.assert_called_once_with(good_message, context={})
@pytest.mark.parametrize(
"route,payload,expected",
[
({"type": "Follow"}, {"type": "Follow"}, True),
({"type": "Follow"}, {"type": "Noop"}, False),
({"type": "Follow"}, {"type": "Follow", "id": "https://hello"}, 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()
recipient = factories["federation.Actor"]()
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"],
},
"actor": actor,
"to": [r1],
"cc": [r2, activity.PUBLIC_ADDRESS],
}
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
for recipient, type in [(r1, "to"), (r2, "cc")]:
item = a.inbox_items.get(actor=recipient)
assert item.is_delivered is False
assert item.last_delivery_date is None
assert item.delivery_attempts == 0
assert item.type == type

View file

@ -1,12 +1,9 @@
import pendulum
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):
@ -25,8 +22,8 @@ def test_actor_fetching(r_mock):
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)
r_mock.get(actor.fid, json=payload)
new_actor = actors.get_actor(actor.fid)
assert new_actor.pk is not None
assert serializers.ActorSerializer(new_actor).data == payload
@ -36,7 +33,7 @@ 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)
new_actor = actors.get_actor(actor.fid)
assert new_actor == actor
get_data.assert_not_called()
@ -49,46 +46,13 @@ def test_get_actor_refresh(factories, preferences, mocker):
# actor changed their username in the meantime
payload["preferredUsername"] = "New me"
mocker.patch("funkwhale_api.federation.actors.get_actor_data", return_value=payload)
new_actor = actors.get_actor(actor.url)
new_actor = actors.get_actor(actor.fid)
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):
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):
mocker.patch(
"funkwhale_api.federation.keys.get_key_pair",
@ -101,7 +65,7 @@ def test_get_test(db, mocker, settings):
"name": "{}'s test account".format(settings.FEDERATION_HOSTNAME),
"manually_approves_followers": False,
"public_key": "public",
"url": utils.full_url(
"fid": utils.full_url(
reverse("federation:instance-actors-detail", kwargs={"actor": "test"})
),
"shared_inbox_url": utils.full_url(
@ -162,7 +126,7 @@ def test_test_post_inbox_handles_create_note(settings, mocker, factories):
now = timezone.now()
mocker.patch("django.utils.timezone.now", return_value=now)
data = {
"actor": actor.url,
"actor": actor.fid,
"type": "Create",
"id": "http://test.federation/activity",
"object": {
@ -180,21 +144,21 @@ def test_test_post_inbox_handles_create_note(settings, mocker, factories):
cc=[],
summary=None,
sensitive=False,
attributedTo=test_actor.url,
attributedTo=test_actor.fid,
attachment=[],
to=[actor.url],
to=[actor.fid],
url="https://{}/activities/note/{}".format(
settings.FEDERATION_HOSTNAME, now.timestamp()
),
tag=[{"href": actor.url, "name": actor.mention_username, "type": "Mention"}],
tag=[{"href": actor.fid, "name": actor.full_username, "type": "Mention"}],
)
expected_activity = {
"@context": serializers.AP_CONTEXT,
"actor": test_actor.url,
"actor": test_actor.fid,
"id": "https://{}/activities/note/{}/activity".format(
settings.FEDERATION_HOSTNAME, now.timestamp()
),
"to": actor.url,
"to": actor.fid,
"type": "Create",
"published": now.isoformat(),
"object": expected_note,
@ -203,14 +167,14 @@ def test_test_post_inbox_handles_create_note(settings, mocker, factories):
actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
deliver.assert_called_once_with(
expected_activity,
to=[actor.url],
to=[actor.fid],
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)
from_db = models.Actor.objects.get(fid=test.fid)
for f in test._meta.fields:
assert getattr(from_db, f.name) == getattr(test, f.name)
@ -247,17 +211,11 @@ def test_actor_system_conf(username, domain, expected, nodb_factories, settings)
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
@pytest.mark.skip("Refactoring in progress")
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)
activity = nodb_factories["federation.Activity"](type="Create", actor=actor)
serializer = serializers.ActivitySerializer(data=activity)
assert serializer.is_valid()
actors.SYSTEM_ACTORS["test"].handle(activity, actor)
@ -270,10 +228,10 @@ def test_test_actor_handles_follow(settings, mocker, factories):
accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow")
test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
data = {
"actor": actor.url,
"actor": actor.fid,
"type": "Follow",
"id": "http://test.federation/user#follows/267",
"object": test_actor.url,
"object": test_actor.fid,
}
actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
follow = models.Follow.objects.get(target=test_actor, approved=True)
@ -282,7 +240,7 @@ def test_test_actor_handles_follow(settings, mocker, factories):
deliver.assert_called_once_with(
serializers.FollowSerializer(follow_back).data,
on_behalf_of=test_actor,
to=[actor.url],
to=[actor.fid],
)
@ -299,215 +257,20 @@ def test_test_actor_handles_undo_follow(settings, mocker, factories):
"@context": serializers.AP_CONTEXT,
"type": "Undo",
"id": follow_serializer.data["id"] + "/undo",
"actor": follow.actor.url,
"actor": follow.actor.fid,
"object": follow_serializer.data,
}
expected_undo = {
"@context": serializers.AP_CONTEXT,
"type": "Undo",
"id": reverse_follow_serializer.data["id"] + "/undo",
"actor": reverse_follow.actor.url,
"actor": reverse_follow.actor.fid,
"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
expected_undo, to=[follow.actor.fid], 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 == pendulum.parse(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 == pendulum.parse(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
)

View file

@ -0,0 +1,53 @@
from funkwhale_api.federation import api_serializers
from funkwhale_api.federation import serializers
def test_library_serializer(factories):
library = factories["music.Library"](files_count=5678)
expected = {
"fid": library.fid,
"uuid": str(library.uuid),
"actor": serializers.APIActorSerializer(library.actor).data,
"name": library.name,
"description": library.description,
"creation_date": library.creation_date.isoformat().split("+")[0] + "Z",
"files_count": library.files_count,
"privacy_level": library.privacy_level,
"follow": None,
}
serializer = api_serializers.LibrarySerializer(library)
assert serializer.data == expected
def test_library_serializer_with_follow(factories):
library = factories["music.Library"](files_count=5678)
follow = factories["federation.LibraryFollow"](target=library)
setattr(library, "_follows", [follow])
expected = {
"fid": library.fid,
"uuid": str(library.uuid),
"actor": serializers.APIActorSerializer(library.actor).data,
"name": library.name,
"description": library.description,
"creation_date": library.creation_date.isoformat().split("+")[0] + "Z",
"files_count": library.files_count,
"privacy_level": library.privacy_level,
"follow": api_serializers.NestedLibraryFollowSerializer(follow).data,
}
serializer = api_serializers.LibrarySerializer(library)
assert serializer.data == expected
def test_library_serializer_validates_existing_follow(factories):
follow = factories["federation.LibraryFollow"]()
serializer = api_serializers.LibraryFollowSerializer(
data={"target": follow.target.uuid}, context={"actor": follow.actor}
)
assert serializer.is_valid() is False
assert "target" in serializer.errors

View file

@ -0,0 +1,51 @@
from django.urls import reverse
from funkwhale_api.federation import api_serializers
from funkwhale_api.federation import serializers
from funkwhale_api.federation import views
def test_user_can_list_their_library_follows(factories, logged_in_api_client):
# followed by someont else
factories["federation.LibraryFollow"]()
follow = factories["federation.LibraryFollow"](
actor__user=logged_in_api_client.user
)
url = reverse("api:v1:federation:library-follows-list")
response = logged_in_api_client.get(url)
assert response.data["count"] == 1
assert response.data["results"][0]["uuid"] == str(follow.uuid)
def test_user_can_scan_library_using_url(mocker, factories, logged_in_api_client):
library = factories["music.Library"]()
mocked_retrieve = mocker.patch(
"funkwhale_api.federation.utils.retrieve", return_value=library
)
url = reverse("api:v1:federation:libraries-scan")
response = logged_in_api_client.post(url, {"fid": library.fid})
assert mocked_retrieve.call_count == 1
args = mocked_retrieve.call_args
assert args[0] == (library.fid,)
assert args[1]["queryset"].model == views.MusicLibraryViewSet.queryset.model
assert args[1]["serializer_class"] == serializers.LibrarySerializer
assert response.status_code == 200
assert response.data["results"] == [api_serializers.LibrarySerializer(library).data]
def test_can_follow_library(factories, logged_in_api_client, mocker):
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
actor = logged_in_api_client.user.create_actor()
library = factories["music.Library"]()
url = reverse("api:v1:federation:library-follows-list")
response = logged_in_api_client.post(url, {"target": library.uuid})
assert response.status_code == 201
follow = library.received_follows.latest("id")
assert follow.approved is None
assert follow.actor == actor
dispatch.assert_called_once_with({"type": "Follow"}, context={"follow": follow})

View file

@ -36,4 +36,4 @@ def test_authenticate(factories, mocker, api_request):
assert user.is_anonymous is True
assert actor.public_key == public.decode("utf-8")
assert actor.url == actor_url
assert actor.fid == actor_url

View file

@ -1,64 +0,0 @@
from funkwhale_api.federation import library, serializers
def test_library_scan_from_account_name(mocker, factories):
actor = factories["federation.Actor"](
preferred_username="library", domain="test.library"
)
get_resource_result = {"actor_url": actor.url}
get_resource = mocker.patch(
"funkwhale_api.federation.webfinger.get_resource",
return_value=get_resource_result,
)
actor_data = serializers.ActorSerializer(actor).data
actor_data["manuallyApprovesFollowers"] = False
actor_data["url"] = [
{
"type": "Link",
"name": "library",
"mediaType": "application/activity+json",
"href": "https://test.library",
}
]
get_actor_data = mocker.patch(
"funkwhale_api.federation.actors.get_actor_data", return_value=actor_data
)
get_library_data_result = {"test": "test"}
get_library_data = mocker.patch(
"funkwhale_api.federation.library.get_library_data",
return_value=get_library_data_result,
)
result = library.scan_from_account_name("library@test.actor")
get_resource.assert_called_once_with("acct:library@test.actor")
get_actor_data.assert_called_once_with(actor.url)
get_library_data.assert_called_once_with(actor_data["url"][0]["href"])
assert result == {
"webfinger": get_resource_result,
"actor": actor_data,
"library": get_library_data_result,
"local": {"following": False, "awaiting_approval": False},
}
def test_get_library_data(r_mock, factories):
actor = factories["federation.Actor"]()
url = "https://test.library"
conf = {"id": url, "items": [], "actor": actor, "page_size": 5}
data = serializers.PaginatedCollectionSerializer(conf).data
r_mock.get(url, json=data)
result = library.get_library_data(url)
for f in ["totalItems", "actor", "id", "type"]:
assert result[f] == data[f]
def test_get_library_data_requires_authentication(r_mock, factories):
url = "https://test.library"
r_mock.get(url, status_code=403)
result = library.get_library_data(url)
assert result["errors"] == ["Permission denied while scanning library"]

View file

@ -20,12 +20,37 @@ def test_cannot_duplicate_follow(factories):
def test_follow_federation_url(factories):
follow = factories["federation.Follow"](local=True)
expected = "{}#follows/{}".format(follow.actor.url, follow.uuid)
expected = "{}#follows/{}".format(follow.actor.fid, follow.uuid)
assert follow.get_federation_url() == expected
assert follow.get_federation_id() == expected
def test_library_model_unique_per_actor(factories):
library = factories["federation.Library"]()
with pytest.raises(db.IntegrityError):
factories["federation.Library"](actor=library.actor)
def test_actor_get_quota(factories):
library = factories["music.Library"]()
factories["music.TrackFile"](
library=library,
import_status="pending",
audio_file__from_path=None,
audio_file__data=b"a",
)
factories["music.TrackFile"](
library=library,
import_status="skipped",
audio_file__from_path=None,
audio_file__data=b"aa",
)
factories["music.TrackFile"](
library=library,
import_status="errored",
audio_file__from_path=None,
audio_file__data=b"aaa",
)
factories["music.TrackFile"](
library=library,
import_status="finished",
audio_file__from_path=None,
audio_file__data=b"aaaa",
)
expected = {"total": 10, "pending": 1, "skipped": 2, "errored": 3, "finished": 4}
assert library.actor.get_current_usage() == expected

View file

@ -1,61 +0,0 @@
from rest_framework.views import APIView
from funkwhale_api.federation import actors, permissions
def test_library_follower(factories, api_request, anonymous_user, preferences):
preferences["federation__music_needs_approval"] = True
view = APIView.as_view()
permission = permissions.LibraryFollower()
request = api_request.get("/")
setattr(request, "user", anonymous_user)
check = permission.has_permission(request, view)
assert check is False
def test_library_follower_actor_non_follower(
factories, api_request, anonymous_user, preferences
):
preferences["federation__music_needs_approval"] = True
actor = factories["federation.Actor"]()
view = APIView.as_view()
permission = permissions.LibraryFollower()
request = api_request.get("/")
setattr(request, "user", anonymous_user)
setattr(request, "actor", actor)
check = permission.has_permission(request, view)
assert check is False
def test_library_follower_actor_follower_not_approved(
factories, api_request, anonymous_user, preferences
):
preferences["federation__music_needs_approval"] = True
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
follow = factories["federation.Follow"](target=library, approved=False)
view = APIView.as_view()
permission = permissions.LibraryFollower()
request = api_request.get("/")
setattr(request, "user", anonymous_user)
setattr(request, "actor", follow.actor)
check = permission.has_permission(request, view)
assert check is False
def test_library_follower_actor_follower(
factories, api_request, anonymous_user, preferences
):
preferences["federation__music_needs_approval"] = True
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
follow = factories["federation.Follow"](target=library, approved=True)
view = APIView.as_view()
permission = permissions.LibraryFollower()
request = api_request.get("/")
setattr(request, "user", anonymous_user)
setattr(request, "actor", follow.actor)
check = permission.has_permission(request, view)
assert check is True

View file

@ -0,0 +1,147 @@
import pytest
from funkwhale_api.federation import routes, serializers
@pytest.mark.parametrize(
"route,handler",
[
({"type": "Follow"}, routes.inbox_follow),
({"type": "Accept"}, routes.inbox_accept),
],
)
def test_inbox_routes(route, handler):
for r, h in routes.inbox.routes:
if r == route:
assert h == handler
return
assert False, "Inbox route {} not found".format(route)
@pytest.mark.parametrize(
"route,handler",
[
({"type": "Accept"}, routes.outbox_accept),
({"type": "Follow"}, routes.outbox_follow),
],
)
def test_outbox_routes(route, handler):
for r, h in routes.outbox.routes:
if r == route:
assert h == handler
return
assert False, "Outbox route {} not found".format(route)
def test_inbox_follow_library_autoapprove(factories, mocker):
mocked_accept_follow = mocker.patch(
"funkwhale_api.federation.activity.accept_follow"
)
local_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
library = factories["music.Library"](actor=local_actor, privacy_level="everyone")
ii = factories["federation.InboxItem"](actor=local_actor)
payload = {
"type": "Follow",
"id": "https://test.follow",
"actor": remote_actor.fid,
"object": library.fid,
}
routes.inbox_follow(
payload,
context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True},
)
follow = library.received_follows.latest("id")
assert follow.fid == payload["id"]
assert follow.actor == remote_actor
assert follow.approved is True
mocked_accept_follow.assert_called_once_with(follow)
def test_inbox_follow_library_manual_approve(factories, mocker):
mocked_accept_follow = mocker.patch(
"funkwhale_api.federation.activity.accept_follow"
)
local_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
library = factories["music.Library"](actor=local_actor, privacy_level="me")
ii = factories["federation.InboxItem"](actor=local_actor)
payload = {
"type": "Follow",
"id": "https://test.follow",
"actor": remote_actor.fid,
"object": library.fid,
}
routes.inbox_follow(
payload,
context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True},
)
follow = library.received_follows.latest("id")
assert follow.fid == payload["id"]
assert follow.actor == remote_actor
assert follow.approved is False
mocked_accept_follow.assert_not_called()
def test_outbox_accept(factories, mocker):
remote_actor = factories["federation.Actor"]()
follow = factories["federation.LibraryFollow"](actor=remote_actor)
activity = list(routes.outbox_accept({"follow": follow}))[0]
serializer = serializers.AcceptFollowSerializer(
follow, context={"actor": follow.target.actor}
)
expected = serializer.data
expected["to"] = [follow.actor]
assert activity["payload"] == expected
assert activity["actor"] == follow.target.actor
def test_inbox_accept(factories, mocker):
mocked_scan = mocker.patch("funkwhale_api.music.models.Library.schedule_scan")
local_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
follow = factories["federation.LibraryFollow"](
actor=local_actor, target__actor=remote_actor
)
assert follow.approved is None
serializer = serializers.AcceptFollowSerializer(
follow, context={"actor": remote_actor}
)
ii = factories["federation.InboxItem"](actor=local_actor)
routes.inbox_accept(
serializer.data,
context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True},
)
follow.refresh_from_db()
assert follow.approved is True
mocked_scan.assert_called_once_with()
def test_outbox_follow_library(factories, mocker):
follow = factories["federation.LibraryFollow"]()
activity = list(routes.outbox_follow({"follow": follow}))[0]
serializer = serializers.FollowSerializer(follow, context={"actor": follow.actor})
expected = serializer.data
expected["to"] = [follow.target.actor]
assert activity["payload"] == expected
assert activity["actor"] == follow.actor

View file

@ -1,8 +1,7 @@
import pendulum
import pytest
from django.core.paginator import Paginator
from funkwhale_api.federation import actors, models, serializers, utils
from funkwhale_api.federation import activity, models, serializers, utils
def test_actor_serializer_from_ap(db):
@ -31,7 +30,7 @@ def test_actor_serializer_from_ap(db):
actor = serializer.build()
assert actor.url == payload["id"]
assert actor.fid == payload["id"]
assert actor.inbox_url == payload["inbox"]
assert actor.outbox_url == payload["outbox"]
assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"]
@ -62,7 +61,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db):
actor = serializer.build()
assert actor.url == payload["id"]
assert actor.fid == payload["id"]
assert actor.inbox_url == payload["inbox"]
assert actor.outbox_url == payload["outbox"]
assert actor.followers_url == payload["followers"]
@ -98,7 +97,7 @@ def test_actor_serializer_to_ap():
"endpoints": {"sharedInbox": "https://test.federation/inbox"},
}
ac = models.Actor(
url=expected["id"],
fid=expected["id"],
inbox_url=expected["inbox"],
outbox_url=expected["outbox"],
shared_inbox_url=expected["endpoints"]["sharedInbox"],
@ -130,7 +129,7 @@ def test_webfinger_serializer():
"aliases": ["https://test.federation/federation/instance/actor"],
}
actor = models.Actor(
url=expected["links"][0]["href"],
fid=expected["links"][0]["href"],
preferred_username="service",
domain="test.federation",
)
@ -149,10 +148,10 @@ def test_follow_serializer_to_ap(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url(),
"id": follow.get_federation_id(),
"type": "Follow",
"actor": follow.actor.url,
"object": follow.target.url,
"actor": follow.actor.fid,
"object": follow.target.fid,
}
assert serializer.data == expected
@ -165,8 +164,8 @@ def test_follow_serializer_save(factories):
data = {
"id": "https://test.follow",
"type": "Follow",
"actor": actor.url,
"object": target.url,
"actor": actor.fid,
"object": target.fid,
}
serializer = serializers.FollowSerializer(data=data)
@ -188,8 +187,8 @@ def test_follow_serializer_save_validates_on_context(factories):
data = {
"id": "https://test.follow",
"type": "Follow",
"actor": actor.url,
"object": target.url,
"actor": actor.fid,
"object": target.fid,
}
serializer = serializers.FollowSerializer(
data=data, context={"follow_actor": impostor, "follow_target": impostor}
@ -210,9 +209,9 @@ def test_accept_follow_serializer_representation(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url() + "/accept",
"id": follow.get_federation_id() + "/accept",
"type": "Accept",
"actor": follow.target.url,
"actor": follow.target.fid,
"object": serializers.FollowSerializer(follow).data,
}
@ -230,9 +229,9 @@ def test_accept_follow_serializer_save(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url() + "/accept",
"id": follow.get_federation_id() + "/accept",
"type": "Accept",
"actor": follow.target.url,
"actor": follow.target.fid,
"object": serializers.FollowSerializer(follow).data,
}
@ -254,7 +253,7 @@ def test_accept_follow_serializer_validates_on_context(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url() + "/accept",
"id": follow.get_federation_id() + "/accept",
"type": "Accept",
"actor": impostor.url,
"object": serializers.FollowSerializer(follow).data,
@ -278,9 +277,9 @@ def test_undo_follow_serializer_representation(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url() + "/undo",
"id": follow.get_federation_id() + "/undo",
"type": "Undo",
"actor": follow.actor.url,
"actor": follow.actor.fid,
"object": serializers.FollowSerializer(follow).data,
}
@ -298,9 +297,9 @@ def test_undo_follow_serializer_save(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url() + "/undo",
"id": follow.get_federation_id() + "/undo",
"type": "Undo",
"actor": follow.actor.url,
"actor": follow.actor.fid,
"object": serializers.FollowSerializer(follow).data,
}
@ -321,7 +320,7 @@ def test_undo_follow_serializer_validates_on_context(factories):
"https://w3id.org/security/v1",
{},
],
"id": follow.get_federation_url() + "/undo",
"id": follow.get_federation_id() + "/undo",
"type": "Undo",
"actor": impostor.url,
"object": serializers.FollowSerializer(follow).data,
@ -355,7 +354,7 @@ def test_paginated_collection_serializer(factories):
],
"type": "Collection",
"id": conf["id"],
"actor": actor.url,
"actor": actor.fid,
"totalItems": len(tfs),
"current": conf["id"] + "?page=1",
"last": conf["id"] + "?page=3",
@ -452,7 +451,7 @@ def test_collection_page_serializer(factories):
],
"type": "CollectionPage",
"id": conf["id"] + "?page=2",
"actor": actor.url,
"actor": actor.fid,
"totalItems": len(tfs),
"partOf": conf["id"],
"prev": conf["id"] + "?page=1",
@ -472,58 +471,148 @@ def test_collection_page_serializer(factories):
assert serializer.data == expected
def test_activity_pub_audio_serializer_to_library_track(factories):
remote_library = factories["federation.Library"]()
audio = factories["federation.Audio"]()
serializer = serializers.AudioSerializer(
data=audio, context={"library": remote_library}
def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories):
remote_library = factories["music.Library"]()
tf = factories["music.TrackFile"].build(library=remote_library)
data = serializers.AudioSerializer(tf).data
serializer1 = serializers.AudioSerializer(data=data)
serializer2 = serializers.AudioSerializer(data=data)
assert serializer1.is_valid(raise_exception=True) is True
assert serializer2.is_valid(raise_exception=True) is True
tf1 = serializer1.save()
tf2 = serializer2.save()
assert tf1 == tf2
assert tf1.library == remote_library
assert tf1.source == utils.full_url(tf.listen_url)
assert tf1.mimetype == tf.mimetype
assert tf1.bitrate == tf.bitrate
assert tf1.duration == tf.duration
assert tf1.size == tf.size
assert tf1.metadata == data
assert tf1.fid == tf.get_federation_id()
assert not tf1.audio_file
def test_music_library_serializer_to_ap(factories):
library = factories["music.Library"]()
# pending, errored and skippednot included
factories["music.TrackFile"](import_status="pending")
factories["music.TrackFile"](import_status="errored")
factories["music.TrackFile"](import_status="finished")
serializer = serializers.LibrarySerializer(library)
expected = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{},
],
"type": "Library",
"id": library.fid,
"name": library.name,
"summary": library.description,
"audience": "",
"actor": library.actor.fid,
"totalItems": 0,
"current": library.fid + "?page=1",
"last": library.fid + "?page=1",
"first": library.fid + "?page=1",
}
assert serializer.data == expected
def test_music_library_serializer_from_public(factories, mocker):
actor = factories["federation.Actor"]()
retrieve = mocker.patch(
"funkwhale_api.federation.utils.retrieve", return_value=actor
)
data = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{},
],
"audience": "https://www.w3.org/ns/activitystreams#Public",
"name": "Hello",
"summary": "World",
"type": "Library",
"id": "https://library.id",
"actor": actor.fid,
"totalItems": 12,
"first": "https://library.id?page=1",
"last": "https://library.id?page=2",
}
serializer = serializers.LibrarySerializer(data=data)
assert serializer.is_valid(raise_exception=True)
lt = serializer.save()
library = serializer.save()
assert lt.pk is not None
assert lt.url == audio["id"]
assert lt.library == remote_library
assert lt.audio_url == audio["url"]["href"]
assert lt.audio_mimetype == audio["url"]["mediaType"]
assert lt.metadata == audio["metadata"]
assert lt.title == audio["metadata"]["recording"]["title"]
assert lt.artist_name == audio["metadata"]["artist"]["name"]
assert lt.album_title == audio["metadata"]["release"]["title"]
assert lt.published_date == pendulum.parse(audio["published"])
def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories):
remote_library = factories["federation.Library"]()
audio = factories["federation.Audio"]()
serializer1 = serializers.AudioSerializer(
data=audio, context={"library": remote_library}
)
serializer2 = serializers.AudioSerializer(
data=audio, context={"library": remote_library}
assert library.actor == actor
assert library.fid == data["id"]
assert library.files_count == data["totalItems"]
assert library.privacy_level == "everyone"
assert library.name == "Hello"
assert library.description == "World"
retrieve.assert_called_once_with(
actor.fid,
queryset=actor.__class__,
serializer_class=serializers.ActorSerializer,
)
assert serializer1.is_valid() is True
assert serializer2.is_valid() is True
lt1 = serializer1.save()
lt2 = serializer2.save()
def test_music_library_serializer_from_private(factories, mocker):
actor = factories["federation.Actor"]()
retrieve = mocker.patch(
"funkwhale_api.federation.utils.retrieve", return_value=actor
)
data = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{},
],
"audience": "",
"name": "Hello",
"summary": "World",
"type": "Library",
"id": "https://library.id",
"actor": actor.fid,
"totalItems": 12,
"first": "https://library.id?page=1",
"last": "https://library.id?page=2",
}
serializer = serializers.LibrarySerializer(data=data)
assert lt1 == lt2
assert models.LibraryTrack.objects.count() == 1
assert serializer.is_valid(raise_exception=True)
library = serializer.save()
assert library.actor == actor
assert library.fid == data["id"]
assert library.files_count == data["totalItems"]
assert library.privacy_level == "me"
assert library.name == "Hello"
assert library.description == "World"
retrieve.assert_called_once_with(
actor.fid,
queryset=actor.__class__,
serializer_class=serializers.ActorSerializer,
)
def test_activity_pub_audio_serializer_to_ap(factories):
tf = factories["music.TrackFile"](
mimetype="audio/mp3", bitrate=42, duration=43, size=44
)
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
expected = {
"@context": serializers.AP_CONTEXT,
"type": "Audio",
"id": tf.get_federation_url(),
"id": tf.get_federation_id(),
"name": tf.track.full_name,
"published": tf.creation_date.isoformat(),
"updated": tf.modification_date.isoformat(),
@ -542,14 +631,14 @@ def test_activity_pub_audio_serializer_to_ap(factories):
"bitrate": tf.bitrate,
},
"url": {
"href": utils.full_url(tf.path),
"href": utils.full_url(tf.listen_url),
"type": "Link",
"mediaType": "audio/mp3",
},
"attributedTo": [library.url],
"library": tf.library.get_federation_id(),
}
serializer = serializers.AudioSerializer(tf, context={"actor": library})
serializer = serializers.AudioSerializer(tf)
assert serializer.data == expected
@ -561,11 +650,10 @@ def test_activity_pub_audio_serializer_to_ap_no_mbid(factories):
track__album__mbid=None,
track__album__artist__mbid=None,
)
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
expected = {
"@context": serializers.AP_CONTEXT,
"type": "Audio",
"id": tf.get_federation_url(),
"id": tf.get_federation_id(),
"name": tf.track.full_name,
"published": tf.creation_date.isoformat(),
"updated": tf.modification_date.isoformat(),
@ -573,116 +661,23 @@ def test_activity_pub_audio_serializer_to_ap_no_mbid(factories):
"artist": {"name": tf.track.artist.name, "musicbrainz_id": None},
"release": {"title": tf.track.album.title, "musicbrainz_id": None},
"recording": {"title": tf.track.title, "musicbrainz_id": None},
"size": None,
"size": tf.size,
"length": None,
"bitrate": None,
},
"url": {
"href": utils.full_url(tf.path),
"href": utils.full_url(tf.listen_url),
"type": "Link",
"mediaType": "audio/mp3",
},
"attributedTo": [library.url],
"library": tf.library.fid,
}
serializer = serializers.AudioSerializer(tf, context={"actor": library})
serializer = serializers.AudioSerializer(tf)
assert serializer.data == expected
def test_collection_serializer_to_ap(factories):
tf1 = factories["music.TrackFile"](mimetype="audio/mp3")
tf2 = factories["music.TrackFile"](mimetype="audio/ogg")
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
expected = {
"@context": serializers.AP_CONTEXT,
"id": "https://test.id",
"actor": library.url,
"totalItems": 2,
"type": "Collection",
"items": [
serializers.AudioSerializer(
tf1, context={"actor": library, "include_ap_context": False}
).data,
serializers.AudioSerializer(
tf2, context={"actor": library, "include_ap_context": False}
).data,
],
}
collection = {
"id": expected["id"],
"actor": library,
"items": [tf1, tf2],
"item_serializer": serializers.AudioSerializer,
}
serializer = serializers.CollectionSerializer(
collection, context={"actor": library, "id": "https://test.id"}
)
assert serializer.data == expected
def test_api_library_create_serializer_save(factories, r_mock):
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
actor = factories["federation.Actor"]()
follow = factories["federation.Follow"](target=actor, actor=library_actor)
actor_data = serializers.ActorSerializer(actor).data
actor_data["url"] = [
{"href": "https://test.library", "name": "library", "type": "Link"}
]
library_conf = {
"id": "https://test.library",
"items": range(10),
"actor": actor,
"page_size": 5,
}
library_data = serializers.PaginatedCollectionSerializer(library_conf).data
r_mock.get(actor.url, json=actor_data)
r_mock.get("https://test.library", json=library_data)
data = {
"actor": actor.url,
"autoimport": False,
"federation_enabled": True,
"download_files": False,
}
serializer = serializers.APILibraryCreateSerializer(data=data)
assert serializer.is_valid(raise_exception=True) is True
library = serializer.save()
follow = models.Follow.objects.get(target=actor, actor=library_actor, approved=None)
assert library.autoimport is data["autoimport"]
assert library.federation_enabled is data["federation_enabled"]
assert library.download_files is data["download_files"]
assert library.tracks_count == 10
assert library.actor == actor
assert library.follow == follow
def test_tapi_library_track_serializer_not_imported(factories):
lt = factories["federation.LibraryTrack"]()
serializer = serializers.APILibraryTrackSerializer(lt)
assert serializer.get_status(lt) == "not_imported"
def test_tapi_library_track_serializer_imported(factories):
tf = factories["music.TrackFile"](federation=True)
lt = tf.library_track
serializer = serializers.APILibraryTrackSerializer(lt)
assert serializer.get_status(lt) == "imported"
def test_tapi_library_track_serializer_import_pending(factories):
job = factories["music.ImportJob"](federation=True, status="pending")
lt = job.library_track
serializer = serializers.APILibraryTrackSerializer(lt)
assert serializer.get_status(lt) == "import_pending"
def test_local_actor_serializer_to_ap(factories):
expected = {
"@context": [
@ -708,7 +703,7 @@ def test_local_actor_serializer_to_ap(factories):
"endpoints": {"sharedInbox": "https://test.federation/inbox"},
}
ac = models.Actor.objects.create(
url=expected["id"],
fid=expected["id"],
inbox_url=expected["inbox"],
outbox_url=expected["outbox"],
shared_inbox_url=expected["endpoints"]["sharedInbox"],
@ -734,3 +729,45 @@ def test_local_actor_serializer_to_ap(factories):
serializer = serializers.ActorSerializer(ac)
assert serializer.data == expected
def test_activity_serializer_clean_recipients_empty(db):
s = serializers.BaseActivitySerializer()
with pytest.raises(serializers.serializers.ValidationError):
s.validate_recipients({})
with pytest.raises(serializers.serializers.ValidationError):
s.validate_recipients({"to": []})
with pytest.raises(serializers.serializers.ValidationError):
s.validate_recipients({"cc": []})
with pytest.raises(serializers.serializers.ValidationError):
s.validate_recipients({"to": ["nope"]})
with pytest.raises(serializers.serializers.ValidationError):
s.validate_recipients({"cc": ["nope"]})
def test_activity_serializer_clean_recipients(factories):
r1, r2, r3 = factories["federation.Actor"].create_batch(size=3)
s = serializers.BaseActivitySerializer()
expected = {"to": [r1, r2], "cc": [r3, activity.PUBLIC_ADDRESS]}
assert (
s.validate_recipients(
{"to": [r1.fid, r2.fid], "cc": [r3.fid, activity.PUBLIC_ADDRESS]}
)
== expected
)
def test_activity_serializer_clean_recipients_local(factories):
r = factories["federation.Actor"]()
s = serializers.BaseActivitySerializer(context={"local_recipients": True})
with pytest.raises(serializers.serializers.ValidationError):
s.validate_recipients({"to": [r]})

View file

@ -1,132 +1,37 @@
import datetime
import os
import pathlib
import pytest
from django.core.paginator import Paginator
from django.utils import timezone
from funkwhale_api.federation import serializers, tasks
def test_scan_library_does_nothing_if_federation_disabled(mocker, factories):
library = factories["federation.Library"](federation_enabled=False)
tasks.scan_library(library_id=library.pk)
assert library.tracks.count() == 0
def test_scan_library_page_does_nothing_if_federation_disabled(mocker, factories):
library = factories["federation.Library"](federation_enabled=False)
tasks.scan_library_page(library_id=library.pk, page_url=None)
assert library.tracks.count() == 0
def test_scan_library_fetches_page_and_calls_scan_page(mocker, factories, r_mock):
now = timezone.now()
library = factories["federation.Library"](federation_enabled=True)
collection_conf = {
"actor": library.actor,
"id": library.url,
"page_size": 10,
"items": range(10),
}
collection = serializers.PaginatedCollectionSerializer(collection_conf)
scan_page = mocker.patch("funkwhale_api.federation.tasks.scan_library_page.delay")
r_mock.get(collection_conf["id"], json=collection.data)
tasks.scan_library(library_id=library.pk)
scan_page.assert_called_once_with(
library_id=library.id, page_url=collection.data["first"], until=None
)
library.refresh_from_db()
assert library.fetched_date > now
def test_scan_page_fetches_page_and_creates_tracks(mocker, factories, r_mock):
library = factories["federation.Library"](federation_enabled=True)
tfs = factories["music.TrackFile"].create_batch(size=5)
page_conf = {
"actor": library.actor,
"id": library.url,
"page": Paginator(tfs, 5).page(1),
"item_serializer": serializers.AudioSerializer,
}
page = serializers.CollectionPageSerializer(page_conf)
r_mock.get(page.data["id"], json=page.data)
tasks.scan_library_page(library_id=library.pk, page_url=page.data["id"])
lts = list(library.tracks.all().order_by("-published_date"))
assert len(lts) == 5
def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock):
patched_scan = mocker.patch(
"funkwhale_api.federation.tasks.scan_library_page.delay"
)
library = factories["federation.Library"](federation_enabled=True)
tfs = factories["music.TrackFile"].create_batch(size=1)
page_conf = {
"actor": library.actor,
"id": library.url,
"page": Paginator(tfs, 3).page(1),
"item_serializer": serializers.AudioSerializer,
}
page = serializers.CollectionPageSerializer(page_conf)
data = page.data
data["next"] = data["id"]
r_mock.get(page.data["id"], json=data)
tasks.scan_library_page(library_id=library.pk, page_url=data["id"])
patched_scan.assert_not_called()
def test_scan_page_stops_once_until_is_reached(mocker, factories, r_mock):
library = factories["federation.Library"](federation_enabled=True)
tfs = list(reversed(factories["music.TrackFile"].create_batch(size=5)))
page_conf = {
"actor": library.actor,
"id": library.url,
"page": Paginator(tfs, 3).page(1),
"item_serializer": serializers.AudioSerializer,
}
page = serializers.CollectionPageSerializer(page_conf)
r_mock.get(page.data["id"], json=page.data)
tasks.scan_library_page(
library_id=library.pk, page_url=page.data["id"], until=tfs[1].creation_date
)
lts = list(library.tracks.all().order_by("-published_date"))
assert len(lts) == 2
for i, tf in enumerate(tfs[:1]):
assert tf.creation_date == lts[i].published_date
from funkwhale_api.federation import tasks
def test_clean_federation_music_cache_if_no_listen(preferences, factories):
preferences["federation__music_cache_duration"] = 60
lt1 = factories["federation.LibraryTrack"](with_audio_file=True)
lt2 = factories["federation.LibraryTrack"](with_audio_file=True)
lt3 = factories["federation.LibraryTrack"](with_audio_file=True)
factories["music.TrackFile"](accessed_date=timezone.now(), library_track=lt1)
factories["music.TrackFile"](
accessed_date=timezone.now() - datetime.timedelta(minutes=61), library_track=lt2
remote_library = factories["music.Library"]()
tf1 = factories["music.TrackFile"](
library=remote_library, accessed_date=timezone.now()
)
factories["music.TrackFile"](accessed_date=None, library_track=lt3)
path1 = lt1.audio_file.path
path2 = lt2.audio_file.path
path3 = lt3.audio_file.path
tf2 = factories["music.TrackFile"](
library=remote_library,
accessed_date=timezone.now() - datetime.timedelta(minutes=61),
)
tf3 = factories["music.TrackFile"](library=remote_library, accessed_date=None)
path1 = tf1.audio_file.path
path2 = tf2.audio_file.path
path3 = tf3.audio_file.path
tasks.clean_music_cache()
lt1.refresh_from_db()
lt2.refresh_from_db()
lt3.refresh_from_db()
tf1.refresh_from_db()
tf2.refresh_from_db()
tf3.refresh_from_db()
assert bool(lt1.audio_file) is True
assert bool(lt2.audio_file) is False
assert bool(lt3.audio_file) is False
assert bool(tf1.audio_file) is True
assert bool(tf2.audio_file) is False
assert bool(tf3.audio_file) is False
assert os.path.exists(path1) is True
assert os.path.exists(path2) is False
assert os.path.exists(path3) is False
@ -134,22 +39,202 @@ def test_clean_federation_music_cache_if_no_listen(preferences, factories):
def test_clean_federation_music_cache_orphaned(settings, preferences, factories):
preferences["federation__music_cache_duration"] = 60
path = os.path.join(settings.MEDIA_ROOT, "federation_cache")
path = os.path.join(settings.MEDIA_ROOT, "federation_cache", "tracks")
keep_path = os.path.join(os.path.join(path, "1a", "b2"), "keep.ogg")
remove_path = os.path.join(os.path.join(path, "c3", "d4"), "remove.ogg")
os.makedirs(os.path.dirname(keep_path), exist_ok=True)
os.makedirs(os.path.dirname(remove_path), exist_ok=True)
pathlib.Path(keep_path).touch()
pathlib.Path(remove_path).touch()
lt = factories["federation.LibraryTrack"](
with_audio_file=True, audio_file__path=keep_path
tf = factories["music.TrackFile"](
accessed_date=timezone.now(), audio_file__path=keep_path
)
factories["music.TrackFile"](library_track=lt, accessed_date=timezone.now())
tasks.clean_music_cache()
lt.refresh_from_db()
tf.refresh_from_db()
assert bool(lt.audio_file) is True
assert os.path.exists(lt.audio_file.path) is True
assert bool(tf.audio_file) is True
assert os.path.exists(tf.audio_file.path) is True
assert os.path.exists(remove_path) is False
def test_handle_in(factories, mocker, now):
mocked_dispatch = mocker.patch("funkwhale_api.federation.routes.inbox.dispatch")
r1 = factories["users.User"](with_actor=True).actor
r2 = factories["users.User"](with_actor=True).actor
a = factories["federation.Activity"](payload={"hello": "world"})
ii1 = factories["federation.InboxItem"](activity=a, actor=r1)
ii2 = factories["federation.InboxItem"](activity=a, actor=r2)
tasks.dispatch_inbox(activity_id=a.pk)
mocked_dispatch.assert_called_once_with(
a.payload, context={"actor": a.actor, "inbox_items": [ii1, ii2]}
)
ii1.refresh_from_db()
ii2.refresh_from_db()
assert ii1.is_delivered is True
assert ii2.is_delivered is True
assert ii1.last_delivery_date == now
assert ii2.last_delivery_date == now
def test_handle_in_error(factories, mocker, now):
mocker.patch(
"funkwhale_api.federation.routes.inbox.dispatch", side_effect=Exception()
)
r1 = factories["users.User"](with_actor=True).actor
r2 = factories["users.User"](with_actor=True).actor
a = factories["federation.Activity"](payload={"hello": "world"})
factories["federation.InboxItem"](activity=a, actor=r1)
factories["federation.InboxItem"](activity=a, actor=r2)
with pytest.raises(Exception):
tasks.dispatch_inbox(activity_id=a.pk)
assert a.inbox_items.filter(is_delivered=False).count() == 2
def test_dispatch_outbox_to_inbox(factories, mocker):
mocked_inbox = mocker.patch("funkwhale_api.federation.tasks.dispatch_inbox.delay")
mocked_deliver_to_remote_inbox = mocker.patch(
"funkwhale_api.federation.tasks.deliver_to_remote_inbox.delay"
)
activity = factories["federation.Activity"](actor__local=True)
factories["federation.InboxItem"](activity=activity, actor__local=True)
remote_ii = factories["federation.InboxItem"](
activity=activity,
actor__shared_inbox_url=None,
actor__inbox_url="https://test.inbox",
)
tasks.dispatch_outbox(activity_id=activity.pk)
mocked_inbox.assert_called_once_with(activity_id=activity.pk)
mocked_deliver_to_remote_inbox.assert_called_once_with(
activity_id=activity.pk, inbox_url=remote_ii.actor.inbox_url
)
def test_dispatch_outbox_to_shared_inbox_url(factories, mocker):
mocked_deliver_to_remote_inbox = mocker.patch(
"funkwhale_api.federation.tasks.deliver_to_remote_inbox.delay"
)
activity = factories["federation.Activity"](actor__local=True)
# shared inbox
remote_ii_shared1 = factories["federation.InboxItem"](
activity=activity, actor__shared_inbox_url="https://shared.inbox"
)
# another on the same shared inbox
factories["federation.InboxItem"](
activity=activity, actor__shared_inbox_url="https://shared.inbox"
)
# one on a dedicated inbox
remote_ii_single = factories["federation.InboxItem"](
activity=activity,
actor__shared_inbox_url=None,
actor__inbox_url="https://single.inbox",
)
tasks.dispatch_outbox(activity_id=activity.pk)
assert mocked_deliver_to_remote_inbox.call_count == 2
mocked_deliver_to_remote_inbox.assert_any_call(
activity_id=activity.pk,
shared_inbox_url=remote_ii_shared1.actor.shared_inbox_url,
)
mocked_deliver_to_remote_inbox.assert_any_call(
activity_id=activity.pk, inbox_url=remote_ii_single.actor.inbox_url
)
def test_deliver_to_remote_inbox_inbox_url(factories, r_mock):
activity = factories["federation.Activity"]()
url = "https://test.shared/"
r_mock.post(url)
tasks.deliver_to_remote_inbox(activity_id=activity.pk, inbox_url=url)
request = r_mock.request_history[0]
assert r_mock.called is True
assert r_mock.call_count == 1
assert request.url == url
assert request.headers["content-type"] == "application/activity+json"
assert request.json() == activity.payload
def test_deliver_to_remote_inbox_shared_inbox_url(factories, r_mock):
activity = factories["federation.Activity"]()
url = "https://test.shared/"
r_mock.post(url)
tasks.deliver_to_remote_inbox(activity_id=activity.pk, shared_inbox_url=url)
request = r_mock.request_history[0]
assert r_mock.called is True
assert r_mock.call_count == 1
assert request.url == url
assert request.headers["content-type"] == "application/activity+json"
assert request.json() == activity.payload
def test_deliver_to_remote_inbox_success_shared_inbox_marks_inbox_items_as_delivered(
factories, r_mock, now
):
activity = factories["federation.Activity"]()
url = "https://test.shared/"
r_mock.post(url)
ii = factories["federation.InboxItem"](
activity=activity, actor__shared_inbox_url=url
)
other_ii = factories["federation.InboxItem"](
activity=activity, actor__shared_inbox_url="https://other.url"
)
tasks.deliver_to_remote_inbox(activity_id=activity.pk, shared_inbox_url=url)
ii.refresh_from_db()
other_ii.refresh_from_db()
assert ii.is_delivered is True
assert ii.last_delivery_date == now
assert other_ii.is_delivered is False
assert other_ii.last_delivery_date is None
def test_deliver_to_remote_inbox_success_single_inbox_marks_inbox_items_as_delivered(
factories, r_mock, now
):
activity = factories["federation.Activity"]()
url = "https://test.single/"
r_mock.post(url)
ii = factories["federation.InboxItem"](activity=activity, actor__inbox_url=url)
other_ii = factories["federation.InboxItem"](
activity=activity, actor__inbox_url="https://other.url"
)
tasks.deliver_to_remote_inbox(activity_id=activity.pk, inbox_url=url)
ii.refresh_from_db()
other_ii.refresh_from_db()
assert ii.is_delivered is True
assert ii.last_delivery_date == now
assert other_ii.is_delivered is False
assert other_ii.last_delivery_date is None
def test_deliver_to_remote_inbox_error(factories, r_mock, now):
activity = factories["federation.Activity"]()
url = "https://test.single/"
r_mock.post(url, status_code=404)
ii = factories["federation.InboxItem"](activity=activity, actor__inbox_url=url)
with pytest.raises(tasks.RequestException):
tasks.deliver_to_remote_inbox(activity_id=activity.pk, inbox_url=url)
ii.refresh_from_db()
assert ii.is_delivered is False
assert ii.last_delivery_date == now
assert ii.delivery_attempts == 1

View file

@ -1,3 +1,4 @@
from rest_framework import serializers
import pytest
from funkwhale_api.federation import utils
@ -50,3 +51,41 @@ def test_extract_headers_from_meta():
"User-Agent": "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)",
}
assert cleaned_headers == expected
def test_retrieve(r_mock):
fid = "https://some.url"
m = r_mock.get(fid, json={"hello": "world"})
result = utils.retrieve(fid)
assert result == {"hello": "world"}
assert m.request_history[-1].headers["Accept"] == "application/activity+json"
def test_retrieve_with_actor(r_mock, factories):
actor = factories["federation.Actor"]()
fid = "https://some.url"
m = r_mock.get(fid, json={"hello": "world"})
result = utils.retrieve(fid, actor=actor)
assert result == {"hello": "world"}
assert m.request_history[-1].headers["Accept"] == "application/activity+json"
assert m.request_history[-1].headers["Signature"] is not None
def test_retrieve_with_queryset(factories):
actor = factories["federation.Actor"]()
assert utils.retrieve(actor.fid, queryset=actor.__class__)
def test_retrieve_with_serializer(r_mock):
class S(serializers.Serializer):
def create(self, validated_data):
return {"persisted": "object"}
fid = "https://some.url"
r_mock.get(fid, json={"hello": "world"})
result = utils.retrieve(fid, serializer_class=S)
assert result == {"persisted": "object"}

View file

@ -1,29 +1,8 @@
import pytest
from django.core.paginator import Paginator
from django.urls import reverse
from django.utils import timezone
from funkwhale_api.federation import (
activity,
actors,
models,
serializers,
utils,
views,
webfinger,
)
from funkwhale_api.music import tasks as music_tasks
@pytest.mark.parametrize(
"view,permissions",
[
(views.LibraryViewSet, ["federation"]),
(views.LibraryTrackViewSet, ["federation"]),
],
)
def test_permissions(assert_user_permission, view, permissions):
assert_user_permission(view, permissions)
from funkwhale_api.federation import actors, serializers, webfinger
@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
@ -39,25 +18,6 @@ def test_instance_actors(system_actor, db, api_client):
assert response.data == serializer.data
@pytest.mark.parametrize(
"route,kwargs",
[
("instance-actors-outbox", {"actor": "library"}),
("instance-actors-inbox", {"actor": "library"}),
("instance-actors-detail", {"actor": "library"}),
("well-known-webfinger", {}),
],
)
def test_instance_endpoints_405_if_federation_disabled(
authenticated_actor, db, preferences, api_client, route, kwargs
):
preferences["federation__enabled"] = False
url = reverse("federation:{}".format(route), kwargs=kwargs)
response = api_client.get(url)
assert response.status_code == 405
def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker):
clean = mocker.spy(webfinger, "clean_resource")
url = reverse("federation:well-known-webfinger")
@ -110,318 +70,6 @@ def test_wellknown_nodeinfo_disabled(db, preferences, api_client):
assert response.status_code == 404
def test_audio_file_list_requires_authenticated_actor(db, preferences, api_client):
preferences["federation__music_needs_approval"] = True
url = reverse("federation:music:files-list")
response = api_client.get(url)
assert response.status_code == 403
def test_audio_file_list_actor_no_page(db, preferences, api_client, factories):
preferences["federation__music_needs_approval"] = False
preferences["federation__collection_page_size"] = 2
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
tfs = factories["music.TrackFile"].create_batch(size=5)
conf = {
"id": utils.full_url(reverse("federation:music:files-list")),
"page_size": 2,
"items": list(reversed(tfs)), # we order by -creation_date
"item_serializer": serializers.AudioSerializer,
"actor": library,
}
expected = serializers.PaginatedCollectionSerializer(conf).data
url = reverse("federation:music:files-list")
response = api_client.get(url)
assert response.status_code == 200
assert response.data == expected
def test_audio_file_list_actor_page(db, preferences, api_client, factories):
preferences["federation__music_needs_approval"] = False
preferences["federation__collection_page_size"] = 2
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
tfs = factories["music.TrackFile"].create_batch(size=5)
conf = {
"id": utils.full_url(reverse("federation:music:files-list")),
"page": Paginator(list(reversed(tfs)), 2).page(2),
"item_serializer": serializers.AudioSerializer,
"actor": library,
}
expected = serializers.CollectionPageSerializer(conf).data
url = reverse("federation:music:files-list")
response = api_client.get(url, data={"page": 2})
assert response.status_code == 200
assert response.data == expected
def test_audio_file_list_actor_page_exclude_federated_files(
db, preferences, api_client, factories
):
preferences["federation__music_needs_approval"] = False
factories["music.TrackFile"].create_batch(size=5, federation=True)
url = reverse("federation:music:files-list")
response = api_client.get(url)
assert response.status_code == 200
assert response.data["totalItems"] == 0
def test_audio_file_list_actor_page_error(db, preferences, api_client, factories):
preferences["federation__music_needs_approval"] = False
url = reverse("federation:music:files-list")
response = api_client.get(url, data={"page": "nope"})
assert response.status_code == 400
def test_audio_file_list_actor_page_error_too_far(
db, preferences, api_client, factories
):
preferences["federation__music_needs_approval"] = False
url = reverse("federation:music:files-list")
response = api_client.get(url, data={"page": 5000})
assert response.status_code == 404
def test_library_actor_includes_library_link(db, preferences, api_client):
url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
response = api_client.get(url)
expected_links = [
{
"type": "Link",
"name": "library",
"mediaType": "application/activity+json",
"href": utils.full_url(reverse("federation:music:files-list")),
}
]
assert response.status_code == 200
assert response.data["url"] == expected_links
def test_can_fetch_library(superuser_api_client, mocker):
result = {"test": "test"}
scan = mocker.patch(
"funkwhale_api.federation.library.scan_from_account_name", return_value=result
)
url = reverse("api:v1:federation:libraries-fetch")
response = superuser_api_client.get(url, data={"account": "test@test.library"})
assert response.status_code == 200
assert response.data == result
scan.assert_called_once_with("test@test.library")
def test_follow_library(superuser_api_client, mocker, factories, r_mock):
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
actor = factories["federation.Actor"]()
follow = {"test": "follow"}
on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
actor_data = serializers.ActorSerializer(actor).data
actor_data["url"] = [
{"href": "https://test.library", "name": "library", "type": "Link"}
]
library_conf = {
"id": "https://test.library",
"items": range(10),
"actor": actor,
"page_size": 5,
}
library_data = serializers.PaginatedCollectionSerializer(library_conf).data
r_mock.get(actor.url, json=actor_data)
r_mock.get("https://test.library", json=library_data)
data = {
"actor": actor.url,
"autoimport": False,
"federation_enabled": True,
"download_files": False,
}
url = reverse("api:v1:federation:libraries-list")
response = superuser_api_client.post(url, data)
assert response.status_code == 201
follow = models.Follow.objects.get(actor=library_actor, target=actor, approved=None)
library = follow.library
assert response.data == serializers.APILibraryCreateSerializer(library).data
on_commit.assert_called_once_with(
activity.deliver,
serializers.FollowSerializer(follow).data,
on_behalf_of=library_actor,
to=[actor.url],
)
def test_can_list_system_actor_following(factories, superuser_api_client):
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
follow1 = factories["federation.Follow"](actor=library_actor)
factories["federation.Follow"]()
url = reverse("api:v1:federation:libraries-following")
response = superuser_api_client.get(url)
assert response.status_code == 200
assert response.data["results"] == [serializers.APIFollowSerializer(follow1).data]
def test_can_list_system_actor_followers(factories, superuser_api_client):
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
factories["federation.Follow"](actor=library_actor)
follow2 = factories["federation.Follow"](target=library_actor)
url = reverse("api:v1:federation:libraries-followers")
response = superuser_api_client.get(url)
assert response.status_code == 200
assert response.data["results"] == [serializers.APIFollowSerializer(follow2).data]
def test_can_list_libraries(factories, superuser_api_client):
library1 = factories["federation.Library"]()
library2 = factories["federation.Library"]()
url = reverse("api:v1:federation:libraries-list")
response = superuser_api_client.get(url)
assert response.status_code == 200
assert response.data["results"] == [
serializers.APILibrarySerializer(library1).data,
serializers.APILibrarySerializer(library2).data,
]
def test_can_detail_library(factories, superuser_api_client):
library = factories["federation.Library"]()
url = reverse(
"api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
)
response = superuser_api_client.get(url)
assert response.status_code == 200
assert response.data == serializers.APILibrarySerializer(library).data
def test_can_patch_library(factories, superuser_api_client):
library = factories["federation.Library"]()
data = {
"federation_enabled": not library.federation_enabled,
"download_files": not library.download_files,
"autoimport": not library.autoimport,
}
url = reverse(
"api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
)
response = superuser_api_client.patch(url, data)
assert response.status_code == 200
library.refresh_from_db()
for k, v in data.items():
assert getattr(library, k) == v
def test_scan_library(factories, mocker, superuser_api_client):
scan = mocker.patch(
"funkwhale_api.federation.tasks.scan_library.delay",
return_value=mocker.Mock(id="id"),
)
library = factories["federation.Library"]()
now = timezone.now()
data = {"until": now}
url = reverse(
"api:v1:federation:libraries-scan", kwargs={"uuid": str(library.uuid)}
)
response = superuser_api_client.post(url, data)
assert response.status_code == 200
assert response.data == {"task": "id"}
scan.assert_called_once_with(library_id=library.pk, until=now)
def test_list_library_tracks(factories, superuser_api_client):
library = factories["federation.Library"]()
lts = list(
reversed(
factories["federation.LibraryTrack"].create_batch(size=5, library=library)
)
)
factories["federation.LibraryTrack"].create_batch(size=5)
url = reverse("api:v1:federation:library-tracks-list")
response = superuser_api_client.get(url, {"library": library.uuid})
assert response.status_code == 200
assert response.data == {
"results": serializers.APILibraryTrackSerializer(lts, many=True).data,
"count": 5,
"previous": None,
"next": None,
}
def test_can_update_follow_status(factories, superuser_api_client, mocker):
patched_accept = mocker.patch("funkwhale_api.federation.activity.accept_follow")
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
follow = factories["federation.Follow"](target=library_actor)
payload = {"follow": follow.pk, "approved": True}
url = reverse("api:v1:federation:libraries-followers")
response = superuser_api_client.patch(url, payload)
follow.refresh_from_db()
assert response.status_code == 200
assert follow.approved is True
patched_accept.assert_called_once_with(follow)
def test_can_filter_pending_follows(factories, superuser_api_client):
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
factories["federation.Follow"](target=library_actor, approved=True)
params = {"pending": True}
url = reverse("api:v1:federation:libraries-followers")
response = superuser_api_client.get(url, params)
assert response.status_code == 200
assert len(response.data["results"]) == 0
def test_library_track_action_import(factories, superuser_api_client, mocker):
lt1 = factories["federation.LibraryTrack"]()
lt2 = factories["federation.LibraryTrack"](library=lt1.library)
lt3 = factories["federation.LibraryTrack"]()
factories["federation.LibraryTrack"](library=lt3.library)
mocked_run = mocker.patch("funkwhale_api.common.utils.on_commit")
payload = {
"objects": "all",
"action": "import",
"filters": {"library": lt1.library.uuid},
}
url = reverse("api:v1:federation:library-tracks-action")
response = superuser_api_client.post(url, payload, format="json")
batch = superuser_api_client.user.imports.latest("id")
expected = {"updated": 2, "action": "import", "result": {"batch": {"id": batch.pk}}}
imported_lts = [lt1, lt2]
assert response.status_code == 200
assert response.data == expected
assert batch.jobs.count() == 2
for i, job in enumerate(batch.jobs.all()):
assert job.library_track == imported_lts[i]
mocked_run.assert_called_once_with(
music_tasks.import_batch_run.delay, import_batch_id=batch.pk
)
def test_local_actor_detail(factories, api_client):
user = factories["users.User"](with_actor=True)
url = reverse(
@ -435,6 +83,34 @@ def test_local_actor_detail(factories, api_client):
assert response.data == serializer.data
def test_local_actor_inbox_post_requires_auth(factories, api_client):
user = factories["users.User"](with_actor=True)
url = reverse(
"federation:actors-inbox",
kwargs={"preferred_username": user.actor.preferred_username},
)
response = api_client.post(url, {"hello": "world"})
assert response.status_code == 403
def test_local_actor_inbox_post(factories, api_client, mocker, authenticated_actor):
patched_receive = mocker.patch("funkwhale_api.federation.activity.receive")
user = factories["users.User"](with_actor=True)
url = reverse(
"federation:actors-inbox",
kwargs={"preferred_username": user.actor.preferred_username},
)
response = api_client.post(url, {"hello": "world"}, format="json")
assert response.status_code == 200
patched_receive.assert_called_once_with(
activity={"hello": "world"},
on_behalf_of=authenticated_actor,
recipient=user.actor,
)
def test_wellknown_webfinger_local(factories, api_client, settings, mocker):
user = factories["users.User"](with_actor=True)
url = reverse("federation:well-known-webfinger")
@ -448,3 +124,60 @@ def test_wellknown_webfinger_local(factories, api_client, settings, mocker):
assert response.status_code == 200
assert response["Content-Type"] == "application/jrd+json"
assert response.data == serializer.data
@pytest.mark.parametrize("privacy_level", ["me", "instance", "everyone"])
def test_music_library_retrieve(factories, api_client, privacy_level):
library = factories["music.Library"](privacy_level=privacy_level)
expected = serializers.LibrarySerializer(library).data
url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid})
response = api_client.get(url)
assert response.status_code == 200
assert response.data == expected
def test_music_library_retrieve_page_public(factories, api_client):
library = factories["music.Library"](privacy_level="everyone")
tf = factories["music.TrackFile"](library=library)
id = library.get_federation_id()
expected = serializers.CollectionPageSerializer(
{
"id": id,
"item_serializer": serializers.AudioSerializer,
"actor": library.actor,
"page": Paginator([tf], 1).page(1),
"name": library.name,
"summary": library.description,
}
).data
url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid})
response = api_client.get(url, {"page": 1})
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("privacy_level", ["me", "instance"])
def test_music_library_retrieve_page_private(factories, api_client, privacy_level):
library = factories["music.Library"](privacy_level=privacy_level)
url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid})
response = api_client.get(url, {"page": 1})
assert response.status_code == 403
@pytest.mark.parametrize("approved,expected", [(True, 200), (False, 403)])
def test_music_library_retrieve_page_follow(
factories, api_client, authenticated_actor, approved, expected
):
library = factories["music.Library"](privacy_level="me")
factories["federation.LibraryFollow"](
actor=authenticated_actor, target=library, approved=approved
)
url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid})
response = api_client.get(url, {"page": 1})
assert response.status_code == expected