diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests.py b/lms/djangoapps/discussion/django_comment_client/base/tests.py index d2a4f921f28b..340f853866a6 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/tests.py +++ b/lms/djangoapps/discussion/django_comment_client/base/tests.py @@ -210,22 +210,6 @@ def test_flag(self, mock_is_forum_v2_enabled, mock_request): response = self.call_view("un_flag_abuse_for_thread", mock_is_forum_v2_enabled, mock_request) self._assert_json_response_contains_group_info(response) - def test_pin(self, mock_is_forum_v2_enabled, mock_request): - response = self.call_view( - "pin_thread", - mock_is_forum_v2_enabled, - mock_request, - user=self.moderator - ) - self._assert_json_response_contains_group_info(response) - response = self.call_view( - "un_pin_thread", - mock_is_forum_v2_enabled, - mock_request, - user=self.moderator - ) - self._assert_json_response_contains_group_info(response) - def test_openclose(self, mock_is_forum_v2_enabled, mock_request): response = self.call_view( "openclose_thread", @@ -1191,42 +1175,6 @@ def setUp(self): self.mock_get_course_id_by_thread = patcher.start() self.addCleanup(patcher.stop) - def test_pin_thread_as_student(self, mock_is_forum_v2_enabled, mock_request): - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, {}) - self.client.login(username=self.student.username, password=self.password) - response = self.client.post( - reverse("pin_thread", kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}) - ) - assert response.status_code == 401 - - def test_pin_thread_as_moderator(self, mock_is_forum_v2_enabled, mock_request): - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, {}) - self.client.login(username=self.moderator.username, password=self.password) - response = self.client.post( - reverse("pin_thread", kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}) - ) - assert response.status_code == 200 - - def test_un_pin_thread_as_student(self, mock_is_forum_v2_enabled, mock_request): - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, {}) - self.client.login(username=self.student.username, password=self.password) - response = self.client.post( - reverse("un_pin_thread", kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}) - ) - assert response.status_code == 401 - - def test_un_pin_thread_as_moderator(self, mock_is_forum_v2_enabled, mock_request): - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, {}) - self.client.login(username=self.moderator.username, password=self.password) - response = self.client.post( - reverse("un_pin_thread", kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}) - ) - assert response.status_code == 200 - def _set_mock_request_thread_and_comment(self, mock_is_forum_v2_enabled, mock_request, thread_data, comment_data): def handle_request(*args, **kwargs): url = args[1] diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py b/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py new file mode 100644 index 000000000000..cabe441039a7 --- /dev/null +++ b/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py @@ -0,0 +1,244 @@ +# pylint: skip-file +"""Tests for django comment client views.""" + +import json +import logging +from contextlib import contextmanager +from unittest import mock +from unittest.mock import ANY, Mock, patch + +import ddt +from django.contrib.auth.models import User +from django.core.management import call_command +from django.test.client import RequestFactory +from django.urls import reverse +from eventtracking.processors.exceptions import EventEmissionExit +from opaque_keys.edx.keys import CourseKey +from openedx_events.learning.signals import ( + FORUM_THREAD_CREATED, + FORUM_THREAD_RESPONSE_CREATED, + FORUM_RESPONSE_COMMENT_CREATED, +) + +from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.course_modes.tests.factories import CourseModeFactory +from common.djangoapps.student.roles import CourseStaffRole, UserBasedRole +from common.djangoapps.student.tests.factories import ( + CourseAccessRoleFactory, + CourseEnrollmentFactory, + UserFactory, +) +from common.djangoapps.track.middleware import TrackMiddleware +from common.djangoapps.track.views import segmentio +from common.djangoapps.track.views.tests.base import ( + SEGMENTIO_TEST_USER_ID, + SegmentIOTrackingTestCaseBase, +) +from common.djangoapps.util.testing import UrlResetMixin +from common.test.utils import MockSignalHandlerMixin, disable_signal +from lms.djangoapps.discussion.django_comment_client.base import views +from lms.djangoapps.discussion.django_comment_client.tests.group_id import ( + CohortedTopicGroupIdTestMixin, + GroupIdAssertionMixin, + NonCohortedTopicGroupIdTestMixin, +) +from lms.djangoapps.discussion.django_comment_client.tests.unicode import ( + UnicodeTestMixin, +) +from lms.djangoapps.discussion.django_comment_client.tests.utils import ( + CohortedTestCase, + ForumsEnableMixin, +) +from lms.djangoapps.teams.tests.factories import ( + CourseTeamFactory, + CourseTeamMembershipFactory, +) +from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted +from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory +from openedx.core.djangoapps.django_comment_common.comment_client import Thread +from openedx.core.djangoapps.django_comment_common.models import ( + FORUM_ROLE_STUDENT, + CourseDiscussionSettings, + Role, + assign_role, +) +from openedx.core.djangoapps.django_comment_common.utils import ( + ThreadContext, + seed_permissions_roles, +) +from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from openedx.core.lib.teams_config import TeamsConfig +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ( + TEST_DATA_SPLIT_MODULESTORE, + ModuleStoreTestCase, + SharedModuleStoreTestCase, +) +from xmodule.modulestore.tests.factories import ( + CourseFactory, + BlockFactory, + check_mongo_calls, +) + +from .event_transformers import ForumThreadViewedEventTransformer +from lms.djangoapps.discussion.django_comment_client.tests.mixins import ( + MockForumApiMixin, +) + + +@disable_signal(views, "thread_edited") +@disable_signal(views, "thread_voted") +@disable_signal(views, "thread_deleted") +class ThreadActionGroupIdTestCase( + CohortedTestCase, GroupIdAssertionMixin, MockForumApiMixin +): + """Test case for thread actions with group ID assertions.""" + + @classmethod + def setUpClass(cls): + """Set up class and forum mock.""" + super().setUpClass() + super().setUpClassAndForumMock() + + @classmethod + def tearDownClass(cls): + """Stop patches after tests complete.""" + super().tearDownClass() + super().disposeForumMocks() + + def call_view( + self, view_name, mock_function, user=None, post_params=None, view_args=None + ): + """Call a view with the given parameters.""" + thread_response = { + "user_id": str(self.student.id), + "group_id": self.student_cohort.id, + "closed": False, + "type": "thread", + "commentable_id": "non_team_dummy_id", + "body": "test body", + } + + self.set_mock_return_value("get_course_id_by_thread", self.course.id) + self.set_mock_return_value("get_thread", thread_response) + self.set_mock_return_value(mock_function, thread_response) + + request = RequestFactory().post("dummy_url", post_params or {}) + request.user = user or self.student + request.view_name = view_name + + return getattr(views, view_name)( + request, + course_id=str(self.course.id), + thread_id="dummy", + **(view_args or {}) + ) + + def test_pin_thread(self): + """Test pinning a thread.""" + response = self.call_view("pin_thread", "pin_thread", user=self.moderator) + assert response.status_code == 200 + self._assert_json_response_contains_group_info(response) + + response = self.call_view("un_pin_thread", "unpin_thread", user=self.moderator) + assert response.status_code == 200 + self._assert_json_response_contains_group_info(response) + + +@disable_signal(views, "comment_endorsed") +class ViewPermissionsTestCase( + ForumsEnableMixin, + UrlResetMixin, + SharedModuleStoreTestCase, + MockForumApiMixin, +): + """Test case for view permissions.""" + + @classmethod + def setUpClass(cls): # pylint: disable=super-method-not-called + """Set up class and forum mock.""" + super().setUpClassAndForumMock() + + with super().setUpClassAndTestData(): + cls.course = CourseFactory.create() + + @classmethod + def tearDownClass(cls): + """Stop patches after tests complete.""" + super().tearDownClass() + super().disposeForumMocks() + + @classmethod + def setUpTestData(cls): + """Set up test data.""" + super().setUpTestData() + + seed_permissions_roles(cls.course.id) + + cls.password = "test password" + cls.student = UserFactory.create(password=cls.password) + cls.moderator = UserFactory.create(password=cls.password) + + CourseEnrollmentFactory(user=cls.student, course_id=cls.course.id) + CourseEnrollmentFactory(user=cls.moderator, course_id=cls.course.id) + + cls.moderator.roles.add( + Role.objects.get(name="Moderator", course_id=cls.course.id) + ) + + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def setUp(self): + """Set up the test case.""" + super().setUp() + + # Set return values dynamically using the mixin method + self.set_mock_return_value("get_course_id_by_comment", self.course.id) + self.set_mock_return_value("get_course_id_by_thread", self.course.id) + self.set_mock_return_value("get_thread", {}) + self.set_mock_return_value("pin_thread", {}) + self.set_mock_return_value("unpin_thread", {}) + + def test_pin_thread_as_student(self): + """Test pinning a thread as a student.""" + self.client.login(username=self.student.username, password=self.password) + response = self.client.post( + reverse( + "pin_thread", + kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}, + ) + ) + assert response.status_code == 401 + + def test_pin_thread_as_moderator(self): + """Test pinning a thread as a moderator.""" + self.client.login(username=self.moderator.username, password=self.password) + response = self.client.post( + reverse( + "pin_thread", + kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}, + ) + ) + assert response.status_code == 200 + + def test_un_pin_thread_as_student(self): + """Test unpinning a thread as a student.""" + self.client.login(username=self.student.username, password=self.password) + response = self.client.post( + reverse( + "un_pin_thread", + kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}, + ) + ) + assert response.status_code == 401 + + def test_un_pin_thread_as_moderator(self): + """Test unpinning a thread as a moderator.""" + self.client.login(username=self.moderator.username, password=self.password) + response = self.client.post( + reverse( + "un_pin_thread", + kwargs={"course_id": str(self.course.id), "thread_id": "dummy"}, + ) + ) + assert response.status_code == 200 diff --git a/lms/djangoapps/discussion/django_comment_client/tests/mixins.py b/lms/djangoapps/discussion/django_comment_client/tests/mixins.py new file mode 100644 index 000000000000..28f1dc0714df --- /dev/null +++ b/lms/djangoapps/discussion/django_comment_client/tests/mixins.py @@ -0,0 +1,67 @@ +""" +Mixin for django_comment_client tests. +""" + +from unittest import mock + + +class MockForumApiMixin: + """Mixin to mock forum_api across different test cases with a single mock instance.""" + + @classmethod + def setUpClass(cls): + """Apply a single forum_api mock at the class level.""" + cls.setUpClassAndForumMock() + + @classmethod + def setUpClassAndForumMock(cls): + """ + Set up the class and apply the forum_api mock. + """ + cls.mock_forum_api = mock.Mock() + + # TODO: Remove this after moving all APIs + cls.flag_v2_patcher = mock.patch( + "openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled" + ) + cls.mock_enable_forum_v2 = cls.flag_v2_patcher.start() + cls.mock_enable_forum_v2.return_value = True + + patch_targets = [ + "openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api", + "openedx.core.djangoapps.django_comment_common.comment_client.comment.forum_api", + "openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api", + "openedx.core.djangoapps.django_comment_common.comment_client.course.forum_api", + "openedx.core.djangoapps.django_comment_common.comment_client.subscriptions.forum_api", + "openedx.core.djangoapps.django_comment_common.comment_client.user.forum_api", + ] + cls.forum_api_patchers = [ + mock.patch(target, cls.mock_forum_api) for target in patch_targets + ] + for patcher in cls.forum_api_patchers: + patcher.start() + + @classmethod + def disposeForumMocks(cls): + """Stop patches after tests complete.""" + cls.flag_v2_patcher.stop() + + for patcher in cls.forum_api_patchers: + patcher.stop() + + @classmethod + def tearDownClass(cls): + """Stop patches after tests complete.""" + cls.disposeForumMocks() + + def set_mock_return_value(self, function_name, return_value): + """ + Set a return value for a specific method in forum_api mock. + + Args: + function_name (str): The method name in the mock to set a return value for. + return_value (Any): The return value for the method. + """ + setattr( + self.mock_forum_api, function_name, mock.Mock(return_value=return_value) + ) diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py index e5739515f9b3..e37c09b851df 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py @@ -248,42 +248,20 @@ def unFlagAbuse(self, user, voteable, removeAll, course_id=None): def pin(self, user, thread_id, course_id=None): course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) - if is_forum_v2_enabled(course_key): - response = forum_api.pin_thread( - user_id=user.id, - thread_id=thread_id, - course_id=str(course_key) - ) - else: - url = _url_for_pin_thread(thread_id) - params = {'user_id': user.id} - response = utils.perform_request( - 'put', - url, - params, - metric_tags=self._metric_tags, - metric_action='thread.pin' - ) + response = forum_api.pin_thread( + user_id=user.id, + thread_id=thread_id, + course_id=str(course_key) + ) self._update_from_response(response) def un_pin(self, user, thread_id, course_id=None): course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) - if is_forum_v2_enabled(course_key): - response = forum_api.unpin_thread( - user_id=user.id, - thread_id=thread_id, - course_id=str(course_key) - ) - else: - url = _url_for_un_pin_thread(thread_id) - params = {'user_id': user.id} - response = utils.perform_request( - 'put', - url, - params, - metric_tags=self._metric_tags, - metric_action='thread.unpin' - ) + response = forum_api.unpin_thread( + user_id=user.id, + thread_id=thread_id, + course_id=str(course_key) + ) self._update_from_response(response)