|
22 | 22 | from enterprise.api.v1.serializers import EnterpriseCustomerSerializer |
23 | 23 | from milestones.tests.utils import MilestonesTestCaseMixin |
24 | 24 | from opaque_keys.edx.locator import CourseLocator |
| 25 | +from openedx_authz.constants.roles import COURSE_EDITOR |
25 | 26 |
|
26 | 27 | import lms.djangoapps.courseware.access as access |
27 | 28 | import lms.djangoapps.courseware.access_response as access_response |
28 | 29 | from common.djangoapps.student.models import CourseEnrollment |
29 | | -from common.djangoapps.student.roles import CourseCcxCoachRole, CourseStaffRole |
| 30 | +from common.djangoapps.student.roles import CourseCcxCoachRole, CourseLimitedStaffRole, CourseStaffRole |
30 | 31 | from common.djangoapps.student.tests.factories import ( |
31 | 32 | AdminFactory, |
32 | 33 | AnonymousUserFactory, |
|
43 | 44 | from lms.djangoapps.courseware.masquerade import CourseMasquerade |
44 | 45 | from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member |
45 | 46 | from lms.djangoapps.courseware.toggles import course_is_invitation_only |
| 47 | +from openedx.core import toggles as core_toggles |
| 48 | +from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin |
46 | 49 | from openedx.core.djangoapps.content.course_overviews.models import CourseOverview |
47 | 50 | from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory |
48 | 51 | from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES |
@@ -1019,3 +1022,109 @@ def test_course_catalog_access_num_queries_enterprise(self, user_attr_name, cour |
1019 | 1022 | course_overview = CourseOverview.get_from_id(course.id) |
1020 | 1023 | with self.assertNumQueries(num_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST): |
1021 | 1024 | bool(access.has_access(user, 'see_exists', course_overview, course_key=course.id)) |
| 1025 | + |
| 1026 | + |
| 1027 | +class AuthzSeeAboutPageAccessTestCase(CourseAuthoringAuthzTestMixin, SharedModuleStoreTestCase): |
| 1028 | + """ |
| 1029 | + see_about_page access when AuthZ course authoring is enabled for the course. |
| 1030 | + """ |
| 1031 | + |
| 1032 | + @classmethod |
| 1033 | + def setUpClass(cls): |
| 1034 | + super().setUpClass() |
| 1035 | + cls.course_public = CourseFactory.create( |
| 1036 | + catalog_visibility=CATALOG_VISIBILITY_CATALOG_AND_ABOUT, |
| 1037 | + course="authzpublic", |
| 1038 | + ) |
| 1039 | + cls.course_about_only = CourseFactory.create( |
| 1040 | + catalog_visibility=CATALOG_VISIBILITY_ABOUT, |
| 1041 | + course="authzabout", |
| 1042 | + ) |
| 1043 | + cls.course_hidden = CourseFactory.create( |
| 1044 | + catalog_visibility=CATALOG_VISIBILITY_NONE, |
| 1045 | + course="authzhidden", |
| 1046 | + ) |
| 1047 | + |
| 1048 | + def _see_about_page_response(self, user, course): |
| 1049 | + course_overview = CourseOverview.get_from_id(course.id) |
| 1050 | + return access.has_access(user, "see_about_page", course_overview, course_key=course.id) |
| 1051 | + |
| 1052 | + def test_learner_granted_via_catalog_visibility_both(self): |
| 1053 | + """Learners without AuthZ roles can view the about page when catalog allows it.""" |
| 1054 | + response = self._see_about_page_response(self.unauthorized_user, self.course_public) |
| 1055 | + self.assertTrue(response) # noqa: PT009 |
| 1056 | + |
| 1057 | + def test_learner_granted_via_catalog_visibility_about_only(self): |
| 1058 | + """Learners without AuthZ roles can view about-only courses.""" |
| 1059 | + response = self._see_about_page_response(self.unauthorized_user, self.course_about_only) |
| 1060 | + self.assertTrue(response) # noqa: PT009 |
| 1061 | + |
| 1062 | + def test_enrolled_learner_denied_when_catalog_hidden(self): |
| 1063 | + """Enrollment alone does not grant about-page access when catalog is hidden.""" |
| 1064 | + CourseEnrollmentFactory(user=self.unauthorized_user, course_id=self.course_hidden.id) |
| 1065 | + |
| 1066 | + response = self._see_about_page_response(self.unauthorized_user, self.course_hidden) |
| 1067 | + |
| 1068 | + self.assertFalse(response) # noqa: PT009 |
| 1069 | + self.assertIsInstance(response, access_response.CatalogVisibilityError) # noqa: PT009 |
| 1070 | + |
| 1071 | + def test_beta_tester_granted_via_catalog_about(self): |
| 1072 | + """Beta testers rely on catalog visibility, not AuthZ authoring permissions.""" |
| 1073 | + beta_tester = BetaTesterFactory.create(course_key=self.course_about_only.id) |
| 1074 | + |
| 1075 | + response = self._see_about_page_response(beta_tester, self.course_about_only) |
| 1076 | + |
| 1077 | + self.assertTrue(response) # noqa: PT009 |
| 1078 | + |
| 1079 | + def test_course_staff_bypass_when_catalog_hidden(self): |
| 1080 | + """Course staff can preview the about page when catalog visibility is none.""" |
| 1081 | + course_staff = StaffFactory.create(course_key=self.course_hidden.id) |
| 1082 | + |
| 1083 | + response = self._see_about_page_response(course_staff, self.course_hidden) |
| 1084 | + |
| 1085 | + self.assertTrue(response) # noqa: PT009 |
| 1086 | + |
| 1087 | + def test_limited_staff_bypass_when_catalog_hidden(self): |
| 1088 | + """Limited staff inherit staff bypass for about-page access.""" |
| 1089 | + limited_staff = UserFactory.create() |
| 1090 | + CourseLimitedStaffRole(self.course_hidden.id).add_users(limited_staff) |
| 1091 | + |
| 1092 | + response = self._see_about_page_response(limited_staff, self.course_hidden) |
| 1093 | + |
| 1094 | + self.assertTrue(response) # noqa: PT009 |
| 1095 | + |
| 1096 | + def test_authz_role_grants_access_when_catalog_hidden(self): |
| 1097 | + """Users with COURSES_VIEW_COURSE can access hidden about pages.""" |
| 1098 | + self.add_user_to_role_in_course(self.unauthorized_user, COURSE_EDITOR.external_key, self.course_hidden.id) |
| 1099 | + |
| 1100 | + response = self._see_about_page_response(self.unauthorized_user, self.course_hidden) |
| 1101 | + |
| 1102 | + self.assertTrue(response) # noqa: PT009 |
| 1103 | + |
| 1104 | + def test_anonymous_user_uses_legacy_path(self): |
| 1105 | + """Anonymous users are routed to the legacy path and follow catalog visibility.""" |
| 1106 | + anonymous_user = AnonymousUserFactory.create() |
| 1107 | + |
| 1108 | + response = self._see_about_page_response(anonymous_user, self.course_public) |
| 1109 | + |
| 1110 | + self.assertTrue(response) # noqa: PT009 |
| 1111 | + |
| 1112 | + def test_denied_returns_catalog_visibility_error(self): |
| 1113 | + """AuthZ path returns CatalogVisibilityError when all checks fail.""" |
| 1114 | + response = self._see_about_page_response(self.unauthorized_user, self.course_hidden) |
| 1115 | + |
| 1116 | + self.assertFalse(response) # noqa: PT009 |
| 1117 | + self.assertIsInstance(response, access_response.CatalogVisibilityError) # noqa: PT009 |
| 1118 | + self.assertEqual(response.error_code, "not_visible_in_catalog") # noqa: PT009 |
| 1119 | + |
| 1120 | + def test_legacy_path_when_authz_disabled(self): |
| 1121 | + """When AuthZ is off, catalog visibility rules still apply.""" |
| 1122 | + with patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, "is_enabled", return_value=False): |
| 1123 | + response = self._see_about_page_response(self.unauthorized_user, self.course_public) |
| 1124 | + |
| 1125 | + self.assertTrue(response) # noqa: PT009 |
| 1126 | + |
| 1127 | + hidden_response = self._see_about_page_response(self.unauthorized_user, self.course_hidden) |
| 1128 | + |
| 1129 | + self.assertFalse(hidden_response) # noqa: PT009 |
| 1130 | + self.assertIsInstance(hidden_response, access_response.CatalogVisibilityError) # noqa: PT009 |
0 commit comments