@@ -590,6 +590,99 @@ def test_migrate_legacy_course_roles_to_authz_and_rollback_with_deletion(self):
590590 # After rollback, we should have 0 UserSubjects related to the course permissions
591591 self .assertEqual (len (state_after_migration_user_subjects ), 0 )
592592
593+ @patch ("openedx_authz.api.data.CourseOverview" , CourseOverview )
594+ def test_migrate_legacy_course_roles_to_authz_and_rollback_with_no_new_role_equivalent (self ):
595+ """Test the migration of legacy course roles to the new Casbin-based model
596+ and the rollback when there is no equivalent new role.
597+ """
598+
599+ # Migrate from legacy CourseAccessRole to new Casbin-based model
600+ permissions_with_errors = migrate_legacy_course_roles_to_authz (CourseAccessRole , delete_after_migration = True )
601+ AuthzEnforcer .get_enforcer ().load_policy ()
602+
603+ # Now let's rollback
604+
605+ # Capture the state of permissions before rollback to verify that rollback restores the original state
606+ original_state_user_subjects = list (
607+ UserSubject .objects .filter (casbin_rules__scope__coursescope__course_overview__isnull = False )
608+ .distinct ()
609+ .order_by ("id" )
610+ .values ("id" , "user_id" )
611+ )
612+ original_state_access_roles = list (
613+ CourseAccessRole .objects .all ().order_by ("id" ).values ("id" , "user_id" , "org" , "course_id" , "role" )
614+ )
615+
616+ # Mock the role_to_legacy_role mapping to only include a mapping
617+ # for COURSE_ADMIN to simulate the scenario where the staff, limited_staff
618+ # and data_researcher roles do not have a legacy role equivalent and
619+ # therefore cannot be migrated back to legacy roles during the rollback.
620+ with patch (
621+ "openedx_authz.engine.utils.role_to_legacy_role" ,
622+ {COURSE_ADMIN .external_key : "instructor" },
623+ ):
624+ permissions_with_errors = migrate_authz_to_legacy_course_roles (
625+ CourseAccessRole , UserSubject , delete_after_migration = True
626+ )
627+
628+ # Check that each user has the expected legacy role after rollback
629+ # and that errors are logged for any permissions that could not be rolled back
630+ for user in self .admin_users :
631+ assignments = get_user_role_assignments_in_scope (
632+ user_external_key = user .username , scope_external_key = self .course_id
633+ )
634+ self .assertEqual (len (assignments ), 0 )
635+ for user in self .staff_users :
636+ assignments = get_user_role_assignments_in_scope (
637+ user_external_key = user .username , scope_external_key = self .course_id
638+ )
639+ # Since we are mocking the role_to_legacy_role mapping to only include a mapping for COURSE_ADMIN,
640+ # the staff role will not have a legacy role equivalent and therefore should not be migrated back
641+ self .assertEqual (len (assignments ), 1 )
642+ for user in self .limited_staff :
643+ assignments = get_user_role_assignments_in_scope (
644+ user_external_key = user .username , scope_external_key = self .course_id
645+ )
646+ # Since we are mocking the role_to_legacy_role mapping to only include a mapping for COURSE_ADMIN,
647+ # the limited_staff role will not have a legacy role equivalent and therefore should not be migrated back
648+ self .assertEqual (len (assignments ), 1 )
649+ for user in self .data_researcher :
650+ assignments = get_user_role_assignments_in_scope (
651+ user_external_key = user .username , scope_external_key = self .course_id
652+ )
653+ # Since we are mocking the role_to_legacy_role mapping to only include a mapping for COURSE_ADMIN,
654+ # the data_researcher role will not have a legacy role equivalent and therefore should not be migrated back
655+ self .assertEqual (len (assignments ), 1 )
656+
657+ # 3 staff + 3 limited_staff + 3 data_researcher = 9 entries with no legacy role equivalent
658+ self .assertEqual (len (permissions_with_errors ), 9 )
659+
660+ state_after_migration_user_subjects = list (
661+ UserSubject .objects .filter (casbin_rules__scope__coursescope__course_overview__isnull = False )
662+ .distinct ()
663+ .order_by ("id" )
664+ .values ("id" , "user_id" )
665+ )
666+ after_migrate_state_access_roles = list (
667+ CourseAccessRole .objects .all ().order_by ("id" ).values ("id" , "user_id" , "org" , "course_id" , "role" )
668+ )
669+
670+ # Before the rollback, we should only have the 1 invalid role entry
671+ # since we set delete_after_migration to True in the migration.
672+ self .assertEqual (len (original_state_access_roles ), 1 )
673+
674+ # All original entries (1) + 3 users * 1 roles = 4
675+ self .assertEqual (len (after_migrate_state_access_roles ), 1 + 3 )
676+
677+ # Before the rollback, we should have the 12 UserSubjects related to the course permissions
678+ # since we had 3 users with 4 roles each in the original state.
679+ self .assertEqual (len (original_state_user_subjects ), 12 )
680+
681+ # After rollback, we should have 9 UserSubjects related to the course permissions
682+ # since the users with staff, limited_staff and data_researcher roles will not be
683+ # migrated back to legacy roles due to our mocked role_to_legacy_role mapping.
684+ self .assertEqual (len (state_after_migration_user_subjects ), 9 )
685+
593686 @patch ("openedx_authz.management.commands.authz_migrate_course_authoring.CourseAccessRole" , CourseAccessRole )
594687 @patch ("openedx_authz.management.commands.authz_migrate_course_authoring.migrate_legacy_course_roles_to_authz" )
595688 def test_authz_migrate_course_authoring_command (self , mock_migrate ):
0 commit comments