2020-02-05 14:06:07 +00:00
|
|
|
import uuid
|
2019-11-25 08:49:49 +00:00
|
|
|
import pytest
|
|
|
|
|
|
|
|
from django.urls import reverse
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
from funkwhale_api.audio import categories
|
2020-03-19 08:43:46 +00:00
|
|
|
from funkwhale_api.audio import renderers
|
2019-11-25 08:49:49 +00:00
|
|
|
from funkwhale_api.audio import serializers
|
2020-02-05 14:06:07 +00:00
|
|
|
from funkwhale_api.audio import views
|
2020-02-23 14:31:03 +00:00
|
|
|
from funkwhale_api.common import locales
|
|
|
|
from funkwhale_api.common import utils
|
2019-11-25 08:49:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_channel_create(logged_in_api_client):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
|
|
|
|
data = {
|
|
|
|
# TODO: cover
|
|
|
|
"name": "My channel",
|
|
|
|
"username": "mychannel",
|
2020-01-15 12:43:25 +00:00
|
|
|
"description": {"text": "This is my channel", "content_type": "text/markdown"},
|
2019-11-25 08:49:49 +00:00
|
|
|
"tags": ["hello", "world"],
|
2020-01-22 10:40:34 +00:00
|
|
|
"content_category": "podcast",
|
2020-01-30 16:28:52 +00:00
|
|
|
"metadata": {"language": "en", "itunes_category": "Sports"},
|
2019-11-25 08:49:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
url = reverse("api:v1:channels-list")
|
2020-01-15 12:43:25 +00:00
|
|
|
response = logged_in_api_client.post(url, data, format="json")
|
2019-11-25 08:49:49 +00:00
|
|
|
|
|
|
|
assert response.status_code == 201
|
|
|
|
|
2020-02-05 14:06:07 +00:00
|
|
|
channel = views.ChannelViewSet.queryset.get(attributed_to=actor)
|
|
|
|
expected = serializers.ChannelSerializer(
|
|
|
|
channel, context={"subscriptions_count": True}
|
|
|
|
).data
|
2019-11-25 08:49:49 +00:00
|
|
|
|
|
|
|
assert response.data == expected
|
|
|
|
assert channel.artist.name == data["name"]
|
|
|
|
assert channel.artist.attributed_to == actor
|
|
|
|
assert (
|
|
|
|
sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
|
|
|
|
== data["tags"]
|
|
|
|
)
|
|
|
|
assert channel.attributed_to == actor
|
2020-02-23 14:31:03 +00:00
|
|
|
assert channel.artist.description.text == data["description"]["text"]
|
|
|
|
assert (
|
|
|
|
channel.artist.description.content_type == data["description"]["content_type"]
|
|
|
|
)
|
2019-11-25 08:49:49 +00:00
|
|
|
assert channel.actor.preferred_username == data["username"]
|
2019-12-09 12:59:54 +00:00
|
|
|
assert channel.library.privacy_level == "everyone"
|
2019-11-25 08:49:49 +00:00
|
|
|
assert channel.library.actor == actor
|
|
|
|
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"field", ["uuid", "actor.preferred_username", "actor.full_username"],
|
|
|
|
)
|
|
|
|
def test_channel_detail(field, factories, logged_in_api_client):
|
2020-03-23 13:29:01 +00:00
|
|
|
channel = factories["audio.Channel"](
|
|
|
|
artist__description=None, local=True, artist__with_cover=True
|
|
|
|
)
|
2020-02-23 14:31:03 +00:00
|
|
|
|
|
|
|
url = reverse(
|
|
|
|
"api:v1:channels-detail",
|
|
|
|
kwargs={"composite": utils.recursive_getattr(channel, field)},
|
|
|
|
)
|
2020-02-05 14:06:07 +00:00
|
|
|
setattr(channel.artist, "_tracks_count", 0)
|
|
|
|
setattr(channel.artist, "_prefetched_tagged_items", [])
|
|
|
|
|
2020-01-20 08:58:04 +00:00
|
|
|
expected = serializers.ChannelSerializer(
|
|
|
|
channel, context={"subscriptions_count": True}
|
|
|
|
).data
|
2019-11-25 08:49:49 +00:00
|
|
|
response = logged_in_api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
assert response.data == expected
|
|
|
|
|
|
|
|
|
|
|
|
def test_channel_list(factories, logged_in_api_client):
|
2020-03-23 13:29:01 +00:00
|
|
|
channel = factories["audio.Channel"](
|
|
|
|
artist__description=None, artist__with_cover=True
|
|
|
|
)
|
2020-02-05 14:06:07 +00:00
|
|
|
setattr(channel.artist, "_tracks_count", 0)
|
|
|
|
setattr(channel.artist, "_prefetched_tagged_items", [])
|
2019-11-25 08:49:49 +00:00
|
|
|
url = reverse("api:v1:channels-list")
|
|
|
|
expected = serializers.ChannelSerializer(channel).data
|
|
|
|
response = logged_in_api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
assert response.data == {
|
|
|
|
"results": [expected],
|
|
|
|
"count": 1,
|
|
|
|
"next": None,
|
|
|
|
"previous": None,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-03-19 08:43:46 +00:00
|
|
|
def test_channel_list_opml(factories, logged_in_api_client, now):
|
|
|
|
channel1 = factories["audio.Channel"]()
|
|
|
|
channel2 = factories["audio.Channel"]()
|
|
|
|
expected_xml = serializers.get_opml(
|
|
|
|
channels=[channel2, channel1], title="Funkwhale channels OPML export", date=now
|
|
|
|
)
|
|
|
|
expected_content = renderers.render_xml(
|
|
|
|
renderers.dict_to_xml_tree("opml", expected_xml)
|
|
|
|
)
|
|
|
|
url = reverse("api:v1:channels-list")
|
|
|
|
response = logged_in_api_client.get(url, {"output": "opml"})
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
assert response.content == expected_content
|
|
|
|
assert response["content-type"] == "application/xml"
|
|
|
|
|
|
|
|
|
2019-11-25 08:49:49 +00:00
|
|
|
def test_channel_update(logged_in_api_client, factories):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"](attributed_to=actor)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
# TODO: cover
|
|
|
|
"name": "new name"
|
|
|
|
}
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
|
2019-11-25 08:49:49 +00:00
|
|
|
response = logged_in_api_client.patch(url, data)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
channel.refresh_from_db()
|
|
|
|
|
|
|
|
assert channel.artist.name == data["name"]
|
|
|
|
|
|
|
|
|
|
|
|
def test_channel_update_permission(logged_in_api_client, factories):
|
|
|
|
logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"]()
|
|
|
|
|
|
|
|
data = {"name": "new name"}
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
|
2019-11-25 08:49:49 +00:00
|
|
|
response = logged_in_api_client.patch(url, data)
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
2020-02-14 15:28:58 +00:00
|
|
|
def test_channel_delete(logged_in_api_client, factories, mocker):
|
|
|
|
|
2019-11-25 08:49:49 +00:00
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"](attributed_to=actor)
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
|
2020-02-14 15:28:58 +00:00
|
|
|
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
2019-11-25 08:49:49 +00:00
|
|
|
response = logged_in_api_client.delete(url)
|
|
|
|
|
|
|
|
assert response.status_code == 204
|
|
|
|
|
|
|
|
with pytest.raises(channel.DoesNotExist):
|
|
|
|
channel.refresh_from_db()
|
|
|
|
|
2020-02-14 15:28:58 +00:00
|
|
|
dispatch.assert_called_once_with(
|
|
|
|
{"type": "Delete", "object": {"type": channel.actor.type}},
|
|
|
|
context={"actor": channel.actor},
|
|
|
|
)
|
|
|
|
|
2019-11-25 08:49:49 +00:00
|
|
|
|
|
|
|
def test_channel_delete_permission(logged_in_api_client, factories):
|
|
|
|
logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"]()
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
|
2019-11-25 08:49:49 +00:00
|
|
|
response = logged_in_api_client.patch(url)
|
|
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
channel.refresh_from_db()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("url_name", ["api:v1:channels-list"])
|
|
|
|
def test_channel_views_disabled_via_feature_flag(
|
|
|
|
url_name, logged_in_api_client, preferences
|
|
|
|
):
|
|
|
|
preferences["audio__channels_enabled"] = False
|
|
|
|
url = reverse(url_name)
|
|
|
|
response = logged_in_api_client.get(url)
|
|
|
|
assert response.status_code == 405
|
2020-01-15 13:24:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_channel_subscribe(factories, logged_in_api_client):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"](artist__description=None)
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-subscribe", kwargs={"composite": channel.uuid})
|
2020-01-15 13:24:22 +00:00
|
|
|
|
|
|
|
response = logged_in_api_client.post(url)
|
|
|
|
|
|
|
|
assert response.status_code == 201
|
|
|
|
|
|
|
|
subscription = actor.emitted_follows.select_related(
|
2020-02-05 14:06:07 +00:00
|
|
|
"target__channel__artist__description",
|
|
|
|
"target__channel__artist__attachment_cover",
|
2020-01-15 13:24:22 +00:00
|
|
|
).latest("id")
|
2020-02-05 14:06:07 +00:00
|
|
|
setattr(subscription.target.channel.artist, "_tracks_count", 0)
|
|
|
|
setattr(subscription.target.channel.artist, "_prefetched_tagged_items", [])
|
2020-01-15 14:25:33 +00:00
|
|
|
assert subscription.fid == subscription.get_federation_id()
|
2020-01-15 13:24:22 +00:00
|
|
|
expected = serializers.SubscriptionSerializer(subscription).data
|
|
|
|
assert response.data == expected
|
|
|
|
assert subscription.target == channel.actor
|
|
|
|
|
|
|
|
|
|
|
|
def test_channel_unsubscribe(factories, logged_in_api_client):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"]()
|
|
|
|
subscription = factories["audio.Subscription"](target=channel.actor, actor=actor)
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-unsubscribe", kwargs={"composite": channel.uuid})
|
2020-01-15 13:24:22 +00:00
|
|
|
|
|
|
|
response = logged_in_api_client.post(url)
|
|
|
|
|
|
|
|
assert response.status_code == 204
|
|
|
|
|
|
|
|
with pytest.raises(subscription.DoesNotExist):
|
|
|
|
subscription.refresh_from_db()
|
2020-01-15 14:25:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_subscriptions_list(factories, logged_in_api_client):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
2020-03-23 13:29:01 +00:00
|
|
|
channel = factories["audio.Channel"](
|
|
|
|
artist__description=None, artist__with_cover=True
|
|
|
|
)
|
2020-01-15 14:25:33 +00:00
|
|
|
subscription = factories["audio.Subscription"](target=channel.actor, actor=actor)
|
2020-02-05 14:06:07 +00:00
|
|
|
setattr(subscription.target.channel.artist, "_tracks_count", 0)
|
|
|
|
setattr(subscription.target.channel.artist, "_prefetched_tagged_items", [])
|
2020-01-15 14:25:33 +00:00
|
|
|
factories["audio.Subscription"](target=channel.actor)
|
|
|
|
url = reverse("api:v1:subscriptions-list")
|
|
|
|
expected = serializers.SubscriptionSerializer(subscription).data
|
|
|
|
response = logged_in_api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
assert response.data["results"][0] == expected
|
|
|
|
assert response.data == {
|
|
|
|
"results": [expected],
|
|
|
|
"count": 1,
|
|
|
|
"next": None,
|
|
|
|
"previous": None,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def test_subscriptions_all(factories, logged_in_api_client):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
channel = factories["audio.Channel"](artist__description=None)
|
|
|
|
subscription = factories["audio.Subscription"](target=channel.actor, actor=actor)
|
|
|
|
factories["audio.Subscription"](target=channel.actor)
|
|
|
|
url = reverse("api:v1:subscriptions-all")
|
|
|
|
response = logged_in_api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
2020-02-05 14:06:07 +00:00
|
|
|
assert response.data == {
|
|
|
|
"results": [{"uuid": subscription.uuid, "channel": uuid.UUID(channel.uuid)}],
|
|
|
|
"count": 1,
|
|
|
|
}
|
2020-01-30 16:28:52 +00:00
|
|
|
|
|
|
|
|
2020-02-14 15:28:58 +00:00
|
|
|
def test_channel_rss_feed(factories, api_client, preferences):
|
|
|
|
preferences["common__api_authentication_required"] = False
|
|
|
|
channel = factories["audio.Channel"](local=True)
|
2020-01-30 16:28:52 +00:00
|
|
|
upload1 = factories["music.Upload"](library=channel.library, playable=True)
|
|
|
|
upload2 = factories["music.Upload"](library=channel.library, playable=True)
|
|
|
|
|
|
|
|
expected = serializers.rss_serialize_channel_full(
|
|
|
|
channel=channel, uploads=[upload2, upload1]
|
|
|
|
)
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
|
2020-01-30 16:28:52 +00:00
|
|
|
|
|
|
|
response = api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
assert response.data == expected
|
|
|
|
assert response["Content-Type"] == "application/rss+xml"
|
2020-02-14 15:28:58 +00:00
|
|
|
|
|
|
|
|
2020-03-13 11:16:51 +00:00
|
|
|
def test_channel_rss_feed_redirects_for_external(factories, api_client, preferences):
|
|
|
|
preferences["common__api_authentication_required"] = False
|
|
|
|
channel = factories["audio.Channel"](external=True)
|
|
|
|
factories["music.Upload"](library=channel.library, playable=True)
|
|
|
|
|
|
|
|
url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
|
|
|
|
|
|
|
|
response = api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 302
|
|
|
|
assert response["Location"] == channel.rss_url
|
|
|
|
|
|
|
|
|
2020-02-14 15:28:58 +00:00
|
|
|
def test_channel_rss_feed_remote(factories, api_client, preferences):
|
|
|
|
preferences["common__api_authentication_required"] = False
|
|
|
|
channel = factories["audio.Channel"]()
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
|
2020-02-14 15:28:58 +00:00
|
|
|
|
|
|
|
response = api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
def test_channel_rss_feed_authentication_required(factories, api_client, preferences):
|
|
|
|
preferences["common__api_authentication_required"] = True
|
|
|
|
channel = factories["audio.Channel"](local=True)
|
|
|
|
|
2020-02-23 14:31:03 +00:00
|
|
|
url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
|
2020-02-14 15:28:58 +00:00
|
|
|
|
|
|
|
response = api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 401
|
2020-02-23 14:31:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_channel_metadata_choices(factories, api_client):
|
|
|
|
|
|
|
|
expected = {
|
|
|
|
"language": [
|
|
|
|
{"value": code, "label": name} for code, name in locales.ISO_639_CHOICES
|
|
|
|
],
|
|
|
|
"itunes_category": [
|
|
|
|
{"value": code, "label": code, "children": children}
|
|
|
|
for code, children in categories.ITUNES_CATEGORIES.items()
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
url = reverse("api:v1:channels-metadata_choices")
|
|
|
|
|
|
|
|
response = api_client.get(url)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
assert response.data == expected
|
2020-03-13 11:16:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_subscribe_to_rss_feed_existing_channel(
|
|
|
|
factories, logged_in_api_client, mocker
|
|
|
|
):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
rss_url = "http://example.test/rss.url"
|
|
|
|
channel = factories["audio.Channel"](rss_url=rss_url, external=True)
|
|
|
|
url = reverse("api:v1:channels-rss_subscribe")
|
|
|
|
|
|
|
|
response = logged_in_api_client.post(url, {"url": rss_url})
|
|
|
|
|
|
|
|
assert response.status_code == 201
|
|
|
|
|
|
|
|
subscription = actor.emitted_follows.select_related(
|
|
|
|
"target__channel__artist__description",
|
|
|
|
"target__channel__artist__attachment_cover",
|
|
|
|
).latest("id")
|
|
|
|
|
|
|
|
assert subscription.target == channel.actor
|
|
|
|
assert subscription.approved is True
|
|
|
|
assert subscription.fid == subscription.get_federation_id()
|
|
|
|
|
|
|
|
setattr(subscription.target.channel.artist, "_tracks_count", 0)
|
|
|
|
setattr(subscription.target.channel.artist, "_prefetched_tagged_items", [])
|
|
|
|
|
|
|
|
expected = serializers.SubscriptionSerializer(subscription).data
|
|
|
|
|
|
|
|
assert response.data == expected
|
|
|
|
|
|
|
|
|
|
|
|
def test_subscribe_to_rss_feed_existing_subscription(
|
|
|
|
factories, logged_in_api_client, mocker
|
|
|
|
):
|
|
|
|
actor = logged_in_api_client.user.create_actor()
|
|
|
|
rss_url = "http://example.test/rss.url"
|
|
|
|
channel = factories["audio.Channel"](rss_url=rss_url, external=True)
|
|
|
|
factories["federation.Follow"](target=channel.actor, approved=True, actor=actor)
|
|
|
|
url = reverse("api:v1:channels-rss_subscribe")
|
|
|
|
|
|
|
|
response = logged_in_api_client.post(url, {"url": rss_url})
|
|
|
|
|
|
|
|
assert response.status_code == 201
|
|
|
|
|
|
|
|
assert channel.actor.received_follows.count() == 1
|
|
|
|
|
|
|
|
|
|
|
|
def test_subscribe_to_rss_creates_channel(factories, logged_in_api_client, mocker):
|
|
|
|
logged_in_api_client.user.create_actor()
|
|
|
|
rss_url = "http://example.test/rss.url"
|
|
|
|
channel = factories["audio.Channel"]()
|
|
|
|
get_channel_from_rss_url = mocker.patch.object(
|
|
|
|
serializers, "get_channel_from_rss_url", return_value=(channel, [])
|
|
|
|
)
|
|
|
|
url = reverse("api:v1:channels-rss_subscribe")
|
|
|
|
|
|
|
|
response = logged_in_api_client.post(url, {"url": rss_url})
|
|
|
|
|
|
|
|
assert response.status_code == 201
|
|
|
|
assert response.data["channel"]["uuid"] == channel.uuid
|
|
|
|
|
|
|
|
get_channel_from_rss_url.assert_called_once_with(rss_url)
|