mirror of
https://code.eliotberriot.com/funkwhale/funkwhale.git
synced 2025-10-06 05:59:55 +02:00
See #170: updates to upload API to support channels publishing
This commit is contained in:
parent
e80eb3eb3e
commit
c94d9214ec
13 changed files with 895 additions and 104 deletions
|
@ -54,6 +54,12 @@ def test_actor_get_quota(factories):
|
|||
audio_file__from_path=None,
|
||||
audio_file__data=b"aaaa",
|
||||
)
|
||||
factories["music.Upload"](
|
||||
library=library,
|
||||
import_status="draft",
|
||||
audio_file__from_path=None,
|
||||
audio_file__data=b"aaaaa",
|
||||
)
|
||||
|
||||
# this one is imported in place and don't count
|
||||
factories["music.Upload"](
|
||||
|
@ -72,7 +78,14 @@ def test_actor_get_quota(factories):
|
|||
audio_file__data=b"aaaa",
|
||||
)
|
||||
|
||||
expected = {"total": 14, "pending": 1, "skipped": 2, "errored": 3, "finished": 8}
|
||||
expected = {
|
||||
"total": 19,
|
||||
"pending": 1,
|
||||
"skipped": 2,
|
||||
"errored": 3,
|
||||
"finished": 8,
|
||||
"draft": 5,
|
||||
}
|
||||
|
||||
assert library.actor.get_current_usage() == expected
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
import uuid
|
||||
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
|
@ -297,6 +298,7 @@ def test_manage_upload_action_relaunch_import(factories, mocker):
|
|||
|
||||
# this one is finished and should stay as is
|
||||
finished = factories["music.Upload"](import_status="finished")
|
||||
draft = factories["music.Upload"](import_status="draft")
|
||||
|
||||
to_relaunch = [
|
||||
factories["music.Upload"](import_status="pending"),
|
||||
|
@ -314,6 +316,8 @@ def test_manage_upload_action_relaunch_import(factories, mocker):
|
|||
|
||||
finished.refresh_from_db()
|
||||
assert finished.import_status == "finished"
|
||||
draft.refresh_from_db()
|
||||
assert draft.import_status == "draft"
|
||||
assert m.call_count == 3
|
||||
|
||||
|
||||
|
@ -357,3 +361,143 @@ def test_update_library_privacy_level_broadcasts_to_followers(
|
|||
dispatch.assert_called_once_with(
|
||||
{"type": "Update", "object": {"type": "Library"}}, context={"library": library}
|
||||
)
|
||||
|
||||
|
||||
def test_upload_with_channel(factories, uploaded_audio_file):
|
||||
channel = factories["audio.Channel"](attributed_to__local=True)
|
||||
user = channel.attributed_to.user
|
||||
data = {
|
||||
"channel": channel.uuid,
|
||||
"audio_file": uploaded_audio_file,
|
||||
"import_status": "draft",
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
upload = serializer.save()
|
||||
|
||||
assert upload.library == channel.library
|
||||
|
||||
|
||||
def test_upload_with_not_owned_channel_fails(factories, uploaded_audio_file):
|
||||
channel = factories["audio.Channel"]()
|
||||
user = factories["users.User"]()
|
||||
data = {
|
||||
"channel": channel.uuid,
|
||||
"audio_file": uploaded_audio_file,
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
assert serializer.is_valid() is False
|
||||
assert "channel" in serializer.errors
|
||||
|
||||
|
||||
def test_upload_with_not_owned_library_fails(factories, uploaded_audio_file):
|
||||
library = factories["music.Library"]()
|
||||
user = factories["users.User"]()
|
||||
data = {
|
||||
"library": library.uuid,
|
||||
"audio_file": uploaded_audio_file,
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
assert serializer.is_valid() is False
|
||||
assert "library" in serializer.errors
|
||||
|
||||
|
||||
def test_upload_requires_library_or_channel(factories, uploaded_audio_file):
|
||||
user = factories["users.User"]()
|
||||
data = {
|
||||
"audio_file": uploaded_audio_file,
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
serializers.serializers.ValidationError,
|
||||
match=r"You need to specify a channel or a library",
|
||||
):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
def test_upload_requires_library_or_channel_but_not_both(
|
||||
factories, uploaded_audio_file
|
||||
):
|
||||
channel = factories["audio.Channel"](attributed_to__local=True)
|
||||
library = channel.library
|
||||
user = channel.attributed_to.user
|
||||
data = {
|
||||
"audio_file": uploaded_audio_file,
|
||||
"library": library.uuid,
|
||||
"channel": channel.uuid,
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
with pytest.raises(
|
||||
serializers.serializers.ValidationError,
|
||||
match=r"You may specify a channel or a library, not both",
|
||||
):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
def test_upload_import_metadata_serializer_simple():
|
||||
serializer = serializers.ImportMetadataSerializer(data={"title": "hello"})
|
||||
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
assert serializer.validated_data == {"title": "hello"}
|
||||
|
||||
|
||||
def test_upload_import_metadata_serializer_full():
|
||||
licenses.load(licenses.LICENSES)
|
||||
data = {
|
||||
"title": "hello",
|
||||
"mbid": "3220fd02-5237-4952-8394-b7e64b0204a6",
|
||||
"tags": ["politics", "gender"],
|
||||
"license": "cc-by-sa-4.0",
|
||||
"copyright": "My work",
|
||||
"position": 42,
|
||||
}
|
||||
expected = data.copy()
|
||||
expected["license"] = models.License.objects.get(code=data["license"])
|
||||
expected["mbid"] = uuid.UUID(data["mbid"])
|
||||
serializer = serializers.ImportMetadataSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
assert serializer.validated_data == expected
|
||||
|
||||
|
||||
def test_upload_with_channel_keeps_import_metadata(factories, uploaded_audio_file):
|
||||
channel = factories["audio.Channel"](attributed_to__local=True)
|
||||
user = channel.attributed_to.user
|
||||
data = {
|
||||
"channel": channel.uuid,
|
||||
"audio_file": uploaded_audio_file,
|
||||
"import_metadata": {"title": "hello"},
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
upload = serializer.save()
|
||||
|
||||
assert upload.import_metadata == data["import_metadata"]
|
||||
|
||||
|
||||
def test_upload_with_channel_validates_import_metadata(factories, uploaded_audio_file):
|
||||
channel = factories["audio.Channel"](attributed_to__local=True)
|
||||
user = channel.attributed_to.user
|
||||
data = {
|
||||
"channel": channel.uuid,
|
||||
"audio_file": uploaded_audio_file,
|
||||
"import_metadata": {"title": None},
|
||||
}
|
||||
serializer = serializers.UploadForOwnerSerializer(
|
||||
data=data, context={"user": user},
|
||||
)
|
||||
with pytest.raises(serializers.serializers.ValidationError):
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
|
|
|
@ -432,6 +432,14 @@ def test_upload_import_skip_existing_track_in_own_library(factories, temp_signal
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("import_status", ["draft", "errored", "finished"])
|
||||
def test_process_upload_picks_ignore_non_pending_uploads(import_status, factories):
|
||||
upload = factories["music.Upload"](import_status=import_status)
|
||||
|
||||
with pytest.raises(upload.DoesNotExist):
|
||||
tasks.process_upload(upload_id=upload.pk)
|
||||
|
||||
|
||||
def test_upload_import_track_uuid(now, factories):
|
||||
track = factories["music.Track"]()
|
||||
upload = factories["music.Upload"](
|
||||
|
@ -911,3 +919,133 @@ def test_get_cover_from_fs_ignored(name, tmpdir):
|
|||
f.write(content)
|
||||
|
||||
assert tasks.get_cover_from_fs(tmpdir) is None
|
||||
|
||||
|
||||
def test_get_track_from_import_metadata_with_forced_values(factories, mocker, faker):
|
||||
actor = factories["federation.Actor"]()
|
||||
forced_values = {
|
||||
"title": "Real title",
|
||||
"artist": factories["music.Artist"](),
|
||||
"album": None,
|
||||
"license": factories["music.License"](),
|
||||
"position": 3,
|
||||
"copyright": "Real copyright",
|
||||
"mbid": faker.uuid4(),
|
||||
"attributed_to": actor,
|
||||
"tags": ["hello", "world"],
|
||||
}
|
||||
metadata = {
|
||||
"title": "Test track",
|
||||
"artists": [{"name": "Test artist"}],
|
||||
"album": {"title": "Test album", "release_date": datetime.date(2012, 8, 15)},
|
||||
"position": 4,
|
||||
"disc_number": 2,
|
||||
"copyright": "2018 Someone",
|
||||
"tags": ["foo", "bar"],
|
||||
}
|
||||
|
||||
track = tasks.get_track_from_import_metadata(metadata, **forced_values)
|
||||
|
||||
assert track.title == forced_values["title"]
|
||||
assert track.mbid == forced_values["mbid"]
|
||||
assert track.position == forced_values["position"]
|
||||
assert track.disc_number == metadata["disc_number"]
|
||||
assert track.copyright == forced_values["copyright"]
|
||||
assert track.album == forced_values["album"]
|
||||
assert track.artist == forced_values["artist"]
|
||||
assert track.attributed_to == forced_values["attributed_to"]
|
||||
assert track.license == forced_values["license"]
|
||||
assert (
|
||||
sorted(track.tagged_items.values_list("tag__name", flat=True))
|
||||
== forced_values["tags"]
|
||||
)
|
||||
|
||||
|
||||
def test_process_channel_upload_forces_artist_and_attributed_to(
|
||||
factories, mocker, faker
|
||||
):
|
||||
track = factories["music.Track"]()
|
||||
channel = factories["audio.Channel"]()
|
||||
import_metadata = {
|
||||
"title": "Real title",
|
||||
"position": 3,
|
||||
"copyright": "Real copyright",
|
||||
"tags": ["hello", "world"],
|
||||
}
|
||||
|
||||
expected_forced_values = import_metadata.copy()
|
||||
expected_forced_values["artist"] = channel.artist
|
||||
expected_forced_values["attributed_to"] = channel.attributed_to
|
||||
upload = factories["music.Upload"](
|
||||
track=None, import_metadata=import_metadata, library=channel.library
|
||||
)
|
||||
get_track_from_import_metadata = mocker.patch.object(
|
||||
tasks, "get_track_from_import_metadata", return_value=track
|
||||
)
|
||||
|
||||
tasks.process_upload(upload_id=upload.pk)
|
||||
|
||||
upload.refresh_from_db()
|
||||
serializer = tasks.metadata.TrackMetadataSerializer(
|
||||
data=tasks.metadata.Metadata(upload.get_audio_file())
|
||||
)
|
||||
assert serializer.is_valid() is True
|
||||
audio_metadata = serializer.validated_data
|
||||
|
||||
expected_final_metadata = tasks.collections.ChainMap(
|
||||
{"upload_source": None}, audio_metadata, {"funkwhale": {}},
|
||||
)
|
||||
assert upload.import_status == "finished"
|
||||
get_track_from_import_metadata.assert_called_once_with(
|
||||
expected_final_metadata, **expected_forced_values
|
||||
)
|
||||
|
||||
|
||||
def test_process_upload_uses_import_metadata_if_valid(factories, mocker):
|
||||
track = factories["music.Track"]()
|
||||
import_metadata = {"title": "hello", "funkwhale": {"foo": "bar"}}
|
||||
upload = factories["music.Upload"](track=None, import_metadata=import_metadata)
|
||||
get_track_from_import_metadata = mocker.patch.object(
|
||||
tasks, "get_track_from_import_metadata", return_value=track
|
||||
)
|
||||
tasks.process_upload(upload_id=upload.pk)
|
||||
|
||||
serializer = tasks.metadata.TrackMetadataSerializer(
|
||||
data=tasks.metadata.Metadata(upload.get_audio_file())
|
||||
)
|
||||
assert serializer.is_valid() is True
|
||||
audio_metadata = serializer.validated_data
|
||||
|
||||
expected_final_metadata = tasks.collections.ChainMap(
|
||||
{"upload_source": None},
|
||||
audio_metadata,
|
||||
{"funkwhale": import_metadata["funkwhale"]},
|
||||
)
|
||||
get_track_from_import_metadata.assert_called_once_with(
|
||||
expected_final_metadata, attributed_to=upload.library.actor, title="hello"
|
||||
)
|
||||
|
||||
|
||||
def test_process_upload_skips_import_metadata_if_invalid(factories, mocker):
|
||||
track = factories["music.Track"]()
|
||||
import_metadata = {"title": None, "funkwhale": {"foo": "bar"}}
|
||||
upload = factories["music.Upload"](track=None, import_metadata=import_metadata)
|
||||
get_track_from_import_metadata = mocker.patch.object(
|
||||
tasks, "get_track_from_import_metadata", return_value=track
|
||||
)
|
||||
tasks.process_upload(upload_id=upload.pk)
|
||||
|
||||
serializer = tasks.metadata.TrackMetadataSerializer(
|
||||
data=tasks.metadata.Metadata(upload.get_audio_file())
|
||||
)
|
||||
assert serializer.is_valid() is True
|
||||
audio_metadata = serializer.validated_data
|
||||
|
||||
expected_final_metadata = tasks.collections.ChainMap(
|
||||
{"upload_source": None},
|
||||
audio_metadata,
|
||||
{"funkwhale": import_metadata["funkwhale"]},
|
||||
)
|
||||
get_track_from_import_metadata.assert_called_once_with(
|
||||
expected_final_metadata, attributed_to=upload.library.actor
|
||||
)
|
||||
|
|
|
@ -681,10 +681,105 @@ def test_user_can_create_upload(logged_in_api_client, factories, mocker, audio_f
|
|||
assert upload.audio_file.read() == audio_file.read()
|
||||
assert upload.source == "upload://test"
|
||||
assert upload.import_reference == "test"
|
||||
assert upload.import_status == "pending"
|
||||
assert upload.track is None
|
||||
m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk)
|
||||
|
||||
|
||||
def test_user_can_create_draft_upload(
|
||||
logged_in_api_client, factories, mocker, audio_file
|
||||
):
|
||||
library = factories["music.Library"](actor__user=logged_in_api_client.user)
|
||||
url = reverse("api:v1:uploads-list")
|
||||
m = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||
|
||||
response = logged_in_api_client.post(
|
||||
url,
|
||||
{
|
||||
"audio_file": audio_file,
|
||||
"source": "upload://test",
|
||||
"import_reference": "test",
|
||||
"import_status": "draft",
|
||||
"library": library.uuid,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
upload = library.uploads.latest("id")
|
||||
|
||||
audio_file.seek(0)
|
||||
assert upload.audio_file.read() == audio_file.read()
|
||||
assert upload.source == "upload://test"
|
||||
assert upload.import_reference == "test"
|
||||
assert upload.import_status == "draft"
|
||||
assert upload.track is None
|
||||
m.assert_not_called()
|
||||
|
||||
|
||||
def test_user_can_patch_draft_upload(
|
||||
logged_in_api_client, factories, mocker, audio_file
|
||||
):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
library = factories["music.Library"](actor=actor)
|
||||
upload = factories["music.Upload"](library__actor=actor, import_status="draft")
|
||||
url = reverse("api:v1:uploads-detail", kwargs={"uuid": upload.uuid})
|
||||
m = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||
|
||||
response = logged_in_api_client.patch(
|
||||
url,
|
||||
{
|
||||
"audio_file": audio_file,
|
||||
"source": "upload://test",
|
||||
"import_reference": "test",
|
||||
"library": library.uuid,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
upload.refresh_from_db()
|
||||
|
||||
audio_file.seek(0)
|
||||
assert upload.audio_file.read() == audio_file.read()
|
||||
assert upload.source == "upload://test"
|
||||
assert upload.import_reference == "test"
|
||||
assert upload.import_status == "draft"
|
||||
assert upload.library == library
|
||||
m.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("import_status", ["pending", "errored", "skipped", "finished"])
|
||||
def test_user_cannot_patch_non_draft_upload(
|
||||
import_status, logged_in_api_client, factories
|
||||
):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
upload = factories["music.Upload"](
|
||||
library__actor=actor, import_status=import_status
|
||||
)
|
||||
url = reverse("api:v1:uploads-detail", kwargs={"uuid": upload.uuid})
|
||||
response = logged_in_api_client.patch(url, {"import_reference": "test"})
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_user_can_patch_draft_upload_status_triggers_processing(
|
||||
logged_in_api_client, factories, mocker
|
||||
):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
upload = factories["music.Upload"](library__actor=actor, import_status="draft")
|
||||
url = reverse("api:v1:uploads-detail", kwargs={"uuid": upload.uuid})
|
||||
m = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||
|
||||
response = logged_in_api_client.patch(url, {"import_status": "pending"})
|
||||
|
||||
upload.refresh_from_db()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert upload.import_status == "pending"
|
||||
m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk)
|
||||
|
||||
|
||||
def test_user_can_list_own_library_follows(factories, logged_in_api_client):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
library = factories["music.Library"](actor=actor)
|
||||
|
@ -1062,3 +1157,17 @@ def test_track_list_exclude_channels(params, expected, factories, logged_in_api_
|
|||
def test_strip_absolute_media_url(media_url, input, expected, settings):
|
||||
settings.MEDIA_URL = media_url
|
||||
assert views.strip_absolute_media_url(input) == expected
|
||||
|
||||
|
||||
def test_get_upload_audio_metadata(logged_in_api_client, factories):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
upload = factories["music.Upload"](library__actor=actor)
|
||||
metadata = tasks.metadata.Metadata(upload.get_audio_file())
|
||||
serializer = tasks.metadata.TrackMetadataSerializer(data=metadata)
|
||||
url = reverse("api:v1:uploads-audio-file-metadata", kwargs={"uuid": upload.uuid})
|
||||
|
||||
response = logged_in_api_client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
assert response.data == serializer.validated_data
|
||||
|
|
|
@ -204,21 +204,23 @@ def test_user_get_quota_status(factories, preferences, mocker):
|
|||
mocker.patch(
|
||||
"funkwhale_api.federation.models.Actor.get_current_usage",
|
||||
return_value={
|
||||
"total": 10 * 1000 * 1000,
|
||||
"total": 15 * 1000 * 1000,
|
||||
"pending": 1 * 1000 * 1000,
|
||||
"skipped": 2 * 1000 * 1000,
|
||||
"errored": 3 * 1000 * 1000,
|
||||
"finished": 4 * 1000 * 1000,
|
||||
"draft": 5 * 1000 * 1000,
|
||||
},
|
||||
)
|
||||
assert user.get_quota_status() == {
|
||||
"max": 66,
|
||||
"remaining": 56,
|
||||
"current": 10,
|
||||
"remaining": 51,
|
||||
"current": 15,
|
||||
"pending": 1,
|
||||
"skipped": 2,
|
||||
"errored": 3,
|
||||
"finished": 4,
|
||||
"draft": 5,
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue