Audio federation
This commit is contained in:
parent
6992c567fb
commit
e49a460203
85 changed files with 2591 additions and 1197 deletions
|
|
@ -1,21 +1,31 @@
|
|||
|
||||
import pytest
|
||||
import uuid
|
||||
|
||||
from funkwhale_api.federation import activity, api_serializers, serializers, tasks
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.federation import (
|
||||
activity,
|
||||
models,
|
||||
api_serializers,
|
||||
serializers,
|
||||
tasks,
|
||||
)
|
||||
|
||||
|
||||
def test_receive_validates_basic_attributes_and_stores_activity(factories, now, mocker):
|
||||
mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||
local_actor = factories["users.User"]().create_actor()
|
||||
local_to_actor = factories["users.User"]().create_actor()
|
||||
local_cc_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],
|
||||
"to": [local_to_actor.fid, remote_actor.fid],
|
||||
"cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS],
|
||||
}
|
||||
|
||||
copy = activity.receive(activity=a, on_behalf_of=remote_actor)
|
||||
|
|
@ -29,8 +39,60 @@ def test_receive_validates_basic_attributes_and_stores_activity(factories, now,
|
|||
tasks.dispatch_inbox.delay, activity_id=copy.pk
|
||||
)
|
||||
|
||||
inbox_item = copy.inbox_items.get(actor__fid=local_actor.fid)
|
||||
assert inbox_item.is_delivered is False
|
||||
assert models.InboxItem.objects.count() == 2
|
||||
for actor, t in [(local_to_actor, "to"), (local_cc_actor, "cc")]:
|
||||
ii = models.InboxItem.objects.get(actor=actor)
|
||||
assert ii.type == t
|
||||
assert ii.activity == copy
|
||||
assert ii.is_read is False
|
||||
|
||||
|
||||
def test_get_actors_from_audience_urls(settings, db):
|
||||
settings.FEDERATION_HOSTNAME = "federation.hostname"
|
||||
library_uuid1 = uuid.uuid4()
|
||||
library_uuid2 = uuid.uuid4()
|
||||
|
||||
urls = [
|
||||
"https://wrong.url",
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "kevin"}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid1}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid2}),
|
||||
activity.PUBLIC_ADDRESS,
|
||||
]
|
||||
followed_query = Q(target__followers_url=urls[0])
|
||||
for url in urls[1:-1]:
|
||||
followed_query |= Q(target__followers_url=url)
|
||||
actor_follows = models.Follow.objects.filter(followed_query, approved=True)
|
||||
library_follows = models.LibraryFollow.objects.filter(followed_query, approved=True)
|
||||
expected = models.Actor.objects.filter(
|
||||
Q(fid__in=urls[0:-1])
|
||||
| Q(pk__in=actor_follows.values_list("actor", flat=True))
|
||||
| Q(pk__in=library_follows.values_list("actor", flat=True))
|
||||
)
|
||||
assert str(activity.get_actors_from_audience(urls).query) == str(expected.query)
|
||||
|
||||
|
||||
def test_get_inbox_urls(factories):
|
||||
a1 = factories["federation.Actor"](
|
||||
shared_inbox_url=None, inbox_url="https://a1.inbox"
|
||||
)
|
||||
a2 = factories["federation.Actor"](
|
||||
shared_inbox_url="https://shared.inbox", inbox_url="https://a2.inbox"
|
||||
)
|
||||
factories["federation.Actor"](
|
||||
shared_inbox_url="https://shared.inbox", inbox_url="https://a3.inbox"
|
||||
)
|
||||
|
||||
expected = sorted(set([a1.inbox_url, a2.shared_inbox_url]))
|
||||
|
||||
assert activity.get_inbox_urls(a1.__class__.objects.all()) == expected
|
||||
|
||||
|
||||
def test_receive_invalid_data(factories):
|
||||
|
|
@ -97,8 +159,6 @@ def test_inbox_routing_send_to_channel(factories, mocker):
|
|||
|
||||
ii.refresh_from_db()
|
||||
|
||||
assert ii.is_delivered is True
|
||||
|
||||
group_send.assert_called_once_with(
|
||||
"user.{}.inbox".format(ii.actor.user.pk),
|
||||
{
|
||||
|
|
@ -118,6 +178,16 @@ def test_inbox_routing_send_to_channel(factories, mocker):
|
|||
({"type": "Follow"}, {"type": "Follow"}, True),
|
||||
({"type": "Follow"}, {"type": "Noop"}, False),
|
||||
({"type": "Follow"}, {"type": "Follow", "id": "https://hello"}, True),
|
||||
(
|
||||
{"type": "Create", "object.type": "Audio"},
|
||||
{"type": "Create", "object": {"type": "Note"}},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{"type": "Create", "object.type": "Audio"},
|
||||
{"type": "Create", "object": {"type": "Audio"}},
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_route_matching(route, payload, expected):
|
||||
|
|
@ -126,7 +196,6 @@ def test_route_matching(route, payload, 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"]()
|
||||
|
|
@ -144,6 +213,9 @@ def test_outbox_router_dispatch(mocker, factories, now):
|
|||
"actor": actor,
|
||||
}
|
||||
|
||||
expected_deliveries_url = activity.get_inbox_urls(
|
||||
models.Actor.objects.filter(pk__in=[r1.pk, r2.pk])
|
||||
)
|
||||
router.connect({"type": "Noop"}, handler)
|
||||
activities = router.dispatch({"type": "Noop"}, {"summary": "hello"})
|
||||
a = activities[0]
|
||||
|
|
@ -163,9 +235,112 @@ def test_outbox_router_dispatch(mocker, factories, now):
|
|||
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
|
||||
assert a.deliveries.count() == 2
|
||||
for url in expected_deliveries_url:
|
||||
delivery = a.deliveries.get(inbox_url=url)
|
||||
assert delivery.is_delivered is False
|
||||
|
||||
|
||||
def test_prepare_deliveries_and_inbox_items(factories):
|
||||
local_actor1 = factories["federation.Actor"](
|
||||
local=True, shared_inbox_url="https://testlocal.inbox"
|
||||
)
|
||||
local_actor2 = factories["federation.Actor"](
|
||||
local=True, shared_inbox_url=local_actor1.shared_inbox_url
|
||||
)
|
||||
local_actor3 = factories["federation.Actor"](local=True, shared_inbox_url=None)
|
||||
|
||||
remote_actor1 = factories["federation.Actor"](
|
||||
shared_inbox_url="https://testremote.inbox"
|
||||
)
|
||||
remote_actor2 = factories["federation.Actor"](
|
||||
shared_inbox_url=remote_actor1.shared_inbox_url
|
||||
)
|
||||
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
|
||||
|
||||
library = factories["music.Library"]()
|
||||
library_follower_local = factories["federation.LibraryFollow"](
|
||||
target=library, actor__local=True, approved=True
|
||||
).actor
|
||||
library_follower_remote = factories["federation.LibraryFollow"](
|
||||
target=library, actor__local=False, approved=True
|
||||
).actor
|
||||
# follow not approved
|
||||
factories["federation.LibraryFollow"](
|
||||
target=library, actor__local=False, approved=False
|
||||
)
|
||||
|
||||
followed_actor = factories["federation.Actor"]()
|
||||
actor_follower_local = factories["federation.Follow"](
|
||||
target=followed_actor, actor__local=True, approved=True
|
||||
).actor
|
||||
actor_follower_remote = factories["federation.Follow"](
|
||||
target=followed_actor, actor__local=False, approved=True
|
||||
).actor
|
||||
# follow not approved
|
||||
factories["federation.Follow"](
|
||||
target=followed_actor, actor__local=False, approved=False
|
||||
)
|
||||
|
||||
recipients = [
|
||||
local_actor1,
|
||||
local_actor2,
|
||||
local_actor3,
|
||||
remote_actor1,
|
||||
remote_actor2,
|
||||
remote_actor3,
|
||||
activity.PUBLIC_ADDRESS,
|
||||
{"type": "followers", "target": library},
|
||||
{"type": "followers", "target": followed_actor},
|
||||
]
|
||||
|
||||
inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items(
|
||||
recipients, "to"
|
||||
)
|
||||
expected_inbox_items = sorted(
|
||||
[
|
||||
models.InboxItem(actor=local_actor1, type="to"),
|
||||
models.InboxItem(actor=local_actor2, type="to"),
|
||||
models.InboxItem(actor=local_actor3, type="to"),
|
||||
models.InboxItem(actor=library_follower_local, type="to"),
|
||||
models.InboxItem(actor=actor_follower_local, type="to"),
|
||||
],
|
||||
key=lambda v: v.actor.pk,
|
||||
)
|
||||
|
||||
expected_deliveries = sorted(
|
||||
[
|
||||
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
|
||||
models.Delivery(inbox_url=remote_actor3.inbox_url),
|
||||
models.Delivery(inbox_url=library_follower_remote.inbox_url),
|
||||
models.Delivery(inbox_url=actor_follower_remote.inbox_url),
|
||||
],
|
||||
key=lambda v: v.inbox_url,
|
||||
)
|
||||
|
||||
expected_urls = [
|
||||
local_actor1.fid,
|
||||
local_actor2.fid,
|
||||
local_actor3.fid,
|
||||
remote_actor1.fid,
|
||||
remote_actor2.fid,
|
||||
remote_actor3.fid,
|
||||
activity.PUBLIC_ADDRESS,
|
||||
library.followers_url,
|
||||
followed_actor.followers_url,
|
||||
]
|
||||
|
||||
assert urls == expected_urls
|
||||
assert len(expected_inbox_items) == len(inbox_items)
|
||||
assert len(expected_deliveries) == len(deliveries)
|
||||
|
||||
for delivery, expected_delivery in zip(
|
||||
sorted(deliveries, key=lambda v: v.inbox_url), expected_deliveries
|
||||
):
|
||||
assert delivery.inbox_url == expected_delivery.inbox_url
|
||||
|
||||
for inbox_item, expected_inbox_item in zip(
|
||||
sorted(inbox_items, key=lambda v: v.actor.pk), expected_inbox_items
|
||||
):
|
||||
assert inbox_item.actor == expected_inbox_item.actor
|
||||
assert inbox_item.type == "to"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from funkwhale_api.federation import serializers
|
|||
|
||||
|
||||
def test_library_serializer(factories):
|
||||
library = factories["music.Library"](files_count=5678)
|
||||
library = factories["music.Library"](uploads_count=5678)
|
||||
expected = {
|
||||
"fid": library.fid,
|
||||
"uuid": str(library.uuid),
|
||||
|
|
@ -11,7 +11,7 @@ def test_library_serializer(factories):
|
|||
"name": library.name,
|
||||
"description": library.description,
|
||||
"creation_date": library.creation_date.isoformat().split("+")[0] + "Z",
|
||||
"files_count": library.files_count,
|
||||
"uploads_count": library.uploads_count,
|
||||
"privacy_level": library.privacy_level,
|
||||
"follow": None,
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ def test_library_serializer(factories):
|
|||
|
||||
|
||||
def test_library_serializer_with_follow(factories):
|
||||
library = factories["music.Library"](files_count=5678)
|
||||
library = factories["music.Library"](uploads_count=5678)
|
||||
follow = factories["federation.LibraryFollow"](target=library)
|
||||
|
||||
setattr(library, "_follows", [follow])
|
||||
|
|
@ -33,7 +33,7 @@ def test_library_serializer_with_follow(factories):
|
|||
"name": library.name,
|
||||
"description": library.description,
|
||||
"creation_date": library.creation_date.isoformat().split("+")[0] + "Z",
|
||||
"files_count": library.files_count,
|
||||
"uploads_count": library.uploads_count,
|
||||
"privacy_level": library.privacy_level,
|
||||
"follow": api_serializers.NestedLibraryFollowSerializer(follow).data,
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ def test_library_serializer_validates_existing_follow(factories):
|
|||
assert "target" in serializer.errors
|
||||
|
||||
|
||||
def test_manage_track_file_action_read(factories):
|
||||
def test_manage_upload_action_read(factories):
|
||||
ii = factories["federation.InboxItem"]()
|
||||
s = api_serializers.InboxItemActionSerializer(queryset=None)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ def test_authenticate(factories, mocker, api_request):
|
|||
"type": "Person",
|
||||
"outbox": "https://test.com",
|
||||
"inbox": "https://test.com",
|
||||
"followers": "https://test.com",
|
||||
"preferredUsername": "test",
|
||||
"publicKey": {
|
||||
"publicKeyPem": public.decode("utf-8"),
|
||||
|
|
|
|||
|
|
@ -27,25 +27,25 @@ def test_follow_federation_url(factories):
|
|||
|
||||
def test_actor_get_quota(factories):
|
||||
library = factories["music.Library"]()
|
||||
factories["music.TrackFile"](
|
||||
factories["music.Upload"](
|
||||
library=library,
|
||||
import_status="pending",
|
||||
audio_file__from_path=None,
|
||||
audio_file__data=b"a",
|
||||
)
|
||||
factories["music.TrackFile"](
|
||||
factories["music.Upload"](
|
||||
library=library,
|
||||
import_status="skipped",
|
||||
audio_file__from_path=None,
|
||||
audio_file__data=b"aa",
|
||||
)
|
||||
factories["music.TrackFile"](
|
||||
factories["music.Upload"](
|
||||
library=library,
|
||||
import_status="errored",
|
||||
audio_file__from_path=None,
|
||||
audio_file__data=b"aaa",
|
||||
)
|
||||
factories["music.TrackFile"](
|
||||
factories["music.Upload"](
|
||||
library=library,
|
||||
import_status="finished",
|
||||
audio_file__from_path=None,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ from funkwhale_api.federation import routes, serializers
|
|||
[
|
||||
({"type": "Follow"}, routes.inbox_follow),
|
||||
({"type": "Accept"}, routes.inbox_accept),
|
||||
({"type": "Create", "object.type": "Audio"}, routes.inbox_create_audio),
|
||||
({"type": "Delete", "object.type": "Library"}, routes.inbox_delete_library),
|
||||
({"type": "Delete", "object.type": "Audio"}, routes.inbox_delete_audio),
|
||||
],
|
||||
)
|
||||
def test_inbox_routes(route, handler):
|
||||
|
|
@ -24,6 +27,9 @@ def test_inbox_routes(route, handler):
|
|||
[
|
||||
({"type": "Accept"}, routes.outbox_accept),
|
||||
({"type": "Follow"}, routes.outbox_follow),
|
||||
({"type": "Create", "object.type": "Audio"}, routes.outbox_create_audio),
|
||||
({"type": "Delete", "object.type": "Library"}, routes.outbox_delete_library),
|
||||
({"type": "Delete", "object.type": "Audio"}, routes.outbox_delete_audio),
|
||||
],
|
||||
)
|
||||
def test_outbox_routes(route, handler):
|
||||
|
|
@ -155,3 +161,153 @@ def test_outbox_follow_library(factories, mocker):
|
|||
assert activity["payload"] == expected
|
||||
assert activity["actor"] == follow.actor
|
||||
assert activity["object"] == follow.target
|
||||
|
||||
|
||||
def test_outbox_create_audio(factories, mocker):
|
||||
upload = factories["music.Upload"]()
|
||||
activity = list(routes.outbox_create_audio({"upload": upload}))[0]
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{
|
||||
"type": "Create",
|
||||
"object": serializers.UploadSerializer(upload).data,
|
||||
"actor": upload.library.actor.fid,
|
||||
}
|
||||
)
|
||||
expected = serializer.data
|
||||
expected["to"] = [{"type": "followers", "target": upload.library}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == upload.library.actor
|
||||
assert activity["target"] == upload.library
|
||||
assert activity["object"] == upload
|
||||
|
||||
|
||||
def test_inbox_create_audio(factories, mocker):
|
||||
activity = factories["federation.Activity"]()
|
||||
upload = factories["music.Upload"](bitrate=42, duration=55)
|
||||
payload = {
|
||||
"type": "Create",
|
||||
"actor": upload.library.actor.fid,
|
||||
"object": serializers.UploadSerializer(upload).data,
|
||||
}
|
||||
library = upload.library
|
||||
upload.delete()
|
||||
init = mocker.spy(serializers.UploadSerializer, "__init__")
|
||||
save = mocker.spy(serializers.UploadSerializer, "save")
|
||||
assert library.uploads.count() == 0
|
||||
result = routes.inbox_create_audio(
|
||||
payload,
|
||||
context={"actor": library.actor, "raise_exception": True, "activity": activity},
|
||||
)
|
||||
assert library.uploads.count() == 1
|
||||
assert result == {"object": library.uploads.latest("id"), "target": library}
|
||||
|
||||
assert init.call_count == 1
|
||||
args = init.call_args
|
||||
assert args[1]["data"] == payload["object"]
|
||||
assert args[1]["context"] == {"activity": activity, "actor": library.actor}
|
||||
assert save.call_count == 1
|
||||
|
||||
|
||||
def test_inbox_delete_library(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
|
||||
library = factories["music.Library"]()
|
||||
payload = {
|
||||
"type": "Delete",
|
||||
"actor": library.actor.fid,
|
||||
"object": {"type": "Library", "id": library.fid},
|
||||
}
|
||||
|
||||
routes.inbox_delete_library(
|
||||
payload,
|
||||
context={"actor": library.actor, "raise_exception": True, "activity": activity},
|
||||
)
|
||||
|
||||
with pytest.raises(library.__class__.DoesNotExist):
|
||||
library.refresh_from_db()
|
||||
|
||||
|
||||
def test_inbox_delete_library_impostor(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
impostor = factories["federation.Actor"]()
|
||||
library = factories["music.Library"]()
|
||||
payload = {
|
||||
"type": "Delete",
|
||||
"actor": library.actor.fid,
|
||||
"object": {"type": "Library", "id": library.fid},
|
||||
}
|
||||
|
||||
routes.inbox_delete_library(
|
||||
payload,
|
||||
context={"actor": impostor, "raise_exception": True, "activity": activity},
|
||||
)
|
||||
|
||||
# not deleted, should still be here
|
||||
library.refresh_from_db()
|
||||
|
||||
|
||||
def test_outbox_delete_library(factories):
|
||||
library = factories["music.Library"]()
|
||||
activity = list(routes.outbox_delete_library({"library": library}))[0]
|
||||
expected = serializers.ActivitySerializer(
|
||||
{"type": "Delete", "object": {"type": "Library", "id": library.fid}}
|
||||
).data
|
||||
|
||||
expected["to"] = [{"type": "followers", "target": library}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == library.actor
|
||||
|
||||
|
||||
def test_inbox_delete_audio(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
|
||||
upload = factories["music.Upload"]()
|
||||
library = upload.library
|
||||
payload = {
|
||||
"type": "Delete",
|
||||
"actor": library.actor.fid,
|
||||
"object": {"type": "Audio", "id": [upload.fid]},
|
||||
}
|
||||
|
||||
routes.inbox_delete_audio(
|
||||
payload,
|
||||
context={"actor": library.actor, "raise_exception": True, "activity": activity},
|
||||
)
|
||||
|
||||
with pytest.raises(upload.__class__.DoesNotExist):
|
||||
upload.refresh_from_db()
|
||||
|
||||
|
||||
def test_inbox_delete_audio_impostor(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
impostor = factories["federation.Actor"]()
|
||||
upload = factories["music.Upload"]()
|
||||
library = upload.library
|
||||
payload = {
|
||||
"type": "Delete",
|
||||
"actor": library.actor.fid,
|
||||
"object": {"type": "Audio", "id": [upload.fid]},
|
||||
}
|
||||
|
||||
routes.inbox_delete_audio(
|
||||
payload,
|
||||
context={"actor": impostor, "raise_exception": True, "activity": activity},
|
||||
)
|
||||
|
||||
# not deleted, should still be here
|
||||
upload.refresh_from_db()
|
||||
|
||||
|
||||
def test_outbox_delete_audio(factories):
|
||||
upload = factories["music.Upload"]()
|
||||
activity = list(routes.outbox_delete_audio({"uploads": [upload]}))[0]
|
||||
expected = serializers.ActivitySerializer(
|
||||
{"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}}
|
||||
).data
|
||||
|
||||
expected["to"] = [{"type": "followers", "target": upload.library}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == upload.library.actor
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import pytest
|
||||
from django.core.paginator import Paginator
|
||||
import uuid
|
||||
|
||||
from funkwhale_api.federation import activity, models, serializers, utils
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from funkwhale_api.federation import models, serializers, utils
|
||||
|
||||
|
||||
def test_actor_serializer_from_ap(db):
|
||||
|
|
@ -336,13 +339,13 @@ def test_undo_follow_serializer_validates_on_context(factories):
|
|||
|
||||
|
||||
def test_paginated_collection_serializer(factories):
|
||||
tfs = factories["music.TrackFile"].create_batch(size=5)
|
||||
uploads = factories["music.Upload"].create_batch(size=5)
|
||||
actor = factories["federation.Actor"](local=True)
|
||||
|
||||
conf = {
|
||||
"id": "https://test.federation/test",
|
||||
"items": tfs,
|
||||
"item_serializer": serializers.AudioSerializer,
|
||||
"items": uploads,
|
||||
"item_serializer": serializers.UploadSerializer,
|
||||
"actor": actor,
|
||||
"page_size": 2,
|
||||
}
|
||||
|
|
@ -355,7 +358,7 @@ def test_paginated_collection_serializer(factories):
|
|||
"type": "Collection",
|
||||
"id": conf["id"],
|
||||
"actor": actor.fid,
|
||||
"totalItems": len(tfs),
|
||||
"totalItems": len(uploads),
|
||||
"current": conf["id"] + "?page=1",
|
||||
"last": conf["id"] + "?page=3",
|
||||
"first": conf["id"] + "?page=1",
|
||||
|
|
@ -425,7 +428,7 @@ def test_collection_page_serializer_can_validate_child():
|
|||
}
|
||||
|
||||
serializer = serializers.CollectionPageSerializer(
|
||||
data=data, context={"item_serializer": serializers.AudioSerializer}
|
||||
data=data, context={"item_serializer": serializers.UploadSerializer}
|
||||
)
|
||||
|
||||
# child are validated but not included in data if not valid
|
||||
|
|
@ -434,14 +437,14 @@ def test_collection_page_serializer_can_validate_child():
|
|||
|
||||
|
||||
def test_collection_page_serializer(factories):
|
||||
tfs = factories["music.TrackFile"].create_batch(size=5)
|
||||
uploads = factories["music.Upload"].create_batch(size=5)
|
||||
actor = factories["federation.Actor"](local=True)
|
||||
|
||||
conf = {
|
||||
"id": "https://test.federation/test",
|
||||
"item_serializer": serializers.AudioSerializer,
|
||||
"item_serializer": serializers.UploadSerializer,
|
||||
"actor": actor,
|
||||
"page": Paginator(tfs, 2).page(2),
|
||||
"page": Paginator(uploads, 2).page(2),
|
||||
}
|
||||
expected = {
|
||||
"@context": [
|
||||
|
|
@ -452,7 +455,7 @@ def test_collection_page_serializer(factories):
|
|||
"type": "CollectionPage",
|
||||
"id": conf["id"] + "?page=2",
|
||||
"actor": actor.fid,
|
||||
"totalItems": len(tfs),
|
||||
"totalItems": len(uploads),
|
||||
"partOf": conf["id"],
|
||||
"prev": conf["id"] + "?page=1",
|
||||
"next": conf["id"] + "?page=3",
|
||||
|
|
@ -471,38 +474,12 @@ def test_collection_page_serializer(factories):
|
|||
assert serializer.data == expected
|
||||
|
||||
|
||||
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")
|
||||
factories["music.Upload"](import_status="pending")
|
||||
factories["music.Upload"](import_status="errored")
|
||||
factories["music.Upload"](import_status="finished")
|
||||
serializer = serializers.LibrarySerializer(library)
|
||||
expected = {
|
||||
"@context": [
|
||||
|
|
@ -520,6 +497,7 @@ def test_music_library_serializer_to_ap(factories):
|
|||
"current": library.fid + "?page=1",
|
||||
"last": library.fid + "?page=1",
|
||||
"first": library.fid + "?page=1",
|
||||
"followers": library.followers_url,
|
||||
}
|
||||
|
||||
assert serializer.data == expected
|
||||
|
|
@ -541,6 +519,7 @@ def test_music_library_serializer_from_public(factories, mocker):
|
|||
"summary": "World",
|
||||
"type": "Library",
|
||||
"id": "https://library.id",
|
||||
"followers": "https://library.id/followers",
|
||||
"actor": actor.fid,
|
||||
"totalItems": 12,
|
||||
"first": "https://library.id?page=1",
|
||||
|
|
@ -554,10 +533,12 @@ def test_music_library_serializer_from_public(factories, mocker):
|
|||
|
||||
assert library.actor == actor
|
||||
assert library.fid == data["id"]
|
||||
assert library.files_count == data["totalItems"]
|
||||
assert library.uploads_count == data["totalItems"]
|
||||
assert library.privacy_level == "everyone"
|
||||
assert library.name == "Hello"
|
||||
assert library.description == "World"
|
||||
assert library.followers_url == data["followers"]
|
||||
|
||||
retrieve.assert_called_once_with(
|
||||
actor.fid,
|
||||
queryset=actor.__class__,
|
||||
|
|
@ -581,6 +562,7 @@ def test_music_library_serializer_from_private(factories, mocker):
|
|||
"summary": "World",
|
||||
"type": "Library",
|
||||
"id": "https://library.id",
|
||||
"followers": "https://library.id/followers",
|
||||
"actor": actor.fid,
|
||||
"totalItems": 12,
|
||||
"first": "https://library.id?page=1",
|
||||
|
|
@ -594,10 +576,11 @@ def test_music_library_serializer_from_private(factories, mocker):
|
|||
|
||||
assert library.actor == actor
|
||||
assert library.fid == data["id"]
|
||||
assert library.files_count == data["totalItems"]
|
||||
assert library.uploads_count == data["totalItems"]
|
||||
assert library.privacy_level == "me"
|
||||
assert library.name == "Hello"
|
||||
assert library.description == "World"
|
||||
assert library.followers_url == data["followers"]
|
||||
retrieve.assert_called_once_with(
|
||||
actor.fid,
|
||||
queryset=actor.__class__,
|
||||
|
|
@ -605,75 +588,349 @@ def test_music_library_serializer_from_private(factories, mocker):
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,serializer_class",
|
||||
[
|
||||
("music.Artist", serializers.ArtistSerializer),
|
||||
("music.Album", serializers.AlbumSerializer),
|
||||
("music.Track", serializers.TrackSerializer),
|
||||
],
|
||||
)
|
||||
def test_music_entity_serializer_create_existing_mbid(
|
||||
model, serializer_class, factories
|
||||
):
|
||||
entity = factories[model]()
|
||||
data = {"musicbrainzId": str(entity.mbid), "id": "https://noop"}
|
||||
serializer = serializer_class()
|
||||
|
||||
assert serializer.create(data) == entity
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,serializer_class",
|
||||
[
|
||||
("music.Artist", serializers.ArtistSerializer),
|
||||
("music.Album", serializers.AlbumSerializer),
|
||||
("music.Track", serializers.TrackSerializer),
|
||||
],
|
||||
)
|
||||
def test_music_entity_serializer_create_existing_fid(
|
||||
model, serializer_class, factories
|
||||
):
|
||||
entity = factories[model](fid="https://entity.url")
|
||||
data = {"musicbrainzId": None, "id": "https://entity.url"}
|
||||
serializer = serializer_class()
|
||||
|
||||
assert serializer.create(data) == entity
|
||||
|
||||
|
||||
def test_activity_pub_artist_serializer_to_ap(factories):
|
||||
artist = factories["music.Artist"]()
|
||||
expected = {
|
||||
"@context": serializers.AP_CONTEXT,
|
||||
"type": "Artist",
|
||||
"id": artist.fid,
|
||||
"name": artist.name,
|
||||
"musicbrainzId": artist.mbid,
|
||||
"published": artist.creation_date.isoformat(),
|
||||
}
|
||||
serializer = serializers.ArtistSerializer(artist)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_activity_pub_artist_serializer_from_ap(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
|
||||
published = timezone.now()
|
||||
data = {
|
||||
"type": "Artist",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
}
|
||||
serializer = serializers.ArtistSerializer(data=data, context={"activity": activity})
|
||||
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
|
||||
artist = serializer.save()
|
||||
|
||||
assert artist.from_activity == activity
|
||||
assert artist.name == data["name"]
|
||||
assert artist.fid == data["id"]
|
||||
assert str(artist.mbid) == data["musicbrainzId"]
|
||||
assert artist.creation_date == published
|
||||
|
||||
|
||||
def test_activity_pub_album_serializer_to_ap(factories):
|
||||
album = factories["music.Album"]()
|
||||
|
||||
expected = {
|
||||
"@context": serializers.AP_CONTEXT,
|
||||
"type": "Album",
|
||||
"id": album.fid,
|
||||
"name": album.title,
|
||||
"cover": {"type": "Image", "url": utils.full_url(album.cover.url)},
|
||||
"musicbrainzId": album.mbid,
|
||||
"published": album.creation_date.isoformat(),
|
||||
"released": album.release_date.isoformat(),
|
||||
"artists": [
|
||||
serializers.ArtistSerializer(
|
||||
album.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
}
|
||||
serializer = serializers.AlbumSerializer(album)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_activity_pub_album_serializer_from_ap(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
|
||||
published = timezone.now()
|
||||
released = timezone.now().date()
|
||||
data = {
|
||||
"type": "Album",
|
||||
"id": "http://hello.album",
|
||||
"name": "Purple album",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
"released": released.isoformat(),
|
||||
"artists": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
}
|
||||
],
|
||||
}
|
||||
serializer = serializers.AlbumSerializer(data=data, context={"activity": activity})
|
||||
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
|
||||
album = serializer.save()
|
||||
artist = album.artist
|
||||
|
||||
assert album.from_activity == activity
|
||||
assert album.title == data["name"]
|
||||
assert album.fid == data["id"]
|
||||
assert str(album.mbid) == data["musicbrainzId"]
|
||||
assert album.creation_date == published
|
||||
assert album.release_date == released
|
||||
|
||||
assert artist.from_activity == activity
|
||||
assert artist.name == data["artists"][0]["name"]
|
||||
assert artist.fid == data["artists"][0]["id"]
|
||||
assert str(artist.mbid) == data["artists"][0]["musicbrainzId"]
|
||||
assert artist.creation_date == published
|
||||
|
||||
|
||||
def test_activity_pub_track_serializer_to_ap(factories):
|
||||
track = factories["music.Track"]()
|
||||
expected = {
|
||||
"@context": serializers.AP_CONTEXT,
|
||||
"published": track.creation_date.isoformat(),
|
||||
"type": "Track",
|
||||
"musicbrainzId": track.mbid,
|
||||
"id": track.fid,
|
||||
"name": track.title,
|
||||
"position": track.position,
|
||||
"artists": [
|
||||
serializers.ArtistSerializer(
|
||||
track.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"album": serializers.AlbumSerializer(
|
||||
track.album, context={"include_ap_context": False}
|
||||
).data,
|
||||
}
|
||||
serializer = serializers.TrackSerializer(track)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_activity_pub_track_serializer_from_ap(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
published = timezone.now()
|
||||
released = timezone.now().date()
|
||||
data = {
|
||||
"type": "Track",
|
||||
"id": "http://hello.track",
|
||||
"published": published.isoformat(),
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"name": "Black in back",
|
||||
"position": 5,
|
||||
"album": {
|
||||
"type": "Album",
|
||||
"id": "http://hello.album",
|
||||
"name": "Purple album",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
"released": released.isoformat(),
|
||||
"artists": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
}
|
||||
],
|
||||
},
|
||||
"artists": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
}
|
||||
],
|
||||
}
|
||||
serializer = serializers.TrackSerializer(data=data, context={"activity": activity})
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
|
||||
track = serializer.save()
|
||||
album = track.album
|
||||
artist = track.artist
|
||||
|
||||
assert track.from_activity == activity
|
||||
assert track.fid == data["id"]
|
||||
assert track.title == data["name"]
|
||||
assert track.position == data["position"]
|
||||
assert track.creation_date == published
|
||||
assert str(track.mbid) == data["musicbrainzId"]
|
||||
|
||||
assert album.from_activity == activity
|
||||
|
||||
assert album.title == data["album"]["name"]
|
||||
assert album.fid == data["album"]["id"]
|
||||
assert str(album.mbid) == data["album"]["musicbrainzId"]
|
||||
assert album.creation_date == published
|
||||
assert album.release_date == released
|
||||
|
||||
assert artist.from_activity == activity
|
||||
assert artist.name == data["artists"][0]["name"]
|
||||
assert artist.fid == data["artists"][0]["id"]
|
||||
assert str(artist.mbid) == data["artists"][0]["musicbrainzId"]
|
||||
assert artist.creation_date == published
|
||||
|
||||
|
||||
def test_activity_pub_upload_serializer_from_ap(factories, mocker):
|
||||
activity = factories["federation.Activity"]()
|
||||
library = factories["music.Library"]()
|
||||
|
||||
published = timezone.now()
|
||||
updated = timezone.now()
|
||||
released = timezone.now().date()
|
||||
data = {
|
||||
"@context": serializers.AP_CONTEXT,
|
||||
"type": "Audio",
|
||||
"id": "https://track.file",
|
||||
"name": "Ignored",
|
||||
"published": published.isoformat(),
|
||||
"updated": updated.isoformat(),
|
||||
"duration": 43,
|
||||
"bitrate": 42,
|
||||
"size": 66,
|
||||
"url": {"href": "https://audio.file", "type": "Link", "mediaType": "audio/mp3"},
|
||||
"library": library.fid,
|
||||
"track": {
|
||||
"type": "Track",
|
||||
"id": "http://hello.track",
|
||||
"published": published.isoformat(),
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"name": "Black in back",
|
||||
"position": 5,
|
||||
"album": {
|
||||
"type": "Album",
|
||||
"id": "http://hello.album",
|
||||
"name": "Purple album",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
"released": released.isoformat(),
|
||||
"artists": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
}
|
||||
],
|
||||
},
|
||||
"artists": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
serializer = serializers.UploadSerializer(data=data, context={"activity": activity})
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
track_create = mocker.spy(serializers.TrackSerializer, "create")
|
||||
upload = serializer.save()
|
||||
|
||||
assert upload.track.from_activity == activity
|
||||
assert upload.from_activity == activity
|
||||
assert track_create.call_count == 1
|
||||
assert upload.fid == data["id"]
|
||||
assert upload.track.fid == data["track"]["id"]
|
||||
assert upload.duration == data["duration"]
|
||||
assert upload.size == data["size"]
|
||||
assert upload.bitrate == data["bitrate"]
|
||||
assert upload.source == data["url"]["href"]
|
||||
assert upload.mimetype == data["url"]["mediaType"]
|
||||
assert upload.creation_date == published
|
||||
assert upload.import_status == "finished"
|
||||
assert upload.modification_date == updated
|
||||
|
||||
|
||||
def test_activity_pub_upload_serializer_validtes_library_actor(factories, mocker):
|
||||
library = factories["music.Library"]()
|
||||
usurpator = factories["federation.Actor"]()
|
||||
|
||||
serializer = serializers.UploadSerializer(data={}, context={"actor": usurpator})
|
||||
|
||||
with pytest.raises(serializers.serializers.ValidationError):
|
||||
serializer.validate_library(library.fid)
|
||||
|
||||
|
||||
def test_activity_pub_audio_serializer_to_ap(factories):
|
||||
tf = factories["music.TrackFile"](
|
||||
upload = factories["music.Upload"](
|
||||
mimetype="audio/mp3", bitrate=42, duration=43, size=44
|
||||
)
|
||||
expected = {
|
||||
"@context": serializers.AP_CONTEXT,
|
||||
"type": "Audio",
|
||||
"id": tf.get_federation_id(),
|
||||
"name": tf.track.full_name,
|
||||
"published": tf.creation_date.isoformat(),
|
||||
"updated": tf.modification_date.isoformat(),
|
||||
"metadata": {
|
||||
"artist": {
|
||||
"musicbrainz_id": tf.track.artist.mbid,
|
||||
"name": tf.track.artist.name,
|
||||
},
|
||||
"release": {
|
||||
"musicbrainz_id": tf.track.album.mbid,
|
||||
"title": tf.track.album.title,
|
||||
},
|
||||
"recording": {"musicbrainz_id": tf.track.mbid, "title": tf.track.title},
|
||||
"size": tf.size,
|
||||
"length": tf.duration,
|
||||
"bitrate": tf.bitrate,
|
||||
},
|
||||
"id": upload.fid,
|
||||
"name": upload.track.full_name,
|
||||
"published": upload.creation_date.isoformat(),
|
||||
"updated": upload.modification_date.isoformat(),
|
||||
"duration": upload.duration,
|
||||
"bitrate": upload.bitrate,
|
||||
"size": upload.size,
|
||||
"url": {
|
||||
"href": utils.full_url(tf.listen_url),
|
||||
"href": utils.full_url(upload.listen_url),
|
||||
"type": "Link",
|
||||
"mediaType": "audio/mp3",
|
||||
},
|
||||
"library": tf.library.get_federation_id(),
|
||||
"library": upload.library.fid,
|
||||
"track": serializers.TrackSerializer(
|
||||
upload.track, context={"include_ap_context": False}
|
||||
).data,
|
||||
}
|
||||
|
||||
serializer = serializers.AudioSerializer(tf)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_activity_pub_audio_serializer_to_ap_no_mbid(factories):
|
||||
tf = factories["music.TrackFile"](
|
||||
mimetype="audio/mp3",
|
||||
track__mbid=None,
|
||||
track__album__mbid=None,
|
||||
track__album__artist__mbid=None,
|
||||
)
|
||||
expected = {
|
||||
"@context": serializers.AP_CONTEXT,
|
||||
"type": "Audio",
|
||||
"id": tf.get_federation_id(),
|
||||
"name": tf.track.full_name,
|
||||
"published": tf.creation_date.isoformat(),
|
||||
"updated": tf.modification_date.isoformat(),
|
||||
"metadata": {
|
||||
"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": tf.size,
|
||||
"length": None,
|
||||
"bitrate": None,
|
||||
},
|
||||
"url": {
|
||||
"href": utils.full_url(tf.listen_url),
|
||||
"type": "Link",
|
||||
"mediaType": "audio/mp3",
|
||||
},
|
||||
"library": tf.library.fid,
|
||||
}
|
||||
|
||||
serializer = serializers.AudioSerializer(tf)
|
||||
serializer = serializers.UploadSerializer(upload)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
|
@ -731,7 +988,7 @@ def test_local_actor_serializer_to_ap(factories):
|
|||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_activity_serializer_clean_recipients_empty(db):
|
||||
def test_activity_serializer_validate_recipients_empty(db):
|
||||
s = serializers.BaseActivitySerializer()
|
||||
|
||||
with pytest.raises(serializers.serializers.ValidationError):
|
||||
|
|
@ -742,32 +999,3 @@ def test_activity_serializer_clean_recipients_empty(db):
|
|||
|
||||
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]})
|
||||
|
|
|
|||
|
|
@ -11,27 +11,27 @@ from funkwhale_api.federation import tasks
|
|||
def test_clean_federation_music_cache_if_no_listen(preferences, factories):
|
||||
preferences["federation__music_cache_duration"] = 60
|
||||
remote_library = factories["music.Library"]()
|
||||
tf1 = factories["music.TrackFile"](
|
||||
upload1 = factories["music.Upload"](
|
||||
library=remote_library, accessed_date=timezone.now()
|
||||
)
|
||||
tf2 = factories["music.TrackFile"](
|
||||
upload2 = factories["music.Upload"](
|
||||
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
|
||||
upload3 = factories["music.Upload"](library=remote_library, accessed_date=None)
|
||||
path1 = upload1.audio_file.path
|
||||
path2 = upload2.audio_file.path
|
||||
path3 = upload3.audio_file.path
|
||||
|
||||
tasks.clean_music_cache()
|
||||
|
||||
tf1.refresh_from_db()
|
||||
tf2.refresh_from_db()
|
||||
tf3.refresh_from_db()
|
||||
upload1.refresh_from_db()
|
||||
upload2.refresh_from_db()
|
||||
upload3.refresh_from_db()
|
||||
|
||||
assert bool(tf1.audio_file) is True
|
||||
assert bool(tf2.audio_file) is False
|
||||
assert bool(tf3.audio_file) is False
|
||||
assert bool(upload1.audio_file) is True
|
||||
assert bool(upload2.audio_file) is False
|
||||
assert bool(upload3.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
|
||||
|
|
@ -46,16 +46,16 @@ def test_clean_federation_music_cache_orphaned(settings, preferences, factories)
|
|||
os.makedirs(os.path.dirname(remove_path), exist_ok=True)
|
||||
pathlib.Path(keep_path).touch()
|
||||
pathlib.Path(remove_path).touch()
|
||||
tf = factories["music.TrackFile"](
|
||||
upload = factories["music.Upload"](
|
||||
accessed_date=timezone.now(), audio_file__path=keep_path
|
||||
)
|
||||
|
||||
tasks.clean_music_cache()
|
||||
|
||||
tf.refresh_from_db()
|
||||
upload.refresh_from_db()
|
||||
|
||||
assert bool(tf.audio_file) is True
|
||||
assert os.path.exists(tf.audio_file.path) is True
|
||||
assert bool(upload.audio_file) is True
|
||||
assert os.path.exists(upload.audio_file.path) is True
|
||||
assert os.path.exists(remove_path) is False
|
||||
|
||||
|
||||
|
|
@ -73,168 +73,47 @@ def test_handle_in(factories, mocker, now, queryset_equal_list):
|
|||
a.payload, context={"actor": a.actor, "activity": a, "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):
|
||||
def test_dispatch_outbox(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"
|
||||
mocked_deliver_to_remote = mocker.patch(
|
||||
"funkwhale_api.federation.tasks.deliver_to_remote.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",
|
||||
)
|
||||
factories["federation.InboxItem"](activity=activity)
|
||||
delivery = factories["federation.Delivery"](activity=activity)
|
||||
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
|
||||
)
|
||||
mocked_deliver_to_remote.assert_called_once_with(delivery_id=delivery.pk)
|
||||
|
||||
|
||||
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)
|
||||
def test_deliver_to_remote_success_mark_as_delivered(factories, r_mock, now):
|
||||
delivery = factories["federation.Delivery"]()
|
||||
r_mock.post(delivery.inbox_url)
|
||||
tasks.deliver_to_remote(delivery_id=delivery.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)
|
||||
delivery.refresh_from_db()
|
||||
|
||||
request = r_mock.request_history[0]
|
||||
|
||||
assert delivery.is_delivered is True
|
||||
assert delivery.attempts == 1
|
||||
assert delivery.last_attempt_date == now
|
||||
assert r_mock.called is True
|
||||
assert r_mock.call_count == 1
|
||||
assert request.url == url
|
||||
assert request.url == delivery.inbox_url
|
||||
assert request.headers["content-type"] == "application/activity+json"
|
||||
assert request.json() == activity.payload
|
||||
assert request.json() == delivery.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)
|
||||
def test_deliver_to_remote_error(factories, r_mock, now):
|
||||
delivery = factories["federation.Delivery"]()
|
||||
r_mock.post(delivery.inbox_url, status_code=404)
|
||||
|
||||
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)
|
||||
tasks.deliver_to_remote(delivery_id=delivery.pk)
|
||||
|
||||
ii.refresh_from_db()
|
||||
delivery.refresh_from_db()
|
||||
|
||||
assert ii.is_delivered is False
|
||||
assert ii.last_delivery_date == now
|
||||
assert ii.delivery_attempts == 1
|
||||
assert delivery.is_delivered is False
|
||||
assert delivery.attempts == 1
|
||||
assert delivery.last_attempt_date == now
|
||||
|
|
|
|||
|
|
@ -109,6 +109,17 @@ def test_local_actor_inbox_post(factories, api_client, mocker, authenticated_act
|
|||
)
|
||||
|
||||
|
||||
def test_shared_inbox_post(factories, api_client, mocker, authenticated_actor):
|
||||
patched_receive = mocker.patch("funkwhale_api.federation.activity.receive")
|
||||
url = reverse("federation:shared-inbox")
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def test_wellknown_webfinger_local(factories, api_client, settings, mocker):
|
||||
user = factories["users.User"](with_actor=True)
|
||||
url = reverse("federation:well-known-webfinger")
|
||||
|
|
@ -138,14 +149,14 @@ def test_music_library_retrieve(factories, api_client, privacy_level):
|
|||
|
||||
def test_music_library_retrieve_page_public(factories, api_client):
|
||||
library = factories["music.Library"](privacy_level="everyone")
|
||||
tf = factories["music.TrackFile"](library=library)
|
||||
upload = factories["music.Upload"](library=library)
|
||||
id = library.get_federation_id()
|
||||
expected = serializers.CollectionPageSerializer(
|
||||
{
|
||||
"id": id,
|
||||
"item_serializer": serializers.AudioSerializer,
|
||||
"item_serializer": serializers.UploadSerializer,
|
||||
"actor": library.actor,
|
||||
"page": Paginator([tf], 1).page(1),
|
||||
"page": Paginator([upload], 1).page(1),
|
||||
"name": library.name,
|
||||
"summary": library.description,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue