1<?php 2// This file is part of Moodle - http://moodle.org/ 3// 4// Moodle is free software: you can redistribute it and/or modify 5// it under the terms of the GNU General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// Moodle is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU General Public License for more details. 13// 14// You should have received a copy of the GNU General Public License 15// along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17/** 18 * Course copy tests. 19 * 20 * @package core_backup 21 * @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/> 22 * @author Matt Porritt <mattp@catalyst-au.net> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25defined('MOODLE_INTERNAL') || die(); 26 27global $CFG; 28require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); 29require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 30require_once($CFG->libdir . '/completionlib.php'); 31 32/** 33 * Course copy tests. 34 * 35 * @package core_backup 36 * @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/> 37 * @author Matt Porritt <mattp@catalyst-au.net> 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40class core_backup_course_copy_testcase extends advanced_testcase { 41 42 /** 43 * 44 * @var \stdClass Course used for testing. 45 */ 46 protected $course; 47 48 /** 49 * 50 * @var int User used to perform backups. 51 */ 52 protected $userid; 53 54 /** 55 * 56 * @var array Ids of users in test course. 57 */ 58 protected $courseusers; 59 60 /** 61 * 62 * @var array Names of the created activities. 63 */ 64 protected $activitynames; 65 66 /** 67 * Set up tasks for all tests. 68 */ 69 protected function setUp() { 70 global $DB, $CFG, $USER; 71 72 $this->resetAfterTest(true); 73 74 $CFG->enableavailability = true; 75 $CFG->enablecompletion = true; 76 77 // Create a course with some availability data set. 78 $generator = $this->getDataGenerator(); 79 $course = $generator->create_course( 80 array('format' => 'topics', 'numsections' => 3, 81 'enablecompletion' => COMPLETION_ENABLED), 82 array('createsections' => true)); 83 $forum = $generator->create_module('forum', array( 84 'course' => $course->id)); 85 $forum2 = $generator->create_module('forum', array( 86 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); 87 88 // We need a grade, easiest is to add an assignment. 89 $assignrow = $generator->create_module('assign', array( 90 'course' => $course->id)); 91 $assign = new assign(context_module::instance($assignrow->cmid), false, false); 92 $item = $assign->get_grade_item(); 93 94 // Make a test grouping as well. 95 $grouping = $generator->create_grouping(array('courseid' => $course->id, 96 'name' => 'Grouping!')); 97 98 // Create some users. 99 $user1 = $generator->create_user(); 100 $user2 = $generator->create_user(); 101 $user3 = $generator->create_user(); 102 $user4 = $generator->create_user(); 103 $this->courseusers = array( 104 $user1->id, $user2->id, $user3->id, $user4->id 105 ); 106 107 // Enrol users into the course. 108 $generator->enrol_user($user1->id, $course->id, 'student'); 109 $generator->enrol_user($user2->id, $course->id, 'editingteacher'); 110 $generator->enrol_user($user3->id, $course->id, 'manager'); 111 $generator->enrol_user($user4->id, $course->id, 'editingteacher'); 112 $generator->enrol_user($user4->id, $course->id, 'manager'); 113 114 $availability = '{"op":"|","show":false,"c":[' . 115 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . 116 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . 117 '{"type":"grouping","id":' . $grouping->id . '}' . 118 ']}'; 119 $DB->set_field('course_modules', 'availability', $availability, array( 120 'id' => $forum->cmid)); 121 $DB->set_field('course_sections', 'availability', $availability, array( 122 'course' => $course->id, 'section' => 1)); 123 124 // Add some user data to the course. 125 $discussion = $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id, 126 'forum' => $forum->id, 'userid' => $user1->id, 'timemodified' => time(), 127 'name' => 'Frog']); 128 $generator->get_plugin_generator('mod_forum')->create_post(['discussion' => $discussion->id, 'userid' => $user1->id]); 129 130 $this->course = $course; 131 $this->userid = $USER->id; // Admin. 132 $this->activitynames = array( 133 $forum->name, 134 $forum2->name, 135 $assignrow->name 136 ); 137 138 // Set the user doing the backup to be a manager in the course. 139 // By default Managers can restore courses AND users, teachers can only do users. 140 $this->setUser($user3); 141 142 // Disable all loggers. 143 $CFG->backup_error_log_logger_level = backup::LOG_NONE; 144 $CFG->backup_output_indented_logger_level = backup::LOG_NONE; 145 $CFG->backup_file_logger_level = backup::LOG_NONE; 146 $CFG->backup_database_logger_level = backup::LOG_NONE; 147 $CFG->backup_file_logger_level_extra = backup::LOG_NONE; 148 } 149 150 /** 151 * Test creating a course copy. 152 */ 153 public function test_create_copy() { 154 155 // Mock up the form data. 156 $formdata = new \stdClass; 157 $formdata->courseid = $this->course->id; 158 $formdata->fullname = 'foo'; 159 $formdata->shortname = 'bar'; 160 $formdata->category = 1; 161 $formdata->visible = 1; 162 $formdata->startdate = 1582376400; 163 $formdata->enddate = 0; 164 $formdata->idnumber = 123; 165 $formdata->userdata = 1; 166 $formdata->role_1 = 1; 167 $formdata->role_3 = 3; 168 $formdata->role_5 = 5; 169 170 $coursecopy = new \core_backup\copy\copy($formdata); 171 $result = $coursecopy->create_copy(); 172 173 // Load the controllers, to extract the data we need. 174 $bc = \backup_controller::load_controller($result['backupid']); 175 $rc = \restore_controller::load_controller($result['restoreid']); 176 177 // Check the backup controller. 178 $this->assertEquals($result, $bc->get_copy()->copyids); 179 $this->assertEquals(backup::MODE_COPY, $bc->get_mode()); 180 $this->assertEquals($this->course->id, $bc->get_courseid()); 181 $this->assertEquals(backup::TYPE_1COURSE, $bc->get_type()); 182 183 // Check the restore controller. 184 $newcourseid = $rc->get_courseid(); 185 $newcourse = get_course($newcourseid); 186 187 $this->assertEquals($result, $rc->get_copy()->copyids); 188 $this->assertEquals(get_string('copyingcourse', 'backup'), $newcourse->fullname); 189 $this->assertEquals(get_string('copyingcourseshortname', 'backup'), $newcourse->shortname); 190 $this->assertEquals(backup::MODE_COPY, $rc->get_mode()); 191 $this->assertEquals($newcourseid, $rc->get_courseid()); 192 193 // Check the created ad-hoc task. 194 $now = time(); 195 $task = \core\task\manager::get_next_adhoc_task($now); 196 197 $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); 198 $this->assertEquals($result, (array)$task->get_custom_data()); 199 $this->assertFalse($task->is_blocking()); 200 201 \core\task\manager::adhoc_task_complete($task); 202 } 203 204 /** 205 * Test getting the current copies. 206 */ 207 public function test_get_copies() { 208 global $USER; 209 210 // Mock up the form data. 211 $formdata = new \stdClass; 212 $formdata->courseid = $this->course->id; 213 $formdata->fullname = 'foo'; 214 $formdata->shortname = 'bar'; 215 $formdata->category = 1; 216 $formdata->visible = 1; 217 $formdata->startdate = 1582376400; 218 $formdata->enddate = 0; 219 $formdata->idnumber = ''; 220 $formdata->userdata = 1; 221 $formdata->role_1 = 1; 222 $formdata->role_3 = 3; 223 $formdata->role_5 = 5; 224 225 $formdata2 = clone($formdata); 226 $formdata2->shortname = 'tree'; 227 228 // Create some copies. 229 $coursecopy = new \core_backup\copy\copy($formdata); 230 $result = $coursecopy->create_copy(); 231 232 // Backup, awaiting. 233 $copies = \core_backup\copy\copy::get_copies($USER->id); 234 $this->assertEquals($result['backupid'], $copies[0]->backupid); 235 $this->assertEquals($result['restoreid'], $copies[0]->restoreid); 236 $this->assertEquals(\backup::STATUS_AWAITING, $copies[0]->status); 237 $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation); 238 239 $bc = \backup_controller::load_controller($result['backupid']); 240 241 // Backup, in progress. 242 $bc->set_status(\backup::STATUS_EXECUTING); 243 $copies = \core_backup\copy\copy::get_copies($USER->id); 244 $this->assertEquals($result['backupid'], $copies[0]->backupid); 245 $this->assertEquals($result['restoreid'], $copies[0]->restoreid); 246 $this->assertEquals(\backup::STATUS_EXECUTING, $copies[0]->status); 247 $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation); 248 249 // Restore, ready to process. 250 $bc->set_status(\backup::STATUS_FINISHED_OK); 251 $copies = \core_backup\copy\copy::get_copies($USER->id); 252 $this->assertEquals($result['backupid'], $copies[0]->backupid); 253 $this->assertEquals($result['restoreid'], $copies[0]->restoreid); 254 $this->assertEquals(\backup::STATUS_REQUIRE_CONV, $copies[0]->status); 255 $this->assertEquals(\backup::OPERATION_RESTORE, $copies[0]->operation); 256 257 // No records. 258 $bc->set_status(\backup::STATUS_FINISHED_ERR); 259 $copies = \core_backup\copy\copy::get_copies($USER->id); 260 $this->assertEmpty($copies); 261 262 $coursecopy2 = new \core_backup\copy\copy($formdata2); 263 $result2 = $coursecopy2->create_copy(); 264 // Set the second copy to be complete. 265 $bc = \backup_controller::load_controller($result2['backupid']); 266 $bc->set_status(\backup::STATUS_FINISHED_OK); 267 // Set the restore to be finished. 268 $rc = \backup_controller::load_controller($result2['restoreid']); 269 $rc->set_status(\backup::STATUS_FINISHED_OK); 270 271 // No records. 272 $copies = \core_backup\copy\copy::get_copies($USER->id); 273 $this->assertEmpty($copies); 274 } 275 276 /** 277 * Test getting the current copies for specific course. 278 */ 279 public function test_get_copies_course() { 280 global $USER; 281 282 // Mock up the form data. 283 $formdata = new \stdClass; 284 $formdata->courseid = $this->course->id; 285 $formdata->fullname = 'foo'; 286 $formdata->shortname = 'bar'; 287 $formdata->category = 1; 288 $formdata->visible = 1; 289 $formdata->startdate = 1582376400; 290 $formdata->enddate = 0; 291 $formdata->idnumber = ''; 292 $formdata->userdata = 1; 293 $formdata->role_1 = 1; 294 $formdata->role_3 = 3; 295 $formdata->role_5 = 5; 296 297 // Create some copies. 298 $coursecopy = new \core_backup\copy\copy($formdata); 299 $coursecopy->create_copy(); 300 301 // No copies match this course id. 302 $copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id + 1)); 303 $this->assertEmpty($copies); 304 } 305 306 /** 307 * Test getting the current copies if course has been deleted. 308 */ 309 public function test_get_copies_course_deleted() { 310 global $USER; 311 312 // Mock up the form data. 313 $formdata = new \stdClass; 314 $formdata->courseid = $this->course->id; 315 $formdata->fullname = 'foo'; 316 $formdata->shortname = 'bar'; 317 $formdata->category = 1; 318 $formdata->visible = 1; 319 $formdata->startdate = 1582376400; 320 $formdata->enddate = 0; 321 $formdata->idnumber = ''; 322 $formdata->userdata = 1; 323 $formdata->role_1 = 1; 324 $formdata->role_3 = 3; 325 $formdata->role_5 = 5; 326 327 // Create some copies. 328 $coursecopy = new \core_backup\copy\copy($formdata); 329 $coursecopy->create_copy(); 330 331 delete_course($this->course->id, false); 332 333 // No copies match this course id as it has been deleted. 334 $copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id)); 335 $this->assertEmpty($copies); 336 } 337 338 /* 339 * Test course copy. 340 */ 341 public function test_course_copy() { 342 global $DB; 343 344 // Mock up the form data. 345 $formdata = new \stdClass; 346 $formdata->courseid = $this->course->id; 347 $formdata->fullname = 'copy course'; 348 $formdata->shortname = 'copy course short'; 349 $formdata->category = 1; 350 $formdata->visible = 0; 351 $formdata->startdate = 1582376400; 352 $formdata->enddate = 1582386400; 353 $formdata->idnumber = 123; 354 $formdata->userdata = 1; 355 $formdata->role_1 = 1; 356 $formdata->role_3 = 3; 357 $formdata->role_5 = 5; 358 359 // Create the course copy records and associated ad-hoc task. 360 $coursecopy = new \core_backup\copy\copy($formdata); 361 $copyids = $coursecopy->create_copy(); 362 363 $courseid = $this->course->id; 364 365 // We are expecting trace output during this test. 366 $this->expectOutputRegex("/$courseid/"); 367 368 // Execute adhoc task. 369 $now = time(); 370 $task = \core\task\manager::get_next_adhoc_task($now); 371 $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); 372 $task->execute(); 373 \core\task\manager::adhoc_task_complete($task); 374 375 $postbackuprec = $DB->get_record('backup_controllers', array('backupid' => $copyids['backupid'])); 376 $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); 377 378 // Check backup was completed successfully. 379 $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status); 380 $this->assertEquals(1.0, $postbackuprec->progress); 381 382 // Check restore was completed successfully. 383 $this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status); 384 $this->assertEquals(1.0, $postrestorerec->progress); 385 386 // Check the restored course itself. 387 $coursecontext = context_course::instance($postrestorerec->itemid); 388 $users = get_enrolled_users($coursecontext); 389 390 $modinfo = get_fast_modinfo($postrestorerec->itemid); 391 $forums = $modinfo->get_instances_of('forum'); 392 $forum = reset($forums); 393 $discussions = forum_get_discussions($forum); 394 $course = $modinfo->get_course(); 395 396 $this->assertEquals($formdata->startdate, $course->startdate); 397 $this->assertEquals($formdata->enddate, $course->enddate); 398 $this->assertEquals('copy course', $course->fullname); 399 $this->assertEquals('copy course short', $course->shortname); 400 $this->assertEquals(0, $course->visible); 401 $this->assertEquals(123, $course->idnumber); 402 403 foreach ($modinfo->get_cms() as $cm) { 404 $this->assertContains($cm->get_formatted_name(), $this->activitynames); 405 } 406 407 foreach ($this->courseusers as $user) { 408 $this->assertEquals($user, $users[$user]->id); 409 } 410 411 $this->assertEquals(count($this->courseusers), count($users)); 412 $this->assertEquals(2, count($discussions)); 413 } 414 415 /* 416 * Test course copy, not including any users (or data). 417 */ 418 public function test_course_copy_no_users() { 419 global $DB; 420 421 // Mock up the form data. 422 $formdata = new \stdClass; 423 $formdata->courseid = $this->course->id; 424 $formdata->fullname = 'copy course'; 425 $formdata->shortname = 'copy course short'; 426 $formdata->category = 1; 427 $formdata->visible = 0; 428 $formdata->startdate = 1582376400; 429 $formdata->enddate = 1582386400; 430 $formdata->idnumber = 123; 431 $formdata->userdata = 1; 432 $formdata->role_1 = 0; 433 $formdata->role_3 = 0; 434 $formdata->role_5 = 0; 435 436 // Create the course copy records and associated ad-hoc task. 437 $coursecopy = new \core_backup\copy\copy($formdata); 438 $copyids = $coursecopy->create_copy(); 439 440 $courseid = $this->course->id; 441 442 // We are expecting trace output during this test. 443 $this->expectOutputRegex("/$courseid/"); 444 445 // Execute adhoc task. 446 $now = time(); 447 $task = \core\task\manager::get_next_adhoc_task($now); 448 $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); 449 $task->execute(); 450 \core\task\manager::adhoc_task_complete($task); 451 452 $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); 453 454 // Check the restored course itself. 455 $coursecontext = context_course::instance($postrestorerec->itemid); 456 $users = get_enrolled_users($coursecontext); 457 458 $modinfo = get_fast_modinfo($postrestorerec->itemid); 459 $forums = $modinfo->get_instances_of('forum'); 460 $forum = reset($forums); 461 $discussions = forum_get_discussions($forum); 462 $course = $modinfo->get_course(); 463 464 $this->assertEquals($formdata->startdate, $course->startdate); 465 $this->assertEquals($formdata->enddate, $course->enddate); 466 $this->assertEquals('copy course', $course->fullname); 467 $this->assertEquals('copy course short', $course->shortname); 468 $this->assertEquals(0, $course->visible); 469 $this->assertEquals(123, $course->idnumber); 470 471 foreach ($modinfo->get_cms() as $cm) { 472 $this->assertContains($cm->get_formatted_name(), $this->activitynames); 473 } 474 475 // Should be no discussions as the user that made them wasn't included. 476 $this->assertEquals(0, count($discussions)); 477 478 // There should only be one user in the new course, and that's the user who did the copy. 479 $this->assertEquals(1, count($users)); 480 $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id); 481 482 } 483 484 /* 485 * Test course copy, including students and their data. 486 */ 487 public function test_course_copy_students_data() { 488 global $DB; 489 490 // Mock up the form data. 491 $formdata = new \stdClass; 492 $formdata->courseid = $this->course->id; 493 $formdata->fullname = 'copy course'; 494 $formdata->shortname = 'copy course short'; 495 $formdata->category = 1; 496 $formdata->visible = 0; 497 $formdata->startdate = 1582376400; 498 $formdata->enddate = 1582386400; 499 $formdata->idnumber = 123; 500 $formdata->userdata = 1; 501 $formdata->role_1 = 0; 502 $formdata->role_3 = 0; 503 $formdata->role_5 = 5; 504 505 // Create the course copy records and associated ad-hoc task. 506 $coursecopy = new \core_backup\copy\copy($formdata); 507 $copyids = $coursecopy->create_copy(); 508 509 $courseid = $this->course->id; 510 511 // We are expecting trace output during this test. 512 $this->expectOutputRegex("/$courseid/"); 513 514 // Execute adhoc task. 515 $now = time(); 516 $task = \core\task\manager::get_next_adhoc_task($now); 517 $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); 518 $task->execute(); 519 \core\task\manager::adhoc_task_complete($task); 520 521 $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); 522 523 // Check the restored course itself. 524 $coursecontext = context_course::instance($postrestorerec->itemid); 525 $users = get_enrolled_users($coursecontext); 526 527 $modinfo = get_fast_modinfo($postrestorerec->itemid); 528 $forums = $modinfo->get_instances_of('forum'); 529 $forum = reset($forums); 530 $discussions = forum_get_discussions($forum); 531 $course = $modinfo->get_course(); 532 533 $this->assertEquals($formdata->startdate, $course->startdate); 534 $this->assertEquals($formdata->enddate, $course->enddate); 535 $this->assertEquals('copy course', $course->fullname); 536 $this->assertEquals('copy course short', $course->shortname); 537 $this->assertEquals(0, $course->visible); 538 $this->assertEquals(123, $course->idnumber); 539 540 foreach ($modinfo->get_cms() as $cm) { 541 $this->assertContains($cm->get_formatted_name(), $this->activitynames); 542 } 543 544 // Should be no discussions as the user that made them wasn't included. 545 $this->assertEquals(2, count($discussions)); 546 547 // There should only be two users in the new course. The copier and one student. 548 $this->assertEquals(2, count($users)); 549 $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id); 550 $this->assertEquals($this->courseusers[0], $users[$this->courseusers[0]]->id); 551 } 552 553 /* 554 * Test course copy, not including any users (or data). 555 */ 556 public function test_course_copy_no_data() { 557 global $DB; 558 559 // Mock up the form data. 560 $formdata = new \stdClass; 561 $formdata->courseid = $this->course->id; 562 $formdata->fullname = 'copy course'; 563 $formdata->shortname = 'copy course short'; 564 $formdata->category = 1; 565 $formdata->visible = 0; 566 $formdata->startdate = 1582376400; 567 $formdata->enddate = 1582386400; 568 $formdata->idnumber = 123; 569 $formdata->userdata = 0; 570 $formdata->role_1 = 1; 571 $formdata->role_3 = 3; 572 $formdata->role_5 = 5; 573 574 // Create the course copy records and associated ad-hoc task. 575 $coursecopy = new \core_backup\copy\copy($formdata); 576 $copyids = $coursecopy->create_copy(); 577 578 $courseid = $this->course->id; 579 580 // We are expecting trace output during this test. 581 $this->expectOutputRegex("/$courseid/"); 582 583 // Execute adhoc task. 584 $now = time(); 585 $task = \core\task\manager::get_next_adhoc_task($now); 586 $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); 587 $task->execute(); 588 \core\task\manager::adhoc_task_complete($task); 589 590 $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); 591 592 // Check the restored course itself. 593 $coursecontext = context_course::instance($postrestorerec->itemid); 594 $users = get_enrolled_users($coursecontext); 595 596 get_fast_modinfo($postrestorerec->itemid, 0, true); 597 $modinfo = get_fast_modinfo($postrestorerec->itemid); 598 $forums = $modinfo->get_instances_of('forum'); 599 $forum = reset($forums); 600 $discussions = forum_get_discussions($forum); 601 $course = $modinfo->get_course(); 602 603 $this->assertEquals($formdata->startdate, $course->startdate); 604 $this->assertEquals($formdata->enddate, $course->enddate); 605 $this->assertEquals('copy course', $course->fullname); 606 $this->assertEquals('copy course short', $course->shortname); 607 $this->assertEquals(0, $course->visible); 608 $this->assertEquals(123, $course->idnumber); 609 610 foreach ($modinfo->get_cms() as $cm) { 611 $this->assertContains($cm->get_formatted_name(), $this->activitynames); 612 } 613 614 // Should be no discussions as the user data wasn't included. 615 $this->assertEquals(0, count($discussions)); 616 617 // There should only be all users in the new course. 618 $this->assertEquals(count($this->courseusers), count($users)); 619 } 620 621 /* 622 * Test instantiation with incomplete formdata. 623 */ 624 public function test_malformed_instantiation() { 625 // Mock up the form data, missing things so we get an exception. 626 $formdata = new \stdClass; 627 $formdata->courseid = $this->course->id; 628 $formdata->fullname = 'copy course'; 629 $formdata->shortname = 'copy course short'; 630 $formdata->category = 1; 631 632 // Expect and exception as form data is incomplete. 633 $this->expectException(\moodle_exception::class); 634 new \core_backup\copy\copy($formdata); 635 } 636}