@@ -325,6 +325,43 @@ def test_scope_validate_external_key(self, external_key, expected_valid, expecte
325325
326326 self .assertEqual (result , expected_valid )
327327
328+ def test_get_subclass_by_external_key_unknown_scope_raises_value_error (self ):
329+ """Unknown namespace should raise ValueError in get_subclass_by_external_key."""
330+ with self .assertRaises (ValueError ):
331+ ScopeMeta .get_subclass_by_external_key ("unknown:DemoX" )
332+
333+ def test_get_subclass_by_external_key_invalid_format_raises_value_error (self ):
334+ """Invalid format (fails subclass.validate_external_key) should raise ValueError."""
335+ with self .assertRaises (ValueError ):
336+ ScopeMeta .get_subclass_by_external_key ("lib:invalid_library_key" )
337+
338+ def test_scope_meta_initializes_registries_when_missing (self ):
339+ """ScopeMeta should create registries if they don't exist on initialization.
340+
341+ This validates the defensive branch in ScopeMeta.__init__ that initializes
342+ scope_registry and glob_registry when they are not present on the class.
343+ """
344+ original_scope_registry = ScopeMeta .scope_registry
345+ original_glob_registry = ScopeMeta .glob_registry
346+
347+ try :
348+ # Simulate an environment where the registries are not yet defined
349+ del ScopeMeta .scope_registry
350+ del ScopeMeta .glob_registry
351+
352+ class TempScope (ScopeData ):
353+ NAMESPACE = "temp"
354+
355+ # Metaclass should have recreated the registries on the class
356+ self .assertTrue (hasattr (TempScope , "scope_registry" ))
357+ self .assertTrue (hasattr (TempScope , "glob_registry" ))
358+ # And the new scope should be registered under its namespace
359+ self .assertIs (TempScope .scope_registry .get ("temp" ), TempScope )
360+ finally :
361+ # Restore original registries to avoid side effects on other tests
362+ ScopeMeta .scope_registry = original_scope_registry
363+ ScopeMeta .glob_registry = original_glob_registry
364+
328365 def test_direct_subclass_instantiation_bypasses_metaclass (self ):
329366 """Test that direct subclass instantiation doesn't trigger metaclass logic.
330367
@@ -680,6 +717,17 @@ class TestOrgLibraryGlobData(TestCase):
680717 ("lib:DemoX:*" , True ),
681718 ("lib:Org-123:*" , True ),
682719 ("lib:Org.with.dots:*" , True ),
720+ ("lib:Org With Space:*" , False ),
721+ ("lib:Org/With/Slash:*" , False ),
722+ ("lib:Org\\ With\\ Backslash:*" , False ),
723+ ("lib:Org,With,Comma:*" , False ),
724+ ("lib:Org;With;Semicolon:*" , False ),
725+ ("lib:Org@WithAt:*" , False ),
726+ ("lib:Org#WithHash:*" , False ),
727+ ("lib:Org$WithDollar:*" , False ),
728+ ("lib:Org&WithAmp:*" , False ),
729+ ("lib:Org+WithPlus:*" , False ),
730+ ("lib:(Org):*" , False ),
683731 ("lib:Org" , False ),
684732 ("other:DemoX:*" , False ),
685733 ("lib:DemoX:*:*" , False ),
@@ -695,6 +743,8 @@ def test_validate_external_key(self, external_key, expected_valid):
695743 ("lib:Org.with.dots:*" , "Org.with.dots" ),
696744 ("lib:Org:With:Colon:*" , None ),
697745 ("lib:*" , None ),
746+ ("lib:DemoX" , None ),
747+ ("lib:DemoX:*:*" , None ),
698748 )
699749 @unpack
700750 def test_get_org (self , external_key , expected_org ):
@@ -728,6 +778,13 @@ def test_exists_false_when_org_exists_but_no_libraries_in_db(self):
728778
729779 self .assertFalse (result )
730780
781+ def test_exists_false_when_org_cannot_be_parsed (self ):
782+ """exists() returns False when org property is None (invalid pattern)."""
783+ scope = OrgLibraryGlobData (external_key = "lib:Org:With:Colon:*" )
784+
785+ self .assertIsNone (scope .org )
786+ self .assertFalse (scope .exists ())
787+
731788
732789@ddt
733790@override_settings (OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL = "course_overviews.CourseOverview" )
@@ -738,6 +795,17 @@ class TestOrgCourseGlobData(TestCase):
738795 ("course-v1:OpenedX+*" , True ),
739796 ("course-v1:My-Org_1+*" , True ),
740797 ("course-v1:Org.with.dots+*" , True ),
798+ ("course-v1:Org With Space+*" , False ),
799+ ("course-v1:Org/With/Slash+*" , False ),
800+ ("course-v1:Org\\ With\\ Backslash+*" , False ),
801+ ("course-v1:Org,With,Comma+*" , False ),
802+ ("course-v1:Org;With;Semicolon+*" , False ),
803+ ("course-v1:Org@WithAt+*" , False ),
804+ ("course-v1:Org#WithHash+*" , False ),
805+ ("course-v1:Org$WithDollar+*" , False ),
806+ ("course-v1:Org&WithAmp+*" , False ),
807+ ("course-v1:Org+WithPlus+*" , False ),
808+ ("course-v1:(Org)+*" , False ),
741809 ("course-v1:Org:With:Plus+*" , False ),
742810 ("course-v1:OpenedX" , False ),
743811 ("other:OpenedX+*" , False ),
@@ -786,3 +854,10 @@ def test_exists_false_when_org_exists_but_no_courses(self):
786854 result = OrgCourseGlobData (external_key = f"course-v1:{ org_name } +*" ).exists ()
787855
788856 self .assertFalse (result )
857+
858+ def test_exists_false_when_org_cannot_be_parsed (self ):
859+ """exists() returns False when org property is None (invalid pattern)."""
860+ scope = OrgCourseGlobData (external_key = "course-v1:Org:With:Colon+*" )
861+
862+ self .assertIsNone (scope .org )
863+ self .assertFalse (scope .exists ())
0 commit comments