import io import pytest from django.urls import reverse from funkwhale_api.common import serializers from funkwhale_api.common import signals from funkwhale_api.common import tasks from funkwhale_api.common import throttling from funkwhale_api.common import utils def test_can_detail_mutation(logged_in_api_client, factories): mutation = factories["common.Mutation"]( payload={}, target=factories["music.Artist"]() ) url = reverse("api:v1:mutations-detail", kwargs={"uuid": mutation.uuid}) response = logged_in_api_client.get(url) expected = serializers.APIMutationSerializer(mutation).data assert response.status_code == 200 assert response.data == expected def test_can_list_mutations(logged_in_api_client, factories): mutation = factories["common.Mutation"]( payload={}, target=factories["music.Artist"]() ) url = reverse("api:v1:mutations-list") response = logged_in_api_client.get(url) expected = serializers.APIMutationSerializer(mutation).data assert response.status_code == 200 assert response.data["results"] == [expected] def test_can_destroy_mutation_creator(logged_in_api_client, factories): actor = logged_in_api_client.user.create_actor() track = factories["music.Track"]() mutation = factories["common.Mutation"]( target=track, type="update", payload={}, created_by=actor ) url = reverse("api:v1:mutations-detail", kwargs={"uuid": mutation.uuid}) response = logged_in_api_client.delete(url) assert response.status_code == 204 def test_can_destroy_mutation_not_creator(logged_in_api_client, factories): logged_in_api_client.user.create_actor() track = factories["music.Track"]() mutation = factories["common.Mutation"](type="update", target=track, payload={}) url = reverse("api:v1:mutations-detail", kwargs={"uuid": mutation.uuid}) response = logged_in_api_client.delete(url) assert response.status_code == 403 mutation.refresh_from_db() def test_can_destroy_mutation_has_perm(logged_in_api_client, factories, mocker): actor = logged_in_api_client.user.create_actor() track = factories["music.Track"]() mutation = factories["common.Mutation"](target=track, type="update", payload={}) has_perm = mocker.patch( "funkwhale_api.common.mutations.registry.has_perm", return_value=True ) url = reverse("api:v1:mutations-detail", kwargs={"uuid": mutation.uuid}) response = logged_in_api_client.delete(url) assert response.status_code == 204 has_perm.assert_called_once_with( obj=mutation.target, type=mutation.type, perm="approve", actor=actor ) @pytest.mark.parametrize("endpoint, expected", [("approve", True), ("reject", False)]) def test_can_approve_reject_mutation_with_perm( endpoint, expected, logged_in_api_client, factories, mocker ): on_commit = mocker.patch("funkwhale_api.common.utils.on_commit") actor = logged_in_api_client.user.create_actor() track = factories["music.Track"]() mutation = factories["common.Mutation"](target=track, type="update", payload={}) has_perm = mocker.patch( "funkwhale_api.common.mutations.registry.has_perm", return_value=True ) url = reverse( "api:v1:mutations-{}".format(endpoint), kwargs={"uuid": mutation.uuid} ) response = logged_in_api_client.post(url) assert response.status_code == 200 has_perm.assert_called_once_with( obj=mutation.target, type=mutation.type, perm="approve", actor=actor ) if expected: on_commit.assert_any_call(tasks.apply_mutation.delay, mutation_id=mutation.id) mutation.refresh_from_db() assert mutation.is_approved == expected assert mutation.approved_by == actor on_commit.assert_any_call( signals.mutation_updated.send, mutation=mutation, sender=None, new_is_approved=expected, old_is_approved=None, ) @pytest.mark.parametrize("endpoint, expected", [("approve", True), ("reject", False)]) def test_cannot_approve_reject_applied_mutation( endpoint, expected, logged_in_api_client, factories, mocker ): on_commit = mocker.patch("funkwhale_api.common.utils.on_commit") logged_in_api_client.user.create_actor() track = factories["music.Track"]() mutation = factories["common.Mutation"]( target=track, type="update", payload={}, is_applied=True ) mocker.patch("funkwhale_api.common.mutations.registry.has_perm", return_value=True) url = reverse( "api:v1:mutations-{}".format(endpoint), kwargs={"uuid": mutation.uuid} ) response = logged_in_api_client.post(url) assert response.status_code == 403 on_commit.assert_not_called() mutation.refresh_from_db() assert mutation.is_approved is None assert mutation.approved_by is None @pytest.mark.parametrize("endpoint, expected", [("approve", True), ("reject", False)]) def test_cannot_approve_reject_without_perm( endpoint, expected, logged_in_api_client, factories, mocker ): on_commit = mocker.patch("funkwhale_api.common.utils.on_commit") logged_in_api_client.user.create_actor() track = factories["music.Track"]() mutation = factories["common.Mutation"](target=track, type="update", payload={}) mocker.patch("funkwhale_api.common.mutations.registry.has_perm", return_value=False) url = reverse( "api:v1:mutations-{}".format(endpoint), kwargs={"uuid": mutation.uuid} ) response = logged_in_api_client.post(url) assert response.status_code == 403 on_commit.assert_not_called() mutation.refresh_from_db() assert mutation.is_approved is None assert mutation.approved_by is None def test_rate_limit(logged_in_api_client, now_time, settings, mocker): expected_ident = {"type": "authenticated", "id": logged_in_api_client.user.pk} expected = { "ident": expected_ident, "scopes": throttling.get_status(expected_ident, now_time), "enabled": settings.THROTTLING_ENABLED, } get_status = mocker.spy(throttling, "get_status") url = reverse("api:v1:rate-limit") response = logged_in_api_client.get(url) assert response.status_code == 200 assert response.data == expected get_status.assert_called_once_with(expected_ident, now_time) @pytest.mark.parametrize( "next, expected", [ ("original", "original"), ("medium_square_crop", "medium_square_crop"), ("unknown", "original"), ], ) def test_attachment_proxy_redirects_original( next, expected, factories, logged_in_api_client, mocker, avatar, r_mock, now ): attachment = factories["common.Attachment"](file=None) avatar_content = avatar.read() fetch_remote_attachment = mocker.spy(tasks, "fetch_remote_attachment") m = r_mock.get(attachment.url, body=io.BytesIO(avatar_content)) proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": attachment.uuid}) response = logged_in_api_client.get(proxy_url, {"next": next}) attachment.refresh_from_db() urls = serializers.AttachmentSerializer(attachment).data["urls"] assert attachment.file.read() == avatar_content assert attachment.last_fetch_date == now fetch_remote_attachment.assert_called_once_with(attachment) assert len(m.request_history) == 1 assert response.status_code == 302 assert response["Location"] == urls[expected] def test_attachment_proxy_dont_crash_on_long_filename( factories, logged_in_api_client, avatar, r_mock, now ): long_filename = "a" * 400 attachment = factories["common.Attachment"]( file=None, url="https://domain/{}.jpg".format(long_filename) ) avatar_content = avatar.read() r_mock.get(attachment.url, body=io.BytesIO(avatar_content)) proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": attachment.uuid}) response = logged_in_api_client.get(proxy_url, {"next": next}) attachment.refresh_from_db() assert response.status_code == 302 assert attachment.file.read() == avatar_content assert attachment.file.name.endswith("/{}.jpg".format("a" * 46)) assert attachment.last_fetch_date == now def test_attachment_create(logged_in_api_client, avatar): actor = logged_in_api_client.user.create_actor() url = reverse("api:v1:attachments-list") content = avatar.read() avatar.seek(0) payload = {"file": avatar} response = logged_in_api_client.post(url, payload) assert response.status_code == 201 attachment = actor.attachments.latest("id") assert attachment.file.read() == content assert attachment.file.size == len(content) def test_attachment_destroy(factories, logged_in_api_client): actor = logged_in_api_client.user.create_actor() attachment = factories["common.Attachment"](actor=actor) url = reverse("api:v1:attachments-detail", kwargs={"uuid": attachment.uuid}) response = logged_in_api_client.delete(url) assert response.status_code == 204 with pytest.raises(attachment.DoesNotExist): attachment.refresh_from_db() def test_attachment_destroy_not_owner(factories, logged_in_api_client): logged_in_api_client.user.create_actor() attachment = factories["common.Attachment"]() url = reverse("api:v1:attachments-detail", kwargs={"uuid": attachment.uuid}) response = logged_in_api_client.delete(url) assert response.status_code == 403 attachment.refresh_from_db() def test_can_render_text_preview(api_client, db): payload = {"text": "Hello world"} url = reverse("api:v1:text-preview") response = api_client.post(url, payload) expected = {"rendered": utils.render_html(payload["text"], "text/markdown")} assert response.status_code == 200 assert response.data == expected def test_can_render_text_preview_permissive(api_client, db): payload = {"text": "Hello world", "permissive": True} url = reverse("api:v1:text-preview") response = api_client.post(url, payload) expected = { "rendered": utils.render_html(payload["text"], "text/markdown", permissive=True) } assert response.status_code == 200 assert response.data == expected