See #170: updates to upload API to support channels publishing

This commit is contained in:
Eliot Berriot 2019-12-04 09:55:07 +01:00
parent e80eb3eb3e
commit c94d9214ec
13 changed files with 895 additions and 104 deletions

View file

@ -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

View file

@ -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)

View file

@ -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
)

View file

@ -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

View file

@ -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,
}