33from unittest .mock import Mock , patch
44
55from ddt import data , ddt , unpack
6- from django .test import TestCase
6+ from django .test import TestCase , override_settings
77from opaque_keys .edx .locator import LibraryLocatorV2
88
99from openedx_authz .api .data import (
1010 ActionData ,
1111 ContentLibraryData ,
1212 CourseOverviewData ,
13+ OrgCourseGlobData ,
14+ OrgLibraryGlobData ,
1315 PermissionData ,
1416 RoleAssignmentData ,
1517 RoleData ,
1921 UserData ,
2022)
2123from openedx_authz .constants import permissions , roles
24+ from openedx_authz .tests .stubs .models import ContentLibrary , CourseOverview , Organization
2225
2326
2427@ddt
@@ -233,9 +236,17 @@ def test_scope_data_registration(self):
233236 self .assertIn ("course-v1" , ScopeData .scope_registry )
234237 self .assertIs (ScopeData .scope_registry ["course-v1" ], CourseOverviewData )
235238
239+ # Glob registries for organization-level scopes
240+ self .assertIn ("lib" , ScopeMeta .glob_registry )
241+ self .assertIs (ScopeMeta .glob_registry ["lib" ], OrgLibraryGlobData )
242+ self .assertIn ("course-v1" , ScopeMeta .glob_registry )
243+ self .assertIs (ScopeMeta .glob_registry ["course-v1" ], OrgCourseGlobData )
244+
236245 @data (
237246 ("course-v1^course-v1:WGU+CS002+2025_T1" , CourseOverviewData ),
238247 ("lib^lib:DemoX:CSPROB" , ContentLibraryData ),
248+ ("lib^lib:DemoX*" , OrgLibraryGlobData ),
249+ ("course-v1^course-v1:OpenedX*" , OrgCourseGlobData ),
239250 ("global^generic_scope" , ScopeData ),
240251 )
241252 @unpack
@@ -254,6 +265,8 @@ def test_dynamic_instantiation_via_namespaced_key(self, namespaced_key, expected
254265 @data (
255266 ("course-v1^course-v1:WGU+CS002+2025_T1" , CourseOverviewData ),
256267 ("lib^lib:DemoX:CSPROB" , ContentLibraryData ),
268+ ("lib^lib:DemoX*" , OrgLibraryGlobData ),
269+ ("course-v1^course-v1:OpenedX*" , OrgCourseGlobData ),
257270 ("global^generic" , ScopeData ),
258271 ("unknown^something" , ScopeData ),
259272 )
@@ -273,6 +286,8 @@ def test_get_subclass_by_namespaced_key(self, namespaced_key, expected_class):
273286 @data (
274287 ("course-v1:WGU+CS002+2025_T1" , CourseOverviewData ),
275288 ("lib:DemoX:CSPROB" , ContentLibraryData ),
289+ ("lib:DemoX*" , OrgLibraryGlobData ),
290+ ("course-v1:OpenedX*" , OrgCourseGlobData ),
276291 ("lib:edX:Demo" , ContentLibraryData ),
277292 ("global:generic_scope" , ScopeData ),
278293 )
@@ -654,3 +669,121 @@ def test_exists_returns_false_when_library_does_not_exist(self, mock_content_lib
654669 result = library_scope .exists ()
655670
656671 self .assertFalse (result )
672+
673+
674+ @ddt
675+ @override_settings (OPENEDX_AUTHZ_CONTENT_LIBRARY_MODEL = "content_libraries.ContentLibrary" )
676+ class TestOrgLibraryGlobData (TestCase ):
677+ """Tests for the OrgLibraryGlobData scope."""
678+
679+ @data (
680+ ("lib:DemoX*" , True ),
681+ ("lib:Org-123*" , True ),
682+ ("lib:Org.with.dots*" , True ),
683+ ("lib:Org:With:Colon*" , False ),
684+ ("lib:Org" , False ),
685+ ("other:DemoX*" , False ),
686+ ("lib:DemoX**" , False ),
687+ )
688+ @unpack
689+ def test_validate_external_key (self , external_key , expected_valid ):
690+ """Validate organization-level library glob external keys."""
691+ self .assertEqual (OrgLibraryGlobData .validate_external_key (external_key ), expected_valid )
692+
693+ @data (
694+ ("lib:DemoX*" , "DemoX" ),
695+ ("lib:Org-123*" , "Org-123" ),
696+ ("lib:Org.with.dots*" , "Org.with.dots" ),
697+ ("lib:Org:With:Colon*" , None ),
698+ ("lib:*" , None ),
699+ )
700+ @unpack
701+ def test_get_org (self , external_key , expected_org ):
702+ """Test organization extraction from library glob pattern."""
703+ self .assertEqual (OrgLibraryGlobData .get_org (external_key ), expected_org )
704+
705+ def test_exists_true_when_org_has_libraries_in_db (self ):
706+ """exists() returns True when at least one library with the org exists in the DB."""
707+ org_name = "DemoX"
708+ organization = Organization .objects .create (short_name = org_name )
709+ ContentLibrary .objects .create (org = organization , slug = "testlib" , title = "Test Library" )
710+
711+ result = OrgLibraryGlobData (external_key = f"lib:{ org_name } *" ).exists ()
712+
713+ self .assertTrue (result )
714+
715+ def test_exists_false_when_org_does_not_exist_in_db (self ):
716+ """exists() returns False when the org does not exist in the DB."""
717+ org_name = "DemoX"
718+
719+ result = OrgLibraryGlobData (external_key = f"lib:{ org_name } *" ).exists ()
720+
721+ self .assertFalse (result )
722+
723+ def test_exists_false_when_org_exists_but_no_libraries_in_db (self ):
724+ """exists() returns False when the org exists but no libraries exist in the DB."""
725+ org_name = "DemoX"
726+ Organization .objects .create (short_name = org_name )
727+
728+ result = OrgLibraryGlobData (external_key = f"lib:{ org_name } *" ).exists ()
729+
730+ self .assertFalse (result )
731+
732+
733+ @ddt
734+ @override_settings (OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL = "course_overviews.CourseOverview" )
735+ class TestOrgCourseGlobData (TestCase ):
736+ """Tests for the OrgCourseGlobData scope."""
737+
738+ @data (
739+ ("course-v1:OpenedX*" , True ),
740+ ("course-v1:My-Org_1*" , True ),
741+ ("course-v1:Org.with.dots*" , True ),
742+ ("course-v1:Org:With:Plus*" , False ),
743+ ("course-v1:OpenedX" , False ),
744+ ("other:OpenedX*" , False ),
745+ ("course-v1:OpenedX**" , False ),
746+ )
747+ @unpack
748+ def test_validate_external_key (self , external_key , expected_valid ):
749+ """Validate organization-level course glob external keys."""
750+ self .assertEqual (OrgCourseGlobData .validate_external_key (external_key ), expected_valid )
751+
752+ @data (
753+ ("course-v1:OpenedX*" , "OpenedX" ),
754+ ("course-v1:My-Org_1*" , "My-Org_1" ),
755+ ("course-v1:Org.with.dots*" , "Org.with.dots" ),
756+ ("course-v1:Org:With:Plus*" , None ),
757+ ("course-v1:*" , None ),
758+ )
759+ @unpack
760+ def test_get_org (self , external_key , expected_org ):
761+ """Test organization extraction from course glob pattern."""
762+ self .assertEqual (OrgCourseGlobData .get_org (external_key ), expected_org )
763+
764+ def test_exists_true_when_org_has_courses (self ):
765+ """exists() returns True when at least one course with the org exists."""
766+ org_name = "OpenedX"
767+ Organization .objects .create (short_name = org_name )
768+ CourseOverview .objects .create (org = org_name , display_name = "Test Course" )
769+
770+ result = OrgCourseGlobData (external_key = f"course-v1:{ org_name } *" ).exists ()
771+
772+ self .assertTrue (result )
773+
774+ def test_exists_false_when_org_does_not_exist (self ):
775+ """exists() returns False when the org does not exist."""
776+ org_name = "OpenedX"
777+
778+ result = OrgCourseGlobData (external_key = f"course-v1:{ org_name } *" ).exists ()
779+
780+ self .assertFalse (result )
781+
782+ def test_exists_false_when_org_exists_but_no_courses (self ):
783+ """exists() returns False when the org exists but no courses exist."""
784+ org_name = "OpenedX"
785+ Organization .objects .create (short_name = org_name )
786+
787+ result = OrgCourseGlobData (external_key = f"course-v1:{ org_name } *" ).exists ()
788+
789+ self .assertFalse (result )
0 commit comments