From 73cb052158b605712a8fcff204a022ee7e3f9646 Mon Sep 17 00:00:00 2001 From: Segun Babalola Date: Tue, 12 Jul 2022 18:53:46 +0100 Subject: [PATCH] MDL-65061 enrol: Add suspend and other behaviours to IMS enrol plugin Change-Id: Ia169a8c1dae9c6132d40158ac2a3e346dc3f5e1e Co-authored-by: Sarah Cotton Co-authored-by: Mark Johnson --- .../lang/en/enrol_imsenterprise.php | 6 + enrol/imsenterprise/lib.php | 79 +- enrol/imsenterprise/settings.php | 15 + .../tests/imsenterprise_unenrol_test.php | 676 ++++++++++++++++++ 4 files changed, 767 insertions(+), 9 deletions(-) create mode 100644 enrol/imsenterprise/tests/imsenterprise_unenrol_test.php diff --git a/enrol/imsenterprise/lang/en/enrol_imsenterprise.php b/enrol/imsenterprise/lang/en/enrol_imsenterprise.php index d9c1ada9aa5..0097964896e 100644 --- a/enrol/imsenterprise/lang/en/enrol_imsenterprise.php +++ b/enrol/imsenterprise/lang/en/enrol_imsenterprise.php @@ -93,3 +93,9 @@ $string['roles'] = 'Roles'; $string['ignore'] = 'Ignore'; $string['importimsfile'] = 'Import IMS Enterprise file'; $string['privacy:metadata'] = 'The IMS Enterprise file enrolment plugin does not store any personal data.'; +$string['unenrolaction'] = 'Unenrol action'; +$string['unenrolaction_desc'] = 'Action to be taken when a user is unenrolled from a course (only applied when the unenrol setting above is active)'; +$string['disableenrolonly'] = 'Suspend enrolment only'; +$string['disableenrolmentandremoveallroles'] = 'Suspend enrolment & remove roles'; +$string['noaction'] = 'No action'; +$string['removeenrolmentandallroles'] = 'Delete enrolment & roles'; diff --git a/enrol/imsenterprise/lib.php b/enrol/imsenterprise/lib.php index 3b35a3b74c3..6d1d174eba1 100644 --- a/enrol/imsenterprise/lib.php +++ b/enrol/imsenterprise/lib.php @@ -584,7 +584,11 @@ class enrol_imsenterprise_plugin extends enrol_plugin { $this->log_line("Created user record ('.$id.') for user '$person->username' (ID number $person->idnumber)."); } } else if ($createnewusers) { - $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber)."); + + $username = $person->username ?? "[unknown username]"; + $personnumber = $person->idnumber ?? "[unknown ID number]"; + + $this->log_line("User record already exists for user '" . $username . "' (ID number " . $personnumber . ")."); // It is totally wrong to mess with deleted users flag directly in database!!! // There is no official way to undelete user, sorry.. @@ -683,10 +687,12 @@ class enrol_imsenterprise_plugin extends enrol_plugin { // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to. // Zero means this roletype is supposed to be skipped. - $moodleroleid = $this->rolemappings[$member->roletype]; + $moodleroleid = (isset($member->roletype) && isset($this->rolemappings[$member->roletype])) + ? $this->rolemappings[$member->roletype] : null; if (!$moodleroleid) { - $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid " - ."($member->idnumber) in course $memberstoreobj->course"); + $this->log_line("SKIPPING role " . + ($member->roletype ?? "[]") . " for $memberstoreobj->userid " . + "($member->idnumber) in course $memberstoreobj->course"); continue; } @@ -741,25 +747,80 @@ class enrol_imsenterprise_plugin extends enrol_plugin { } else if ($this->get_config('imsunenrol')) { // Unenrol member. + $unenrolsetting = $this->get_config('unenrolaction'); $einstances = $DB->get_records('enrol', array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id)); - foreach ($einstances as $einstance) { - // Unenrol the user from all imsenterprise enrolment instances. - $this->unenrol_user($einstance, $memberstoreobj->userid); + + switch ($unenrolsetting) { + case ENROL_EXT_REMOVED_SUSPEND: + case ENROL_EXT_REMOVED_SUSPENDNOROLES: { + foreach ($einstances as $einstance) { + $this->update_user_enrol($einstance, $memberstoreobj->userid, + ENROL_USER_SUSPENDED, $timeframe->begin, $timeframe->end); + + $this->log_line("Suspending user enrolment for $member->idnumber in " . + " course $ship->coursecode "); + + if (intval($unenrolsetting) === intval(ENROL_EXT_REMOVED_SUSPENDNOROLES)) { + if (!$context = + context_course::instance($courseobj->id, IGNORE_MISSING)) { + + $this->log_line("Unable to process IMS unenrolment request " . + " because course context not found. User: " . + "#$memberstoreobj->userid ($member->idnumber) , " . + " course: $memberstoreobj->course"); + } else { + + role_unassign_all([ + 'contextid' => $context->id, + 'userid' => $memberstoreobj->userid, + 'component' => 'enrol_imsenterprise', + 'itemid' => $einstance->id + ]); + + $this->log_line("Removing role assignments for user " . + "$member->idnumber from role $moodleroleid in course " . + "$ship->coursecode "); + } + } + } + } + break; + + case ENROL_EXT_REMOVED_UNENROL: { + foreach ($einstances as $einstance) { + $this->unenrol_user($einstance, $memberstoreobj->userid); + $this->log_line("Removing user enrolment record for $member->idnumber " . + " in course $ship->coursecode "); + } + } + break; + + case ENROL_EXT_REMOVED_KEEP: { + $this->log_line("Processed KEEP IMS unenrol instruction (i.e. do nothing)"); + } + break; + + default: + $this->log_line("Unable to process IMS unenrolment request because " . + " the value set for plugin parameter, unenrol action, is not recognised. " . + " User: #$memberstoreobj->userid ($member->idnumber) " . + " , course: $memberstoreobj->course"); + break; } $membersuntally++; - $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course"); } } } $this->log_line("Added $memberstally users to course $ship->coursecode"); if ($membersuntally > 0) { - $this->log_line("Removed $membersuntally users from course $ship->coursecode"); + $this->log_line("Processed $membersuntally unenrol instructions for course $ship->coursecode"); } } + } // End process_membership_tag(). /** diff --git a/enrol/imsenterprise/settings.php b/enrol/imsenterprise/settings.php index d55b8ee5499..a906a721815 100644 --- a/enrol/imsenterprise/settings.php +++ b/enrol/imsenterprise/settings.php @@ -111,6 +111,21 @@ if ($ADMIN->fulltree) { $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsunenrol', get_string('allowunenrol', 'enrol_imsenterprise'), get_string('allowunenrol_desc', 'enrol_imsenterprise'), 0)); + /* Action to take when a request to remove a user enrolment record is detected in the IMS file */ + $options = [ + ENROL_EXT_REMOVED_KEEP => get_string('noaction', 'enrol_imsenterprise'), + ENROL_EXT_REMOVED_UNENROL => get_string('removeenrolmentandallroles', 'enrol_imsenterprise'), + ENROL_EXT_REMOVED_SUSPEND => get_string('disableenrolonly', 'enrol_imsenterprise'), + ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('disableenrolmentandremoveallroles', 'enrol_imsenterprise'), + ]; + + $settings->add( + new admin_setting_configselect('enrol_imsenterprise/unenrolaction', + get_string('unenrolaction', 'enrol_imsenterprise'), + get_string('unenrolaction_desc', 'enrol_imsenterprise'), + ENROL_EXT_REMOVED_UNENROL, $options) + ); + if (!during_initial_install()) { $imscourses = new imsenterprise_courses(); foreach ($imscourses->get_courseattrs() as $courseattr) { diff --git a/enrol/imsenterprise/tests/imsenterprise_unenrol_test.php b/enrol/imsenterprise/tests/imsenterprise_unenrol_test.php new file mode 100644 index 00000000000..4c41962822d --- /dev/null +++ b/enrol/imsenterprise/tests/imsenterprise_unenrol_test.php @@ -0,0 +1,676 @@ +. + +namespace enrol_imsenterprise; + +use core_course_category; +use enrol_imsenterprise_plugin; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/enrol/imsenterprise/locallib.php'); +require_once($CFG->dirroot . '/enrol/imsenterprise/lib.php'); + +/** + * IMS Enterprise test case + * + * @package enrol_imsenterprise + * @category test + * @copyright 2019 Segun Babalola + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \enrol_imsenterprise_plugin + */ +class imsenterprise_unenrol_test extends \advanced_testcase { + + /** + * @var $imsplugin enrol_imsenterprise_plugin IMS plugin instance. + */ + public $imsplugin; + + /** + * Setup required for all tests. + */ + protected function setUp(): void { + $this->resetAfterTest(true); + $this->imsplugin = enrol_get_plugin('imsenterprise'); + $this->set_test_config(); + } + + /** + * Sets the plugin configuration for testing + */ + public function set_test_config() { + $this->imsplugin->set_config('mailadmins', false); + $this->imsplugin->set_config('prev_path', ''); + $this->imsplugin->set_config('createnewusers', true); + $this->imsplugin->set_config('imsupdateusers', true); + $this->imsplugin->set_config('createnewcourses', true); + $this->imsplugin->set_config('updatecourses', true); + $this->imsplugin->set_config('createnewcategories', true); + $this->imsplugin->set_config('categoryseparator', ''); + $this->imsplugin->set_config('categoryidnumber', false); + $this->imsplugin->set_config('nestedcategories', false); + } + + + /** + * Creates an IMS enterprise XML file and adds it's path to config settings. + * + * @param bool|array $users false or array of users StdClass + * @param bool|array $courses false or of courses StdClass + * @param bool|array $usercoursemembership false or of courses StdClass + */ + public function set_xml_file($users = false, $courses = false, $usercoursemembership = false) { + + $xmlcontent = ''; + + // Users. + if (!empty($users) && is_array($users)) { + foreach ($users as $user) { + $xmlcontent .= 'recstatus) ? ' recstatus="'.$user->recstatus.'"' : '').'>'; + $xmlcontent .= 'TestSource'.$user->idnumber.''; + $xmlcontent .= 'auth) ? ' authenticationtype="'.$user->auth.'"' : ''); + $xmlcontent .= '>'.$user->username.''; + $xmlcontent .= '' + .''.$user->firstname.' '.$user->lastname.'' + .''.$user->lastname.''.$user->firstname.'' + .'' + .''.$user->email.''; + $xmlcontent .= ''; + } + } + + // Courses. + // Mapping based on default course attributes - IMS group tags mapping. + if (!empty($courses) && is_array($courses)) { + foreach ($courses as $course) { + + $xmlcontent .= 'recstatus) ? ' recstatus="'.$course->recstatus.'"' : '').'>'; + $xmlcontent .= 'TestSource'.$course->idnumber.''; + $xmlcontent .= ''.(!empty($course->imsshort) ? ''.$course->imsshort.'' : ''); + $xmlcontent .= (!empty($course->imslong) ? ''.$course->imslong.'' : ''); + $xmlcontent .= (!empty($course->imsfull) ? ''.$course->imsfull.'' : ''); + $xmlcontent .= ''; + + // The orgunit tag value is used by moodle as category name. + $xmlcontent .= ''; + + // Optional category name. + if (isset($course->category)) { + if (is_array($course->category)) { + foreach ($course->category as $category) { + $xmlcontent .= '' . $category . ''; + } + } else if (is_object($course->category)) { + $xmlcontent .= '' . $course->category->name . ''; + } else if (!empty($course->category)) { + $xmlcontent .= '' . $course->category . ''; + } + } + + $xmlcontent .= ''; + $xmlcontent .= ''; + } + } + + // User course membership (i.e. roles and enrolments). + if (!empty($usercoursemembership) && is_array($usercoursemembership)) { + foreach ($usercoursemembership as $crsemship) { + + // Only process records that have a source/id (i.e. course code) set in the IMS file. + // Note that we could also check that there is a corresponding $course with the course code given here, + // however it is possible that we want to test the behaviour of orphan membership elements in future, + // so leaving the check out for now. + if (isset($crsemship->crseidnumber) && isset($crsemship->member) && is_array($crsemship->member) + && count($crsemship->member)) { + $xmlcontent .= 'TestSource' + .$crsemship->crseidnumber . ''; + + foreach ($crsemship->member as $crsemember) { + if (!empty($crsemember->useridnumber)) { + $xmlcontent .= ''; + $xmlcontent .= 'TestSource'. $crsemember->useridnumber + .''; + + // Indicates whether the member is a Person (1) or another Group (2). + // We're only handling user membership here, so hard-code value of 1. + $xmlcontent .= '1'; + + if (isset($crsemember->role) && is_array($crsemember->role)) { + foreach ($crsemember->role as $role) { + $xmlcontent .= ''; + $xmlcontent .= ''; + $xmlcontent .= '' . $role->rolestatus . ''; + $xmlcontent .= ''; + } + } + + $xmlcontent .= ''; + } + } + + $xmlcontent .= ''; + } + } + } + + $xmlcontent .= ''; + + // Creating the XML file. + $filename = 'ims_' . rand(1000, 9999) . '.xml'; + $tmpdir = make_temp_directory('enrol_imsenterprise'); + $xmlfilepath = $tmpdir . '/' . $filename; + file_put_contents($xmlfilepath, $xmlcontent); + + // Setting the file path in CFG. + $this->imsplugin->set_config('imsfilelocation', $xmlfilepath); + } + + /** + * Utility function for generating test user records + * + * @param int $numberofrecordsrequired - number of test users required + * @return array of StdClass objects representing test user records + */ + private function generate_test_user_records($numberofrecordsrequired) { + $users = []; + for ($i = 0; $i < $numberofrecordsrequired; $i++) { + $usernumber = $i + 101; + $users[] = (object)[ + 'recstatus' => enrol_imsenterprise_plugin::IMSENTERPRISE_ADD, + 'idnumber' => $usernumber, + 'username' => 'UID' .$usernumber, + 'email' => 'user' . $usernumber . '@moodle.org', + 'firstname' => 'User' . $usernumber . ' firstname', + 'lastname' => 'User' . $usernumber . ' lastname' + ]; + } + + return $users; + } + + /** + * Utility function for generating test course records + * + * @param int $numberofrecordsrequired - number of test course records required + * @return array of StdClass objects representing test course records + */ + private function generate_test_course_records($numberofrecordsrequired) { + $courses = []; + for ($i = 0; $i < $numberofrecordsrequired; $i++) { + $coursenumber = $i + 101; + $courses[] = (object)[ + 'recstatus' => enrol_imsenterprise_plugin::IMSENTERPRISE_ADD, + 'idnumber' => 'CID' . $coursenumber, + 'imsshort' => 'Course ' . $coursenumber, + 'category' => core_course_category::get_default() + ]; + } + + return $courses; + } + + /** + * Utility function for generating test membership structure for given users and courses. + * Linkmatrix is expected to be in [row, col] format, where courses are rows and users are columns. + * Each element of the link matrix is expected to contain ::. + * + * @param array $users + * @param array $courses + * @param array $linkmatrix - matrix/two dimensional array of required user course enrolments + * @return array + */ + private function link_users_with_courses($users, $courses, $linkmatrix) { + + $memberships = []; + + foreach ($courses as $i => $c) { + + $membership = new stdClass(); + $membership->member = []; + $membership->crseidnumber = $c->idnumber; + + foreach ($users as $j => $u) { + if (isset($linkmatrix[$i][$j])) { + + list($roletype, $rolestatus, $rolerecstatus) = explode(':', $linkmatrix[$i][$j]); + + if (strlen($rolerecstatus) && strlen($roletype) && strlen($rolestatus)) { + $membership->member[] = (object)[ + 'useridnumber' => $u->idnumber, + 'role' => [(object)[ + 'roletype' => $roletype, + 'rolestatus' => $rolestatus, + 'recstatus' => $rolerecstatus + ]] + ]; + } + } + } + + $memberships[] = $membership; + } + + return $memberships; + } + + /** + * Add new users, courses and enrolments + */ + public function test_users_are_enroled_on_courses() { + global $DB; + + $prevnuserenrolments = $DB->count_records('user_enrolments'); + $prevnusers = $DB->count_records('user'); + $prevncourses = $DB->count_records('course'); + + $courses = $this->generate_test_course_records(1); + $users = $this->generate_test_user_records(1); + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + [ + ['01:1:1'] // First course. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + } + + /** + * Check that the unenrol actions are completely ignored when "unenrol" setting is disabled + */ + public function test_no_action_when_unenrol_disabled() { + global $DB; + + $prevnuserenrolments = $DB->count_records('user_enrolments'); + $prevnusers = $DB->count_records('user'); + $prevncourses = $DB->count_records('course'); + + // Create user and course. + $courses = $this->generate_test_course_records(3); + $users = $this->generate_test_user_records(2); + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:1:1', '01:1:1'], // Course 1. + ['01:1:1', '01:1:1'], // Course 2. + ['::', '01:1:1'], // Course 3. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevnuserenrolments + 5), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnusers + 2), $DB->count_records('user')); + $this->assertEquals(($prevncourses + 3), $DB->count_records('course')); + + // Disallow unenrolment, and check that unenroling has no effect. + $this->imsplugin->set_config('imsunenrol', 0); + + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:1:3', '01:1:3'], // Course 1. + ['::', '01:1:3'], // Course 2. + ['::', '01:1:3'], // Course 3. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevnuserenrolments + 5), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnusers + 2), $DB->count_records('user')); + $this->assertEquals(($prevncourses + 3), $DB->count_records('course')); + } + + /** + * When a user has existing roles and enrolments, they are unaffected by IMS instructions for other courses + */ + public function test_existing_roles_and_enrolments_unaffected() { + + global $DB; + + $this->imsplugin->set_config('imsunenrol', 1); + $this->imsplugin->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); + + $prevnuserenrolments = $DB->count_records('user_enrolments'); + $prevnusers = $DB->count_records('user'); + $prevncourses = $DB->count_records('course'); + + $courses = $this->generate_test_course_records(2); + + // Create_course seems to expect the category to be passed as ID, so extract from the object. + $course1 = $courses[0]; + $course1->category = $course1->category->id; + $course1 = $this->getDataGenerator()->create_course($courses[0]); + + // Enrol user1 on course1. + $DB->insert_record('enrol', (object)['enrol' => 'imsenterprise', + 'courseid' => $course1->id, 'status' => 1, 'roleid' => 5 + ], true); + + $user1 = $this->getDataGenerator()->create_and_enrol($course1, 'student', + ['idnumber' => 'UserIDNumber100'], 'imsenterprise'); + $user1->username = $user1->idnumber; + + // Confirm user was added and that the enrolment happened. + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + + // Capture DB id of enrolment record. + $initialusernerolment = $DB->get_record('user_enrolments', ['userid' => $user1->id], + '*', MUST_EXIST); + $initialroleassigned = $DB->get_record('role_assignments', ['userid' => $user1->id], + '*', MUST_EXIST); + + // Add a new enrolment for the same user via IMS file. + $coursemembership = $this->link_users_with_courses( + [$user1], + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['::'], // Course 1. + ['01:1:1'], // Course 2. + ] + ); + + $this->set_xml_file([$user1], $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(2, $DB->count_records('user_enrolments', ['userid' => $user1->id])); + $this->assertEquals(($prevncourses + 2), $DB->count_records('course')); + + // Unenrol the user from course2 via IMS file. + $coursemembership = $this->link_users_with_courses( + [$user1], + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['::'], // Course 1. + ['01:0:3'], // Course 2. + ] + ); + + $this->set_xml_file([$user1], $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(1, $DB->count_records('user_enrolments', ['userid' => $user1->id])); + $this->assertTrue($DB->record_exists('user_enrolments', ['id' => $initialusernerolment->id, + 'userid' => $initialusernerolment->userid])); + $this->assertTrue($DB->record_exists('role_assignments', ['id' => $initialroleassigned->id, + 'userid' => $initialusernerolment->userid])); + } + + /** + * Enrolments alone are disabled + */ + public function test_disable_enrolments_only() { + + global $DB; + + $this->imsplugin->set_config('imsunenrol', 1); + $this->imsplugin->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND); + + $prevnuserenrolments = $DB->count_records('user_enrolments'); + $prevnroles = $DB->count_records('role_assignments'); + $prevnusers = $DB->count_records('user'); + $prevncourses = $DB->count_records('course'); + + $courses = $this->generate_test_course_records(1); + $users = $this->generate_test_user_records(1); + + // Add a new enrolment for the same user via IMS file. + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:1:1'], // Course 1. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnroles + 1), $DB->count_records('role_assignments')); + + // Capture DB ids. + $dbuser = $DB->get_record('user', ['idnumber' => $users[0]->idnumber], '*', MUST_EXIST); + + $dbenrolment = $DB->get_record('user_enrolments', + ['userid' => $dbuser->id, 'status' => ENROL_USER_ACTIVE], + '*', MUST_EXIST + ); + + $dbrole = $DB->get_record('role_assignments', ['userid' => $dbuser->id], '*', MUST_EXIST); + + // Unenrol the user, check that the enrolment and role exist, but the enrolment is suspended. + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:0:3'], // Course 1. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnroles + 1), $DB->count_records('role_assignments')); + + $this->assertEquals(1, $DB->count_records('user_enrolments', + ['userid' => $dbuser->id, 'id' => $dbenrolment->id, 'status' => ENROL_USER_SUSPENDED])); + + $this->assertEquals(1, $DB->count_records('role_assignments', + ['userid' => $dbuser->id, 'id' => $dbrole->id])); + } + + /** + * Enrolments are disabled but retained) and roles removed + */ + public function test_disable_enrolments_and_remove_roles() { + + global $DB; + + $this->imsplugin->set_config('imsunenrol', 1); + $this->imsplugin->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); + + $prevnuserenrolments = $DB->count_records('user_enrolments'); + $prevnroles = $DB->count_records('role_assignments'); + $prevnusers = $DB->count_records('user'); + $prevncourses = $DB->count_records('course'); + + $courses = $this->generate_test_course_records(1); + $users = $this->generate_test_user_records(1); + + // Add a new enrolment for the same user via IMS file. + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:1:1'], // Course 1. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnroles + 1), $DB->count_records('role_assignments')); + + // Capture DB ids. + $dbuser = $DB->get_record('user', ['idnumber' => $users[0]->idnumber], '*', MUST_EXIST); + + $dbenrolment = $DB->get_record('user_enrolments', + ['userid' => $dbuser->id, 'status' => ENROL_USER_ACTIVE], + '*', MUST_EXIST + ); + + $dbrole = $DB->get_record('role_assignments', ['userid' => $dbuser->id], '*', MUST_EXIST); + + // Unenrol the user, check that the enrolment and role exist, but the enrolment is suspended. + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:0:3'], // Course 1. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnroles), $DB->count_records('role_assignments')); + + $this->assertEquals(1, $DB->count_records('user_enrolments', + ['userid' => $dbuser->id, 'id' => $dbenrolment->id, 'status' => ENROL_USER_SUSPENDED])); + + $this->assertEquals(0, $DB->count_records('role_assignments', + ['userid' => $dbuser->id, 'id' => $dbrole->id])); + + } + + /** + * Enrolments and roles are deleted for specified user + */ + public function test_delete_roles_and_enrolments() { + + global $DB; + + $this->imsplugin->set_config('imsunenrol', 1); + $this->imsplugin->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); + + $prevnuserenrolments = $DB->count_records('user_enrolments'); + $prevnroles = $DB->count_records('role_assignments'); + $prevnusers = $DB->count_records('user'); + $prevncourses = $DB->count_records('course'); + + $courses = $this->generate_test_course_records(1); + $users = $this->generate_test_user_records(1); + + // Add a new enrolment for the same user via IMS file. + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:1:1'], // Course 1. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevnuserenrolments + 1), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnroles + 1), $DB->count_records('role_assignments')); + + // Capture DB ids. + $dbuser = $DB->get_record('user', ['idnumber' => $users[0]->idnumber], '*', MUST_EXIST); + + $dbenrolment = $DB->get_record('user_enrolments', + ['userid' => $dbuser->id, 'status' => ENROL_USER_ACTIVE], + '*', MUST_EXIST + ); + + $dbrole = $DB->get_record('role_assignments', ['userid' => $dbuser->id], '*', MUST_EXIST); + + // Unenrol the user, check that the enrolment and role exist, but the enrolment is suspended. + $coursemembership = $this->link_users_with_courses( + $users, + $courses, + // Role types: 01=Learner, 02=Instructor, 03=Content Dev, 04=Member, 05=Manager, 06=Mentor, 07=Admin, 08=TA. + // Role statuses: 0=Inactive, 1=Active. + // Role recstatus: 1=Add, 2=Update, 3=Delete. + // Format of matrix elements: ::. + [ + ['01:1:3'], // Course 1. + ] + ); + + $this->set_xml_file($users, $courses, $coursemembership); + $this->imsplugin->cron(); + + $this->assertEquals(($prevncourses + 1), $DB->count_records('course')); + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals(($prevnuserenrolments), $DB->count_records('user_enrolments')); + $this->assertEquals(($prevnroles), $DB->count_records('role_assignments')); + + $this->assertEquals(0, $DB->count_records('user_enrolments', + ['userid' => $dbuser->id, 'id' => $dbenrolment->id, 'status' => ENROL_USER_SUSPENDED])); + + $this->assertEquals(0, $DB->count_records('role_assignments', + ['userid' => $dbuser->id, 'id' => $dbrole->id])); + } +}