449 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			449 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
| import pytest
 | |
| from django.urls import reverse
 | |
| 
 | |
| from funkwhale_api.audio import categories, renderers, serializers, views
 | |
| from funkwhale_api.common import locales, utils
 | |
| 
 | |
| 
 | |
| def test_channel_create(logged_in_api_client):
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
| 
 | |
|     data = {
 | |
|         # TODO: cover
 | |
|         "name": "My channel",
 | |
|         "username": "mychannel",
 | |
|         "description": {"text": "This is my channel", "content_type": "text/markdown"},
 | |
|         "tags": ["hello", "world"],
 | |
|         "content_category": "podcast",
 | |
|         "metadata": {"language": "en", "itunes_category": "Sports"},
 | |
|     }
 | |
| 
 | |
|     url = reverse("api:v1:channels-list")
 | |
|     response = logged_in_api_client.post(url, data, format="json")
 | |
| 
 | |
|     assert response.status_code == 201
 | |
| 
 | |
|     channel = views.ChannelViewSet.queryset.get(attributed_to=actor)
 | |
|     expected = serializers.ChannelSerializer(
 | |
|         channel, context={"subscriptions_count": True}
 | |
|     ).data
 | |
| 
 | |
|     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
 | |
|     assert channel.artist.description.text == data["description"]["text"]
 | |
|     assert (
 | |
|         channel.artist.description.content_type == data["description"]["content_type"]
 | |
|     )
 | |
|     assert channel.actor.preferred_username == data["username"]
 | |
|     assert channel.library.privacy_level == "everyone"
 | |
|     assert channel.library.actor == actor
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize(
 | |
|     "field",
 | |
|     ["uuid", "actor.preferred_username", "actor.full_username"],
 | |
| )
 | |
| def test_channel_detail(field, factories, logged_in_api_client):
 | |
|     channel = factories["audio.Channel"](
 | |
|         artist__description=None, local=True, artist__with_cover=True
 | |
|     )
 | |
| 
 | |
|     url = reverse(
 | |
|         "api:v1:channels-detail",
 | |
|         kwargs={"composite": utils.recursive_getattr(channel, field)},
 | |
|     )
 | |
|     setattr(channel.artist, "_tracks_count", 0)
 | |
|     setattr(channel.artist, "_prefetched_tagged_items", [])
 | |
| 
 | |
|     expected = serializers.ChannelSerializer(
 | |
|         channel, context={"subscriptions_count": True}
 | |
|     ).data
 | |
|     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):
 | |
|     channel = factories["audio.Channel"](
 | |
|         artist__description=None, artist__with_cover=True
 | |
|     )
 | |
|     setattr(channel.artist, "_tracks_count", 0)
 | |
|     setattr(channel.artist, "_prefetched_tagged_items", [])
 | |
|     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,
 | |
|     }
 | |
| 
 | |
| 
 | |
| 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"
 | |
| 
 | |
| 
 | |
| 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"
 | |
|     }
 | |
| 
 | |
|     url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
 | |
|     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"}
 | |
| 
 | |
|     url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
 | |
|     response = logged_in_api_client.patch(url, data)
 | |
| 
 | |
|     assert response.status_code == 403
 | |
| 
 | |
| 
 | |
| def test_channel_delete(logged_in_api_client, factories, mocker):
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
|     channel = factories["audio.Channel"](attributed_to=actor)
 | |
| 
 | |
|     url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
 | |
|     on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
 | |
|     response = logged_in_api_client.delete(url)
 | |
| 
 | |
|     assert response.status_code == 204
 | |
| 
 | |
|     on_commit.assert_called_once_with(
 | |
|         views.federation_tasks.remove_actor.delay, actor_id=channel.actor.pk
 | |
|     )
 | |
|     with pytest.raises(channel.DoesNotExist):
 | |
|         channel.refresh_from_db()
 | |
| 
 | |
| 
 | |
| def test_channel_delete_permission(logged_in_api_client, factories):
 | |
|     logged_in_api_client.user.create_actor()
 | |
|     channel = factories["audio.Channel"]()
 | |
| 
 | |
|     url = reverse("api:v1:channels-detail", kwargs={"composite": channel.uuid})
 | |
|     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
 | |
| 
 | |
| 
 | |
| def test_channel_subscribe(factories, logged_in_api_client):
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
|     channel = factories["audio.Channel"](artist__description=None)
 | |
|     url = reverse("api:v1:channels-subscribe", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     response = logged_in_api_client.post(url)
 | |
| 
 | |
|     assert response.status_code == 201
 | |
| 
 | |
|     subscription = actor.emitted_follows.select_related(
 | |
|         "target__channel__artist__description",
 | |
|         "target__channel__artist__attachment_cover",
 | |
|     ).latest("id")
 | |
|     setattr(subscription.target.channel.artist, "_tracks_count", 0)
 | |
|     setattr(subscription.target.channel.artist, "_prefetched_tagged_items", [])
 | |
|     assert subscription.fid == subscription.get_federation_id()
 | |
|     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)
 | |
|     url = reverse("api:v1:channels-unsubscribe", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     response = logged_in_api_client.post(url)
 | |
| 
 | |
|     assert response.status_code == 204
 | |
| 
 | |
|     with pytest.raises(subscription.DoesNotExist):
 | |
|         subscription.refresh_from_db()
 | |
| 
 | |
| 
 | |
| def test_channel_subscribe_remote(factories, logged_in_api_client, mocker):
 | |
|     dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
|     channel_actor = factories["federation.Actor"]()
 | |
|     channel = factories["audio.Channel"](artist__description=None, actor=channel_actor)
 | |
|     url = reverse("api:v1:channels-subscribe", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     response = logged_in_api_client.post(url)
 | |
| 
 | |
|     assert response.status_code == 201
 | |
|     subscription = actor.emitted_follows.latest("id")
 | |
|     dispatch.assert_called_once_with(
 | |
|         {"type": "Follow"}, context={"follow": subscription}
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_channel_unsubscribe_remote(factories, logged_in_api_client, mocker):
 | |
|     dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
|     channel_actor = factories["federation.Actor"]()
 | |
|     channel = factories["audio.Channel"](actor=channel_actor)
 | |
|     subscription = factories["audio.Subscription"](target=channel.actor, actor=actor)
 | |
|     url = reverse("api:v1:channels-unsubscribe", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     response = logged_in_api_client.post(url)
 | |
| 
 | |
|     assert response.status_code == 204
 | |
|     dispatch.assert_called_once_with(
 | |
|         {"type": "Undo", "object": {"type": "Follow"}}, context={"follow": subscription}
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_subscriptions_list(factories, logged_in_api_client):
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
|     channel = factories["audio.Channel"](
 | |
|         artist__description=None, artist__with_cover=True
 | |
|     )
 | |
|     subscription = factories["audio.Subscription"](target=channel.actor, actor=actor)
 | |
|     setattr(subscription.target.channel.artist, "_tracks_count", 0)
 | |
|     setattr(subscription.target.channel.artist, "_prefetched_tagged_items", [])
 | |
|     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
 | |
|     assert response.data == {
 | |
|         "results": [{"uuid": subscription.uuid, "channel": channel.uuid}],
 | |
|         "count": 1,
 | |
|     }
 | |
| 
 | |
| 
 | |
| def test_channel_rss_feed(factories, api_client, preferences):
 | |
|     preferences["common__api_authentication_required"] = False
 | |
|     channel = factories["audio.Channel"](local=True)
 | |
|     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]
 | |
|     )
 | |
| 
 | |
|     url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     response = api_client.get(url)
 | |
| 
 | |
|     assert response.status_code == 200
 | |
|     assert response.data == expected
 | |
|     assert response["Content-Type"] == "application/rss+xml"
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
| 
 | |
| def test_channel_rss_feed_remote(factories, api_client, preferences):
 | |
|     preferences["common__api_authentication_required"] = False
 | |
|     channel = factories["audio.Channel"]()
 | |
| 
 | |
|     url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     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)
 | |
| 
 | |
|     url = reverse("api:v1:channels-rss", kwargs={"composite": channel.uuid})
 | |
| 
 | |
|     response = api_client.get(url)
 | |
| 
 | |
|     assert response.status_code == 401
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
| 
 | |
| 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)
 | |
| 
 | |
| 
 | |
| def test_refresh_channel_when_param_is_true(
 | |
|     factories,
 | |
|     mocker,
 | |
|     logged_in_api_client,
 | |
|     queryset_equal_queries,
 | |
| ):
 | |
|     obj = factories["audio.Channel"]()
 | |
|     refetch_obj = mocker.patch(
 | |
|         "funkwhale_api.music.views.refetch_obj", return_value=obj
 | |
|     )
 | |
|     url = reverse("api:v1:channels-detail", kwargs={"composite": obj.uuid})
 | |
|     response = logged_in_api_client.get(url, {"refresh": "true"})
 | |
| 
 | |
|     assert response.status_code == 200
 | |
|     assert refetch_obj.call_count == 1
 | |
|     assert refetch_obj.call_args[0][0] == obj
 | |
| 
 | |
| 
 | |
| def test_can_filter_channels_through_api_scope(factories, logged_in_api_client):
 | |
|     channel = factories["audio.Channel"](
 | |
|         attributed_to__preferred_username="PauseLecturePod"
 | |
|     )
 | |
|     factories["audio.Channel"]()
 | |
|     url = reverse("api:v1:channels-list")
 | |
|     response = logged_in_api_client.get(
 | |
|         url, {"scope": f"actor:{channel.attributed_to.full_username}"}
 | |
|     )
 | |
| 
 | |
|     assert response.status_code == 200
 | |
|     assert len(response.data["results"]) == 1
 | |
|     assert response.data["results"][0]["uuid"] == channel.uuid
 |