Resolve "Per-user libraries" (use !368 instead)
This commit is contained in:
parent
b0ca181016
commit
2ea21994ee
144 changed files with 6709 additions and 5307 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
53
api/tests/federation/test_api_serializers.py
Normal file
53
api/tests/federation/test_api_serializers.py
Normal 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
|
||||
51
api/tests/federation/test_api_views.py
Normal file
51
api/tests/federation/test_api_views.py
Normal 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})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
147
api/tests/federation/test_routes.py
Normal file
147
api/tests/federation/test_routes.py
Normal 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
|
||||
|
|
@ -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]})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue