Skip to content

Commit 040c426

Browse files
committed
test: add unit tests for new scope
1 parent 38842db commit 040c426

2 files changed

Lines changed: 147 additions & 2 deletions

File tree

openedx_authz/tests/api/test_data.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
OrgContentLibraryGlobData,
1515
OrgCourseOverviewGlobData,
1616
PermissionData,
17+
PlatformCourseOverviewGlobData,
1718
RoleAssignmentData,
1819
RoleData,
1920
ScopeData,
@@ -267,12 +268,17 @@ def test_scope_data_registration(self):
267268
self.assertIn("course-v1", ScopeMeta.org_glob_registry)
268269
self.assertIs(ScopeMeta.org_glob_registry["course-v1"], OrgCourseOverviewGlobData)
269270

271+
# Glob registries for platform-level scopes
272+
self.assertIn("course-v1", ScopeMeta.platform_glob_registry)
273+
self.assertIs(ScopeMeta.platform_glob_registry["course-v1"], PlatformCourseOverviewGlobData)
274+
270275
@data(
271276
("ccx-v1^ccx-v1:OpenedX+DemoX+DemoCourse+ccx@1", CCXCourseOverviewData),
272277
("course-v1^course-v1:WGU+CS002+2025_T1", CourseOverviewData),
273278
("lib^lib:DemoX:CSPROB", ContentLibraryData),
274279
("lib^lib:DemoX*", OrgContentLibraryGlobData),
275280
("course-v1^course-v1:OpenedX*", OrgCourseOverviewGlobData),
281+
("course-v1^course-v1:*", PlatformCourseOverviewGlobData),
276282
("global^generic_scope", ScopeData),
277283
)
278284
@unpack
@@ -294,6 +300,7 @@ def test_dynamic_instantiation_via_namespaced_key(self, namespaced_key, expected
294300
("lib^lib:DemoX:CSPROB", ContentLibraryData),
295301
("lib^lib:DemoX:*", OrgContentLibraryGlobData),
296302
("course-v1^course-v1:OpenedX+*", OrgCourseOverviewGlobData),
303+
("course-v1^course-v1:*", PlatformCourseOverviewGlobData),
297304
("global^generic", ScopeData),
298305
("unknown^something", ScopeData),
299306
)
@@ -318,6 +325,7 @@ def test_get_subclass_by_namespaced_key(self, namespaced_key, expected_class):
318325
("lib:DemoX:CSPROB", ContentLibraryData),
319326
("lib:DemoX:*", OrgContentLibraryGlobData),
320327
("course-v1:OpenedX+*", OrgCourseOverviewGlobData),
328+
("course-v1:*", PlatformCourseOverviewGlobData),
321329
("lib:edX:Demo", ContentLibraryData),
322330
("global:generic_scope", ScopeData),
323331
)
@@ -906,3 +914,79 @@ def test_exists_false_when_org_cannot_be_parsed(self):
906914

907915
self.assertIsNone(scope.org)
908916
self.assertFalse(scope.exists())
917+
918+
919+
@ddt
920+
class TestPlatformCourseOverviewGlobData(TestCase):
921+
"""Tests for the PlatformCourseOverviewGlobData scope."""
922+
923+
PLATFORM_GLOB_EXTERNAL_KEY = "course-v1:*"
924+
PLATFORM_GLOB_NAMESPACED_KEY = "course-v1^course-v1:*"
925+
926+
def test_build_external_key(self):
927+
"""build_external_key returns the platform-wide course glob pattern."""
928+
self.assertEqual(PlatformCourseOverviewGlobData.build_external_key(), self.PLATFORM_GLOB_EXTERNAL_KEY)
929+
930+
@data(
931+
("course-v1:*", True),
932+
("course-v1:OpenedX+*", False),
933+
("course-v1:OpenedX*", False),
934+
("course-v1:OpenedX", False),
935+
("course-v1:*:*", False),
936+
("other:*", False),
937+
("lib:*", False),
938+
)
939+
@unpack
940+
def test_validate_external_key(self, external_key, expected_valid):
941+
"""Validate platform-level course glob external keys."""
942+
self.assertEqual(PlatformCourseOverviewGlobData.validate_external_key(external_key), expected_valid)
943+
944+
def test_exists_always_true(self):
945+
"""exists() returns True without checking the database."""
946+
scope = PlatformCourseOverviewGlobData(external_key=self.PLATFORM_GLOB_EXTERNAL_KEY)
947+
948+
self.assertTrue(scope.exists())
949+
950+
def test_get_object_returns_none(self):
951+
"""Platform glob scopes do not map to a concrete domain object."""
952+
scope = PlatformCourseOverviewGlobData(external_key=self.PLATFORM_GLOB_EXTERNAL_KEY)
953+
954+
self.assertIsNone(scope.get_object())
955+
956+
def test_namespaced_key(self):
957+
"""namespaced_key includes namespace prefix and external key."""
958+
scope = PlatformCourseOverviewGlobData(external_key=self.PLATFORM_GLOB_EXTERNAL_KEY)
959+
960+
self.assertEqual(scope.namespaced_key, self.PLATFORM_GLOB_NAMESPACED_KEY)
961+
962+
def test_dynamic_instantiation_via_scope_data(self):
963+
"""ScopeData resolves course-v1:* to PlatformCourseOverviewGlobData."""
964+
scope = ScopeData(external_key=self.PLATFORM_GLOB_EXTERNAL_KEY)
965+
966+
self.assertIsInstance(scope, PlatformCourseOverviewGlobData)
967+
self.assertEqual(scope.external_key, self.PLATFORM_GLOB_EXTERNAL_KEY)
968+
969+
def test_get_admin_view_permission(self):
970+
"""View permission matches course team view permission."""
971+
self.assertEqual(
972+
PlatformCourseOverviewGlobData.get_admin_view_permission(), permissions.COURSES_VIEW_COURSE_TEAM
973+
)
974+
975+
def test_get_admin_manage_permission(self):
976+
"""Manage permission matches course team manage permission."""
977+
self.assertEqual(
978+
PlatformCourseOverviewGlobData.get_admin_manage_permission(),
979+
permissions.COURSES_MANAGE_COURSE_TEAM,
980+
)
981+
982+
def test_is_platform_glob(self):
983+
"""Platform course glob is flagged as a platform-level glob scope."""
984+
self.assertTrue(PlatformCourseOverviewGlobData.IS_PLATFORM_GLOB)
985+
self.assertFalse(PlatformCourseOverviewGlobData.IS_ORG_GLOB)
986+
987+
def test_get_all_platform_glob_namespaces(self):
988+
"""Platform glob namespace is registered in ScopeMeta."""
989+
platform_globs = ScopeMeta.get_all_platform_glob_namespaces()
990+
991+
self.assertIn("course-v1", platform_globs)
992+
self.assertIs(platform_globs["course-v1"], PlatformCourseOverviewGlobData)

openedx_authz/tests/test_enforcement.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
CourseOverviewData,
2323
OrgContentLibraryGlobData,
2424
OrgCourseOverviewGlobData,
25+
PlatformCourseOverviewGlobData,
2526
)
2627
from openedx_authz.constants import roles
2728
from openedx_authz.constants.permissions import (
@@ -675,6 +676,41 @@ def test_org_level_glob_enforcement(self, request: AuthRequest):
675676
self._test_enforcement(self.POLICIES + self.ASSIGNMENTS, request)
676677

677678

679+
@ddt
680+
class PlatformGlobCourseEnforcementTests(CasbinEnforcementTestCase):
681+
"""
682+
Tests for platform-level glob patterns in course scopes.
683+
684+
This test class verifies that policies defined with platform-level glob patterns
685+
(e.g., ``course-v1:*``) are correctly enforced for concrete course scopes across
686+
all organizations on the platform.
687+
"""
688+
689+
POLICIES = [
690+
make_policy(roles.COURSE_STAFF.external_key, COURSES_VIEW_COURSE.identifier, CourseOverviewData.NAMESPACE)
691+
]
692+
693+
ASSIGNMENTS = [
694+
make_course_assignment("user1", roles.COURSE_STAFF.external_key, "course-v1:*"),
695+
]
696+
697+
CASES = [
698+
# Permission granted across organizations
699+
make_course_case("user1", COURSES_VIEW_COURSE.identifier, "course-v1:OpenedX+Python+2026", True),
700+
make_course_case("user1", COURSES_VIEW_COURSE.identifier, "course-v1:OtherOrg+Course+2025", True),
701+
make_course_case("user1", COURSES_VIEW_COURSE.identifier, "course-v1:InexistentOrg+Demo+2026", True),
702+
make_course_case("user1", COURSES_VIEW_COURSE.identifier, "course-v1:OpenedXv2+Demo+2026", True),
703+
# Permission denied
704+
make_course_case("user1", COURSES_CREATE_FILES.identifier, "course-v1:OpenedX+Python+2026", False),
705+
make_course_case("user2", COURSES_VIEW_COURSE.identifier, "course-v1:OpenedX+Demo+2026", False),
706+
]
707+
708+
@data(*CASES)
709+
def test_platform_level_glob_enforcement(self, request: AuthRequest):
710+
"""Test that platform-level glob patterns in course scopes are enforced correctly."""
711+
self._test_enforcement(self.POLICIES + self.ASSIGNMENTS, request)
712+
713+
678714
def make_org_library_glob_key(key: str) -> str:
679715
"""Create a namespaced org-level library glob key (e.g., 'lib^lib:DemoX:*')."""
680716
return f"{OrgContentLibraryGlobData.NAMESPACE}{OrgContentLibraryGlobData.SEPARATOR}{key}"
@@ -685,6 +721,11 @@ def make_org_course_glob_key(key: str) -> str:
685721
return f"{OrgCourseOverviewGlobData.NAMESPACE}{OrgCourseOverviewGlobData.SEPARATOR}{key}"
686722

687723

724+
def make_platform_course_glob_key(key: str) -> str:
725+
"""Create a namespaced platform-level course glob key (e.g., 'course-v1^course-v1:*')."""
726+
return f"{PlatformCourseOverviewGlobData.NAMESPACE}{PlatformCourseOverviewGlobData.SEPARATOR}{key}"
727+
728+
688729
@pytest.mark.django_db
689730
@ddt
690731
class StaffSuperuserAccessTests(CasbinEnforcementTestCase):
@@ -869,6 +910,25 @@ def setUp(self) -> None:
869910
make_org_course_glob_key("course-v1:TestOrg+*"),
870911
False,
871912
),
913+
# PlatformCourseOverviewGlobData scope
914+
(
915+
make_user_key("staff_user"),
916+
make_action_key("courses.view_course"),
917+
make_platform_course_glob_key("course-v1:*"),
918+
True,
919+
),
920+
(
921+
make_user_key("superuser"),
922+
make_action_key("courses.view_course"),
923+
make_platform_course_glob_key("course-v1:*"),
924+
True,
925+
),
926+
(
927+
make_user_key("regular_user"),
928+
make_action_key("courses.view_course"),
929+
make_platform_course_glob_key("course-v1:*"),
930+
False,
931+
),
872932
# Unsupported scope type - no one is granted access via this matcher
873933
(make_user_key("staff_user"), make_action_key("manage"), make_scope_key("org", "TestOrg"), False),
874934
(make_user_key("superuser"), make_action_key("manage"), make_scope_key("org", "TestOrg"), False),
@@ -885,13 +945,14 @@ def test_is_admin_or_superuser_check(
885945
886946
Verifies that:
887947
- Staff users are always allowed for ContentLibraryData, CourseOverviewData,
888-
OrgContentLibraryGlobData, and OrgCourseOverviewGlobData scopes.
948+
OrgContentLibraryGlobData, OrgCourseOverviewGlobData, and
949+
PlatformCourseOverviewGlobData scopes.
889950
- Superusers are always allowed for the same scopes.
890951
- Regular users are denied when they have no role assignments.
891952
- Unsupported scope types (e.g., org) are denied even for staff/superusers.
892953
893954
Expected result:
894-
- staff_user and superuser: True for all four supported scope types.
955+
- staff_user and superuser: True for all supported scope types.
895956
- regular_user: False for all scope types (no role assignments).
896957
- staff_user and superuser: False for unsupported scope types.
897958
"""

0 commit comments

Comments
 (0)