Skip to content
Open
274 changes: 274 additions & 0 deletions cms/djangoapps/contentstore/tests/test_course_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from ccx_keys.locator import CCXLocator
from django.test import RequestFactory
from opaque_keys.edx.locations import CourseLocator
from openedx_authz.api.users import assign_role_to_user_in_scope
from openedx_authz.constants.roles import COURSE_DATA_RESEARCHER, COURSE_EDITOR, COURSE_STAFF

from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
from cms.djangoapps.contentstore.utils import delete_course
Expand All @@ -36,6 +38,8 @@
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.core.djangoapps.authz.tests.mixins import AuthzTestMixin
from openedx.core import toggles as core_toggles
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -405,3 +409,273 @@ def _set_of_course_keys(course_list, key_attribute_name='id'):
self.assertSetEqual(
_set_of_course_keys(courses_in_progress), _set_of_course_keys(unsucceeded_course_actions, 'course_key')
)


@ddt.ddt
class TestCourseListingAuthz(AuthzTestMixin, ModuleStoreTestCase):
"""
Tests course listing using the new AuthZ authorization framework.
"""

def setUp(self):
super().setUp()

self.factory = RequestFactory()

def _create_course(self, course_key):
"""Helper method to create a course and its overview."""
course = CourseFactory.create(
org=course_key.org,
number=course_key.course,
run=course_key.run,
)

return CourseOverviewFactory.create(id=course.id, org=course_key.org)

def test_course_listing_with_course_staff_authz_permission(self):
# Create courses and assign access to only some of them to the user.
# Verify that only those courses are returned in the course listing.
# Using COURSE_STAFF role here.

course_key_1 = CourseLocator("Org1", "Course1", "Run1")
course1 = self._create_course(course_key_1)

course_key_2 = CourseLocator("Org1", "Course2", "Run1")
course2 = self._create_course(course_key_2)

assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
str(course_key_1),
)

request = self.factory.get("/course")
request.user = self.authorized_user

courses_list, _ = get_courses_accessible_to_user(request)

courses = list(courses_list)

self.assertEqual(len(courses), 1)
self.assertEqual(courses[0].id, course1.id)
self.assertEqual(course2.id, course_key_2)

def test_course_listing_with_course_editor_authz_permission(self):
# Create courses and assign access to only some of them to the user.
# Verify that only those courses are returned in the course listing.
# Using COURSE_EDITOR role here.

course_key_1 = CourseLocator("Org1", "Course1", "Run1")
course1 = self._create_course(course_key_1)

course_key_2 = CourseLocator("Org1", "Course2", "Run1")
course2 = self._create_course(course_key_2)

assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_EDITOR.external_key,
str(course_key_1),
)

request = self.factory.get("/course")
request.user = self.authorized_user

courses_list, _ = get_courses_accessible_to_user(request)

courses = list(courses_list)

self.assertEqual(len(courses), 1)
self.assertEqual(courses[0].id, course1.id)
self.assertEqual(course2.id, course_key_2)

def test_course_listing_without_permissions(self):
# Create a course but do not assign access to the user.
# Verify that no courses are returned in the course listing.

course_key = CourseLocator("Org1", "Course1", "Run1")

self._create_course(course_key)

request = self.factory.get("/course")
request.user = self.unauthorized_user

courses_list, _ = get_courses_accessible_to_user(request)

self.assertEqual(len(list(courses_list)), 0)

def test_non_staff_user_cannot_access(self):
"""
Create a course and assign a non-staff role to the user.
Verify that the course is not returned in the course listing.
"""
non_staff_user = UserFactory()
course_key = CourseLocator("Org1", "Course1", "Run1")
self._create_course(course_key)
self.add_user_to_role(non_staff_user, COURSE_DATA_RESEARCHER.external_key, course_key)

request = self.factory.get("/course")
request.user = non_staff_user

courses_list, _ = get_courses_accessible_to_user(request)

self.assertEqual(len(list(courses_list)), 0)

def _mock_authz_toggle(self, enabled_keys):
def _is_enabled(course_key=None, **_):
return str(course_key) in enabled_keys
return _is_enabled

def _make_request(self, user):
request = self.factory.get("/course")
request.user = user
return request

def _create_courses(self):
"""Helper method to create multiple courses for testing."""
authz_keys = [
CourseLocator("Org1", "Course1", "AuthzRun"),
CourseLocator("Org1", "Course2", "AuthzRun"),
CourseLocator("Org1", "Course3", "AuthzRun"),
]

legacy_keys = [
CourseLocator("Org1", "Course1", "LegacyRun"),
CourseLocator("Org1", "Course2", "LegacyRun"),
CourseLocator("Org1", "Course3", "LegacyRun"),
]

authz_courses = [self._create_course(k) for k in authz_keys]
legacy_courses = [self._create_course(k) for k in legacy_keys]

return authz_keys, legacy_keys, authz_courses, legacy_courses

@ddt.data(
{
"name": "authz_and_legacy_basic",
"is_staff": False,
"is_superuser": False,
"authz_enabled": [0, 2], # toggle ON for Course1, Course3

"authz_roles": [
{"index": 0, "role": COURSE_STAFF}, # valid (toggle ON)
{"index": 1, "role": COURSE_EDITOR}, # ignored (toggle OFF)
],
"legacy_roles": [0],

"expected": [("authz", 0), ("legacy", 0)],
},
{
"name": "authz_role_but_toggle_off",
"is_staff": False,
"is_superuser": False,
"authz_enabled": [2], # only Course3 enabled

"authz_roles": [
{"index": 1, "role": COURSE_EDITOR}, # should NOT appear
],
"legacy_roles": [],

"expected": [],
},
{
"name": "multiple_roles_mixed",
"is_staff": False,
"is_superuser": False,
"authz_enabled": [0, 1, 2],

"authz_roles": [
{"index": 0, "role": COURSE_STAFF},
{"index": 1, "role": COURSE_EDITOR},
],
"legacy_roles": [2],

"expected": [("authz", 0), ("authz", 1), ("legacy", 2)],
},
{
"name": "staff_gets_all",
"is_staff": True,
"is_superuser": False,
"authz_enabled": [],

"authz_roles": [],
"legacy_roles": [],

"expected": [
("authz", 0), ("authz", 1), ("authz", 2),
("legacy", 0), ("legacy", 1), ("legacy", 2),
],
},
{
"name": "superuser_gets_all",
"is_staff": False,
"is_superuser": True,
"authz_enabled": [],

"authz_roles": [],
"legacy_roles": [],

"expected": [
("authz", 0), ("authz", 1), ("authz", 2),
("legacy", 0), ("legacy", 1), ("legacy", 2),
],
},
)
@ddt.unpack
def test_course_access_matrix(
self,
name,
is_staff,
is_superuser,
authz_enabled,
authz_roles,
legacy_roles,
expected,
):
# --- Setup toggle ---
enabled_keys = set()

authz_keys, legacy_keys, authz_courses, legacy_courses = self._create_courses()

for i in authz_enabled:
enabled_keys.add(str(authz_keys[i]))

with patch.object(
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
"is_enabled",
side_effect=self._mock_authz_toggle(enabled_keys),
):
# --- Create user ---
user = UserFactory(is_superuser=is_superuser)

if is_staff:
GlobalStaff().add_users(user)

# --- Assign AUTHZ roles ---
for r in authz_roles:
assign_role_to_user_in_scope(
user.username,
r["role"].external_key,
str(authz_keys[r["index"]]),
)

# --- Assign LEGACY roles ---
for i in legacy_roles:
CourseInstructorRole(legacy_keys[i]).add_users(user)

# --- Execute ---
request = self._make_request(user)
courses, _ = get_courses_accessible_to_user(request)

# --- Expected set ---
expected_ids = {
(authz_courses[i].id if group == "authz" else legacy_courses[i].id)
for group, i in expected
}

result_ids = {course.id for course in courses}

self.assertEqual(
result_ids,
expected_ids,
msg=f"Failed scenario: {name}",
)
Loading
Loading