from datetime import timedelta from django.utils import timezone from django.urls import reverse from misago.categories import THREADS_ROOT_NAME from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread from misago.threads.test import patch_category_acl from misago.threads.threadtypes import trees_map from misago.users.testutils import AuthenticatedUserTestCase class ThreadsApiTestCase(AuthenticatedUserTestCase): def setUp(self): super().setUp() threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME) self.root = Category.objects.get(tree_id=threads_tree_id, level=0) self.category = Category.objects.get(slug="first-category") self.thread = testutils.post_thread(category=self.category) self.api_link = self.thread.get_api_url() def get_thread_json(self): response = self.client.get(self.thread.get_api_url()) self.assertEqual(response.status_code, 200) return response.json() class ThreadRetrieveApiTests(ThreadsApiTestCase): def setUp(self): super().setUp() self.tested_links = [ self.api_link, "%sposts/" % self.api_link, "%sposts/?page=1" % self.api_link, ] @patch_category_acl() def test_api_returns_thread(self): """api has no showstoppers""" for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json["id"], self.thread.pk) self.assertEqual(response_json["title"], self.thread.title) if "posts" in link: self.assertIn("post_set", response_json) @patch_category_acl({"can_see_all_threads": False}) def test_api_shows_owned_thread(self): """api handles "owned threads only""" for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 404) self.thread.starter = self.user self.thread.save() for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 200) @patch_category_acl({"can_see": False}) def test_api_validates_category_see_permission(self): """api validates category visiblity""" for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 404) @patch_category_acl({"can_browse": False}) def test_api_validates_category_browse_permission(self): """api validates category browsability""" for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 404) def test_api_validates_posts_visibility(self): """api validates posts visiblity""" hidden_post = testutils.reply_thread( self.thread, is_hidden=True, message="I'am hidden test message!" ) with patch_category_acl({"can_hide_posts": 0}): response = self.client.get(self.tested_links[1]) self.assertNotContains( response, hidden_post.parsed ) # post's body is hidden # add permission to see hidden posts with patch_category_acl({"can_hide_posts": 1}): response = self.client.get(self.tested_links[1]) self.assertContains( response, hidden_post.parsed ) # hidden post's body is visible with permission # unapproved posts shouldn't show at all unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True) with patch_category_acl({"can_approve_content": False}): response = self.client.get(self.tested_links[1]) self.assertNotContains(response, unapproved_post.get_absolute_url()) # add permission to see unapproved posts with patch_category_acl({"can_approve_content": True}): response = self.client.get(self.tested_links[1]) self.assertContains(response, unapproved_post.get_absolute_url()) def test_api_validates_has_unapproved_posts_visibility(self): """api checks acl before exposing unapproved flag""" self.thread.has_unapproved_posts = True self.thread.save() with patch_category_acl({"can_approve_content": False}): for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json["id"], self.thread.pk) self.assertEqual(response_json["title"], self.thread.title) self.assertFalse(response_json["has_unapproved_posts"]) with patch_category_acl({"can_approve_content": True}): for link in self.tested_links: response = self.client.get(link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json["id"], self.thread.pk) self.assertEqual(response_json["title"], self.thread.title) self.assertTrue(response_json["has_unapproved_posts"]) class ThreadDeleteApiTests(ThreadsApiTestCase): def setUp(self): super().setUp() self.last_thread = testutils.post_thread(category=self.category) self.api_link = self.last_thread.get_api_url() def test_delete_thread_no_permission(self): """api tests permission to delete threads""" with patch_category_acl({"can_hide_threads": 0}): response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "You can't delete threads in this category." ) with patch_category_acl({"can_hide_threads": 1}): response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "You can't delete threads in this category." ) @patch_category_acl({"can_hide_threads": 1, "can_hide_own_threads": 2}) def test_delete_other_user_thread_no_permission(self): """api tests thread owner when deleting own thread""" response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "You can't delete other users theads in this category.", ) @patch_category_acl( {"can_hide_threads": 2, "can_hide_own_threads": 2, "can_close_threads": False} ) def test_delete_thread_closed_category_no_permission(self): """api tests category's closed state""" self.category.is_closed = True self.category.save() response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "This category is closed. You can't delete threads in it.", ) @patch_category_acl( {"can_hide_threads": 2, "can_hide_own_threads": 2, "can_close_threads": False} ) def test_delete_thread_closed_no_permission(self): """api tests thread's closed state""" self.last_thread.is_closed = True self.last_thread.save() response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "This thread is closed. You can't delete it." ) @patch_category_acl( {"can_hide_threads": 1, "can_hide_own_threads": 2, "thread_edit_time": 1} ) def test_delete_owned_thread_no_time(self): """api tests permission to delete owned thread within time limit""" self.last_thread.starter = self.user self.last_thread.started_on = timezone.now() - timedelta(minutes=10) self.last_thread.save() response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "You can't delete threads that are older than 1 minute.", ) @patch_category_acl({"can_hide_threads": 2}) def test_delete_thread(self): """DELETE to API link with permission deletes thread""" category = Category.objects.get(slug="first-category") self.assertEqual(category.last_thread_id, self.last_thread.pk) response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) with self.assertRaises(Thread.DoesNotExist): Thread.objects.get(pk=self.last_thread.pk) # category was synchronised after deletion category = Category.objects.get(slug="first-category") self.assertEqual(category.last_thread_id, self.thread.pk) # test that last thread's deletion triggers category sync response = self.client.delete(self.thread.get_api_url()) self.assertEqual(response.status_code, 200) with self.assertRaises(Thread.DoesNotExist): Thread.objects.get(pk=self.thread.pk) category = Category.objects.get(slug="first-category") self.assertIsNone(category.last_thread_id) @patch_category_acl( {"can_hide_threads": 1, "can_hide_own_threads": 2, "thread_edit_time": 30} ) def test_delete_owned_thread(self): """api lets owner to delete owned thread within time limit""" self.last_thread.starter = self.user self.last_thread.started_on = timezone.now() - timedelta(minutes=10) self.last_thread.save() response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200)