Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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  namespace core_backup;
  18  
  19  use backup;
  20  use backup_controller;
  21  use restore_controller;
  22  use restore_dbops;
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  
  26  global $CFG;
  27  require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
  28  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  29  require_once($CFG->libdir . '/completionlib.php');
  30  
  31  /**
  32   * Tests for Moodle 2 format backup operation.
  33   *
  34   * @package core_backup
  35   * @copyright 2014 The Open University
  36   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class moodle2_test extends \advanced_testcase {
  39  
  40      /**
  41       * Tests the availability field on modules and sections is correctly
  42       * backed up and restored.
  43       */
  44      public function test_backup_availability() {
  45          global $DB, $CFG;
  46  
  47          $this->resetAfterTest(true);
  48          $this->setAdminUser();
  49          $CFG->enableavailability = true;
  50          $CFG->enablecompletion = true;
  51  
  52          // Create a course with some availability data set.
  53          $generator = $this->getDataGenerator();
  54          $course = $generator->create_course(
  55                  array('format' => 'topics', 'numsections' => 3,
  56                      'enablecompletion' => COMPLETION_ENABLED),
  57                  array('createsections' => true));
  58          $forum = $generator->create_module('forum', array(
  59                  'course' => $course->id));
  60          $forum2 = $generator->create_module('forum', array(
  61                  'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
  62  
  63          // We need a grade, easiest is to add an assignment.
  64          $assignrow = $generator->create_module('assign', array(
  65                  'course' => $course->id));
  66          $assign = new \assign(\context_module::instance($assignrow->cmid), false, false);
  67          $item = $assign->get_grade_item();
  68  
  69          // Make a test grouping as well.
  70          $grouping = $generator->create_grouping(array('courseid' => $course->id,
  71                  'name' => 'Grouping!'));
  72  
  73          $availability = '{"op":"|","show":false,"c":[' .
  74                  '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
  75                  '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
  76                  '{"type":"grouping","id":' . $grouping->id . '}' .
  77                  ']}';
  78          $DB->set_field('course_modules', 'availability', $availability, array(
  79                  'id' => $forum->cmid));
  80          $DB->set_field('course_sections', 'availability', $availability, array(
  81                  'course' => $course->id, 'section' => 1));
  82  
  83          // Backup and restore it.
  84          $newcourseid = $this->backup_and_restore($course);
  85  
  86          // Check settings in new course.
  87          $modinfo = get_fast_modinfo($newcourseid);
  88          $forums = array_values($modinfo->get_instances_of('forum'));
  89          $assigns = array_values($modinfo->get_instances_of('assign'));
  90          $newassign = new \assign(\context_module::instance($assigns[0]->id), false, false);
  91          $newitem = $newassign->get_grade_item();
  92          $newgroupingid = $DB->get_field('groupings', 'id', array('courseid' => $newcourseid));
  93  
  94          // Expected availability should have new ID for the forum, grade, and grouping.
  95          $newavailability = str_replace(
  96                  '"grouping","id":' . $grouping->id,
  97                  '"grouping","id":' . $newgroupingid,
  98                  str_replace(
  99                      '"grade","id":' . $item->id,
 100                      '"grade","id":' . $newitem->id,
 101                      str_replace(
 102                          '"cm":' . $forum2->cmid,
 103                          '"cm":' . $forums[1]->id,
 104                          $availability)));
 105  
 106          $this->assertEquals($newavailability, $forums[0]->availability);
 107          $this->assertNull($forums[1]->availability);
 108          $this->assertEquals($newavailability, $modinfo->get_section_info(1, MUST_EXIST)->availability);
 109          $this->assertNull($modinfo->get_section_info(2, MUST_EXIST)->availability);
 110      }
 111  
 112      /**
 113       * The availability data format was changed in Moodle 2.7. This test
 114       * ensures that a Moodle 2.6 backup with this data can still be correctly
 115       * restored.
 116       */
 117      public function test_restore_legacy_availability() {
 118          global $DB, $USER, $CFG;
 119          require_once($CFG->dirroot . '/grade/querylib.php');
 120          require_once($CFG->libdir . '/completionlib.php');
 121  
 122          $this->resetAfterTest(true);
 123          $this->setAdminUser();
 124          $CFG->enableavailability = true;
 125          $CFG->enablecompletion = true;
 126  
 127          // Extract backup file.
 128          $backupid = 'abc';
 129          $backuppath = make_backup_temp_directory($backupid);
 130          get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
 131                  __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath);
 132  
 133          // Do restore to new course with default settings.
 134          $generator = $this->getDataGenerator();
 135          $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
 136          $newcourseid = restore_dbops::create_new_course(
 137                  'Test fullname', 'Test shortname', $categoryid);
 138          $rc = new restore_controller($backupid, $newcourseid,
 139                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 140                  backup::TARGET_NEW_COURSE);
 141          $thrown = null;
 142          try {
 143              $this->assertTrue($rc->execute_precheck());
 144              $rc->execute_plan();
 145              $rc->destroy();
 146          } catch (Exception $e) {
 147              $thrown = $e;
 148              // Because of the PHPUnit exception behaviour in this situation, we
 149              // will not see this message unless it is explicitly echoed (just
 150              // using it in a fail() call or similar will not work).
 151              echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' .
 152                      $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n";
 153          }
 154  
 155          $this->assertNull($thrown);
 156  
 157          // Get information about the resulting course and check that it is set
 158          // up correctly.
 159          $modinfo = get_fast_modinfo($newcourseid);
 160          $pages = array_values($modinfo->get_instances_of('page'));
 161          $forums = array_values($modinfo->get_instances_of('forum'));
 162          $quizzes = array_values($modinfo->get_instances_of('quiz'));
 163          $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid));
 164  
 165          // FROM date.
 166          $this->assertEquals(
 167                  '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}',
 168                  $pages[1]->availability);
 169          // UNTIL date.
 170          $this->assertEquals(
 171                  '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}',
 172                  $pages[2]->availability);
 173          // FROM and UNTIL.
 174          $this->assertEquals(
 175                  '{"op":"&","showc":[true,false],"c":[' .
 176                  '{"type":"date","d":">=","t":1449705600},' .
 177                  '{"type":"date","d":"<","t":1893456000}' .
 178                  ']}',
 179                  $pages[3]->availability);
 180          // Grade >= 75%.
 181          $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true));
 182          $gradeid = $grades[0]->id;
 183          $coursegrade = \grade_item::fetch_course_item($newcourseid);
 184          $this->assertEquals(
 185                  '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}',
 186                  $pages[4]->availability);
 187          // Grade < 25%.
 188          $this->assertEquals(
 189                  '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}',
 190                  $pages[5]->availability);
 191          // Grade 90-100%.
 192          $this->assertEquals(
 193                  '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}',
 194                  $pages[6]->availability);
 195          // Email contains frog.
 196          $this->assertEquals(
 197                  '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}',
 198                  $pages[7]->availability);
 199          // Page marked complete..
 200          $this->assertEquals(
 201                  '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id .
 202                  ',"e":' . COMPLETION_COMPLETE . '}]}',
 203                  $pages[8]->availability);
 204          // Quiz complete but failed.
 205          $this->assertEquals(
 206                  '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
 207                  ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}',
 208                  $pages[9]->availability);
 209          // Quiz complete and succeeded.
 210          $this->assertEquals(
 211                  '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
 212                  ',"e":' . COMPLETION_COMPLETE_PASS. '}]}',
 213                  $pages[10]->availability);
 214          // Quiz not complete.
 215          $this->assertEquals(
 216                  '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
 217                  ',"e":' . COMPLETION_INCOMPLETE . '}]}',
 218                  $pages[11]->availability);
 219          // Grouping.
 220          $this->assertEquals(
 221                  '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
 222                  $pages[12]->availability);
 223  
 224          // All the options.
 225          $this->assertEquals('{"op":"&",' .
 226                  '"showc":[false,true,false,true,true,true,true,true,true],' .
 227                  '"c":[' .
 228                  '{"type":"grouping","id":' . $grouping->id . '},' .
 229                  '{"type":"date","d":">=","t":1488585600},' .
 230                  '{"type":"date","d":"<","t":1709510400},' .
 231                  '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
 232                  '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' .
 233                  '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' .
 234                  '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' .
 235                  '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' .
 236                  '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' .
 237                  ']}', $pages[13]->availability);
 238  
 239          // Group members only forum.
 240          $this->assertEquals(
 241                  '{"op":"&","showc":[false],"c":[{"type":"group"}]}',
 242                  $forums[0]->availability);
 243  
 244          // Section with lots of conditions.
 245          $this->assertEquals(
 246                  '{"op":"&","showc":[false,false,false,false],"c":[' .
 247                  '{"type":"date","d":">=","t":1417737600},' .
 248                  '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
 249                  '{"type":"grade","id":' . $gradeid . ',"min":20},' .
 250                  '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}',
 251                  $modinfo->get_section_info(3)->availability);
 252  
 253          // Section with grouping.
 254          $this->assertEquals(
 255                  '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
 256                  $modinfo->get_section_info(4)->availability);
 257      }
 258  
 259      /**
 260       * Tests the backup and restore of single activity to same course (duplicate)
 261       * when it contains availability conditions that depend on other items in
 262       * course.
 263       */
 264      public function test_duplicate_availability() {
 265          global $DB, $CFG;
 266  
 267          $this->resetAfterTest(true);
 268          $this->setAdminUser();
 269          $CFG->enableavailability = true;
 270          $CFG->enablecompletion = true;
 271  
 272          // Create a course with completion enabled and 2 forums.
 273          $generator = $this->getDataGenerator();
 274          $course = $generator->create_course(
 275                  array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED));
 276          $forum = $generator->create_module('forum', array(
 277                  'course' => $course->id));
 278          $forum2 = $generator->create_module('forum', array(
 279                  'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
 280  
 281          // We need a grade, easiest is to add an assignment.
 282          $assignrow = $generator->create_module('assign', array(
 283                  'course' => $course->id));
 284          $assign = new \assign(\context_module::instance($assignrow->cmid), false, false);
 285          $item = $assign->get_grade_item();
 286  
 287          // Make a test group and grouping as well.
 288          $group = $generator->create_group(array('courseid' => $course->id,
 289                  'name' => 'Group!'));
 290          $grouping = $generator->create_grouping(array('courseid' => $course->id,
 291                  'name' => 'Grouping!'));
 292  
 293          // Set the forum to have availability conditions on all those things,
 294          // plus some that don't exist or are special values.
 295          $availability = '{"op":"|","show":false,"c":[' .
 296                  '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
 297                  '{"type":"completion","cm":99999999,"e":1},' .
 298                  '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
 299                  '{"type":"grade","id":99999998,"min":4,"max":94},' .
 300                  '{"type":"grouping","id":' . $grouping->id . '},' .
 301                  '{"type":"grouping","id":99999997},' .
 302                  '{"type":"group","id":' . $group->id . '},' .
 303                  '{"type":"group"},' .
 304                  '{"type":"group","id":99999996}' .
 305                  ']}';
 306          $DB->set_field('course_modules', 'availability', $availability, array(
 307                  'id' => $forum->cmid));
 308  
 309          // Duplicate it.
 310          $newcmid = $this->duplicate($course, $forum->cmid);
 311  
 312          // For those which still exist on the course we expect it to keep using
 313          // the real ID. For those which do not exist on the course any more
 314          // (e.g. simulating backup/restore of single activity between 2 courses)
 315          // we expect the IDs to be replaced with marker value: 0 for cmid
 316          // and grade, -1 for group/grouping.
 317          $expected = str_replace(
 318                  array('99999999', '99999998', '99999997', '99999996'),
 319                  array(0, 0, -1, -1),
 320                  $availability);
 321  
 322          // Check settings in new activity.
 323          $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid));
 324          $this->assertEquals($expected, $actual);
 325      }
 326  
 327      /**
 328       * When restoring a course, you can change the start date, which shifts other
 329       * dates. This test checks that certain dates are correctly modified.
 330       */
 331      public function test_restore_dates() {
 332          global $DB, $CFG;
 333  
 334          $this->resetAfterTest(true);
 335          $this->setAdminUser();
 336          $CFG->enableavailability = true;
 337  
 338          // Create a course with specific start date.
 339          $generator = $this->getDataGenerator();
 340          $course = $generator->create_course(array(
 341              'startdate' => strtotime('1 Jan 2014 00:00 GMT'),
 342              'enddate' => strtotime('3 Aug 2014 00:00 GMT')
 343          ));
 344  
 345          // Add a forum with conditional availability date restriction, including
 346          // one of them nested inside a tree.
 347          $availability = '{"op":"&","showc":[true,true],"c":[' .
 348                  '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' .
 349                  '{"type":"date","d":"<","t":DATE2}]}';
 350          $before = str_replace(
 351                  array('DATE1', 'DATE2'),
 352                  array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')),
 353                  $availability);
 354          $forum = $generator->create_module('forum', array('course' => $course->id,
 355                  'availability' => $before));
 356  
 357          // Add an assign with defined start date.
 358          $assign = $generator->create_module('assign', array('course' => $course->id,
 359                  'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT')));
 360  
 361          // Do backup and restore.
 362          $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT'));
 363  
 364          $newcourse = $DB->get_record('course', array('id' => $newcourseid));
 365          $this->assertEquals(strtotime('5 Aug 2015 00:00 GMT'), $newcourse->enddate);
 366  
 367          $modinfo = get_fast_modinfo($newcourseid);
 368  
 369          // Check forum dates are modified by the same amount as the course start.
 370          $newforums = $modinfo->get_instances_of('forum');
 371          $newforum = reset($newforums);
 372          $after = str_replace(
 373              array('DATE1', 'DATE2'),
 374              array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')),
 375              $availability);
 376          $this->assertEquals($after, $newforum->availability);
 377  
 378          // Check assign date.
 379          $newassigns = $modinfo->get_instances_of('assign');
 380          $newassign = reset($newassigns);
 381          $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field(
 382                  'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance)));
 383      }
 384  
 385      /**
 386       * Test front page backup/restore and duplicate activities
 387       * @return void
 388       */
 389      public function test_restore_frontpage() {
 390          global $DB, $CFG, $USER;
 391  
 392          $this->resetAfterTest(true);
 393          $this->setAdminUser();
 394          $generator = $this->getDataGenerator();
 395  
 396          $frontpage = $DB->get_record('course', array('id' => SITEID));
 397          $forum = $generator->create_module('forum', array('course' => $frontpage->id));
 398  
 399          // Activities can be duplicated.
 400          $this->duplicate($frontpage, $forum->cmid);
 401  
 402          $modinfo = get_fast_modinfo($frontpage);
 403          $this->assertEquals(2, count($modinfo->get_instances_of('forum')));
 404  
 405          // Front page backup.
 406          $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id,
 407                  backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 408                  $USER->id);
 409          $frontpagebackupid = $frontpagebc->get_backupid();
 410          $frontpagebc->execute_plan();
 411          $frontpagebc->destroy();
 412  
 413          $course = $generator->create_course();
 414          $newcourseid = restore_dbops::create_new_course(
 415                  $course->fullname . ' 2', $course->shortname . '_2', $course->category);
 416  
 417          // Other course backup.
 418          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 419                  backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 420                  $USER->id);
 421          $otherbackupid = $bc->get_backupid();
 422          $bc->execute_plan();
 423          $bc->destroy();
 424  
 425          // We can only restore a front page over the front page.
 426          $rc = new restore_controller($frontpagebackupid, $course->id,
 427                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 428                  backup::TARGET_CURRENT_ADDING);
 429          $this->assertFalse($rc->execute_precheck());
 430          $rc->destroy();
 431  
 432          $rc = new restore_controller($frontpagebackupid, $newcourseid,
 433                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 434                  backup::TARGET_NEW_COURSE);
 435          $this->assertFalse($rc->execute_precheck());
 436          $rc->destroy();
 437  
 438          $rc = new restore_controller($frontpagebackupid, $frontpage->id,
 439                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 440                  backup::TARGET_CURRENT_ADDING);
 441          $this->assertTrue($rc->execute_precheck());
 442          $rc->execute_plan();
 443          $rc->destroy();
 444  
 445          // We can't restore a non-front page course on the front page course.
 446          $rc = new restore_controller($otherbackupid, $frontpage->id,
 447                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 448                  backup::TARGET_CURRENT_ADDING);
 449          $this->assertFalse($rc->execute_precheck());
 450          $rc->destroy();
 451  
 452          $rc = new restore_controller($otherbackupid, $newcourseid,
 453                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 454                  backup::TARGET_NEW_COURSE);
 455          $this->assertTrue($rc->execute_precheck());
 456          $rc->execute_plan();
 457          $rc->destroy();
 458      }
 459  
 460      /**
 461       * Backs a course up and restores it.
 462       *
 463       * @param \stdClass $course Course object to backup
 464       * @param int $newdate If non-zero, specifies custom date for new course
 465       * @param callable|null $inbetween If specified, function that is called before restore
 466       * @return int ID of newly restored course
 467       */
 468      protected function backup_and_restore($course, $newdate = 0, $inbetween = null) {
 469          global $USER, $CFG;
 470  
 471          // Turn off file logging, otherwise it can't delete the file (Windows).
 472          $CFG->backup_file_logger_level = backup::LOG_NONE;
 473  
 474          // Do backup with default settings. MODE_IMPORT means it will just
 475          // create the directory and not zip it.
 476          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 477                  backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 478                  $USER->id);
 479          $backupid = $bc->get_backupid();
 480          $bc->execute_plan();
 481          $bc->destroy();
 482  
 483          if ($inbetween) {
 484              $inbetween($backupid);
 485          }
 486  
 487          // Do restore to new course with default settings.
 488          $newcourseid = restore_dbops::create_new_course(
 489                  $course->fullname, $course->shortname . '_2', $course->category);
 490          $rc = new restore_controller($backupid, $newcourseid,
 491                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 492                  backup::TARGET_NEW_COURSE);
 493          if ($newdate) {
 494              $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
 495          }
 496          $this->assertTrue($rc->execute_precheck());
 497          $rc->execute_plan();
 498          $rc->destroy();
 499  
 500          return $newcourseid;
 501      }
 502  
 503      /**
 504       * Duplicates a single activity within a course.
 505       *
 506       * This is based on the code from course/modduplicate.php, but reduced for
 507       * simplicity.
 508       *
 509       * @param \stdClass $course Course object
 510       * @param int $cmid Activity to duplicate
 511       * @return int ID of new activity
 512       */
 513      protected function duplicate($course, $cmid) {
 514          global $USER;
 515  
 516          // Do backup.
 517          $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE,
 518                  backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
 519          $backupid = $bc->get_backupid();
 520          $bc->execute_plan();
 521          $bc->destroy();
 522  
 523          // Do restore.
 524          $rc = new restore_controller($backupid, $course->id,
 525                  backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
 526          $this->assertTrue($rc->execute_precheck());
 527          $rc->execute_plan();
 528  
 529          // Find cmid.
 530          $tasks = $rc->get_plan()->get_tasks();
 531          $cmcontext = \context_module::instance($cmid);
 532          $newcmid = 0;
 533          foreach ($tasks as $task) {
 534              if (is_subclass_of($task, 'restore_activity_task')) {
 535                  if ($task->get_old_contextid() == $cmcontext->id) {
 536                      $newcmid = $task->get_moduleid();
 537                      break;
 538                  }
 539              }
 540          }
 541          $rc->destroy();
 542          if (!$newcmid) {
 543              throw new \coding_exception('Unexpected: failure to find restored cmid');
 544          }
 545          return $newcmid;
 546      }
 547  
 548      /**
 549       * Help function for enrolment methods backup/restore tests:
 550       *
 551       * - Creates a course ($course), adds self-enrolment method and a user
 552       * - Makes a backup
 553       * - Creates a target course (if requested) ($newcourseid)
 554       * - Initialises restore controller for this backup file ($rc)
 555       *
 556       * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc.
 557       * @param array $additionalcaps - additional capabilities to give to user
 558       * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc]
 559       */
 560      protected function prepare_for_enrolments_test($target, $additionalcaps = []) {
 561          global $CFG, $DB;
 562          $this->resetAfterTest(true);
 563  
 564          // Turn off file logging, otherwise it can't delete the file (Windows).
 565          $CFG->backup_file_logger_level = backup::LOG_NONE;
 566  
 567          $user = $this->getDataGenerator()->create_user();
 568          $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description');
 569  
 570          $course = $this->getDataGenerator()->create_course();
 571  
 572          // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it.
 573          $selfplugin = enrol_get_plugin('self');
 574          $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self'));
 575          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 576          $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
 577          $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id);
 578  
 579          // Give current user capabilities to do backup and restore and assign student role.
 580          $categorycontext = \context_course::instance($course->id)->get_parent_context();
 581  
 582          $caps = array_merge([
 583              'moodle/course:view',
 584              'moodle/course:create',
 585              'moodle/backup:backupcourse',
 586              'moodle/backup:configure',
 587              'moodle/backup:backuptargetimport',
 588              'moodle/restore:restorecourse',
 589              'moodle/role:assign',
 590              'moodle/restore:configure',
 591          ], $additionalcaps);
 592  
 593          foreach ($caps as $cap) {
 594              assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext);
 595          }
 596  
 597          core_role_set_assign_allowed($roleidcat, $studentrole->id);
 598          role_assign($roleidcat, $user->id, $categorycontext);
 599          accesslib_clear_all_caches_for_unit_testing();
 600  
 601          $this->setUser($user);
 602  
 603          // Do backup with default settings. MODE_IMPORT means it will just
 604          // create the directory and not zip it.
 605          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 606              backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE,
 607              $user->id);
 608          $backupid = $bc->get_backupid();
 609          $backupbasepath = $bc->get_plan()->get_basepath();
 610          $bc->execute_plan();
 611          $results = $bc->get_results();
 612          $file = $results['backup_destination'];
 613          $bc->destroy();
 614  
 615          // Restore the backup immediately.
 616  
 617          // Check if we need to unzip the file because the backup temp dir does not contains backup files.
 618          if (!file_exists($backupbasepath . "/moodle_backup.xml")) {
 619              $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath);
 620          }
 621  
 622          if ($target == backup::TARGET_NEW_COURSE) {
 623              $newcourseid = restore_dbops::create_new_course($course->fullname . '_2',
 624                  $course->shortname . '_2',
 625                  $course->category);
 626          } else {
 627              $newcourse = $this->getDataGenerator()->create_course();
 628              $newcourseid = $newcourse->id;
 629          }
 630          $rc = new restore_controller($backupid, $newcourseid,
 631              backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target);
 632  
 633          return [$course, $newcourseid, $rc];
 634      }
 635  
 636      /**
 637       * Backup a course with enrolment methods and restore it without user data and without enrolment methods
 638       */
 639      public function test_restore_without_users_without_enrolments() {
 640          global $DB;
 641  
 642          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE);
 643  
 644          // Ensure enrolment methods will not be restored without capability.
 645          $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
 646          $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value());
 647  
 648          $this->assertTrue($rc->execute_precheck());
 649          $rc->execute_plan();
 650          $rc->destroy();
 651  
 652          // Self-enrolment method was not enabled, users were not restored.
 653          $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 654              'status' => ENROL_INSTANCE_ENABLED]));
 655          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 656            join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 657          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 658          $this->assertEmpty($enrolments);
 659      }
 660  
 661      /**
 662       * Backup a course with enrolment methods and restore it without user data with enrolment methods
 663       */
 664      public function test_restore_without_users_with_enrolments() {
 665          global $DB;
 666  
 667          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
 668              ['moodle/course:enrolconfig']);
 669  
 670          // Ensure enrolment methods will be restored.
 671          $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
 672          $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value());
 673          // Set "Include enrolment methods" to "Always" so they can be restored without users.
 674          $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS);
 675  
 676          $this->assertTrue($rc->execute_precheck());
 677          $rc->execute_plan();
 678          $rc->destroy();
 679  
 680          // Self-enrolment method was restored (it is enabled), users were not restored.
 681          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 682              'status' => ENROL_INSTANCE_ENABLED]);
 683          $this->assertNotEmpty($enrol);
 684  
 685          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 686              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 687          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 688          $this->assertEmpty($enrolments);
 689      }
 690  
 691      /**
 692       * Backup a course with enrolment methods and restore it with user data and without enrolment methods
 693       */
 694      public function test_restore_with_users_without_enrolments() {
 695          global $DB;
 696  
 697          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
 698              ['moodle/backup:userinfo', 'moodle/restore:userinfo']);
 699  
 700          // Ensure enrolment methods will not be restored without capability.
 701          $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
 702          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 703  
 704          global $qwerty;
 705          $qwerty = 1;
 706          $this->assertTrue($rc->execute_precheck());
 707          $rc->execute_plan();
 708          $rc->destroy();
 709          $qwerty = 0;
 710  
 711          // Self-enrolment method was not restored, student was restored as manual enrolment.
 712          $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 713              'status' => ENROL_INSTANCE_ENABLED]));
 714  
 715          $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]);
 716          $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id]));
 717      }
 718  
 719      /**
 720       * Backup a course with enrolment methods and restore it with user data with enrolment methods
 721       */
 722      public function test_restore_with_users_with_enrolments() {
 723          global $DB;
 724  
 725          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
 726              ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
 727  
 728          // Ensure enrolment methods will be restored.
 729          $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
 730          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 731  
 732          $this->assertTrue($rc->execute_precheck());
 733          $rc->execute_plan();
 734          $rc->destroy();
 735  
 736          // Self-enrolment method was restored (it is enabled), student was restored.
 737          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 738              'status' => ENROL_INSTANCE_ENABLED]);
 739          $this->assertNotEmpty($enrol);
 740  
 741          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 742              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 743          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 744          $this->assertEquals(1, count($enrolments));
 745          $enrolment = reset($enrolments);
 746          $this->assertEquals('self', $enrolment->enrol);
 747      }
 748  
 749      /**
 750       * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course
 751       */
 752      public function test_restore_with_users_with_enrolments_merging() {
 753          global $DB;
 754  
 755          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING,
 756              ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
 757  
 758          // Ensure enrolment methods will be restored.
 759          $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
 760          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 761  
 762          $this->assertTrue($rc->execute_precheck());
 763          $rc->execute_plan();
 764          $rc->destroy();
 765  
 766          // User was restored with self-enrolment method.
 767          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 768              'status' => ENROL_INSTANCE_ENABLED]);
 769          $this->assertNotEmpty($enrol);
 770  
 771          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 772              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 773          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 774          $this->assertEquals(1, count($enrolments));
 775          $enrolment = reset($enrolments);
 776          $this->assertEquals('self', $enrolment->enrol);
 777      }
 778  
 779      /**
 780       * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents
 781       */
 782      public function test_restore_with_users_with_enrolments_deleting() {
 783          global $DB;
 784  
 785          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING,
 786              ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
 787  
 788          // Ensure enrolment methods will be restored.
 789          $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
 790          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 791  
 792          $this->assertTrue($rc->execute_precheck());
 793          $rc->execute_plan();
 794          $rc->destroy();
 795  
 796          // Self-enrolment method was restored (it is enabled), student was restored.
 797          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 798              'status' => ENROL_INSTANCE_ENABLED]);
 799          $this->assertNotEmpty($enrol);
 800  
 801          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 802              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 803          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 804          $this->assertEquals(1, count($enrolments));
 805          $enrolment = reset($enrolments);
 806          $this->assertEquals('self', $enrolment->enrol);
 807      }
 808  
 809      /**
 810       * Test the block instance time fields (timecreated, timemodified) through a backup and restore.
 811       */
 812      public function test_block_instance_times_backup() {
 813          global $DB;
 814          $this->resetAfterTest();
 815  
 816          $this->setAdminUser();
 817          $generator = $this->getDataGenerator();
 818  
 819          // Create course and add HTML block.
 820          $course = $generator->create_course();
 821          $context = \context_course::instance($course->id);
 822          $page = new \moodle_page();
 823          $page->set_context($context);
 824          $page->set_course($course);
 825          $page->set_pagelayout('standard');
 826          $page->set_pagetype('course-view');
 827          $page->blocks->load_blocks();
 828          $page->blocks->add_block_at_end_of_default_region('html');
 829  
 830          // Update (hack in database) timemodified and timecreated to specific values for testing.
 831          $blockdata = $DB->get_record('block_instances',
 832                  ['blockname' => 'html', 'parentcontextid' => $context->id]);
 833          $originalblockid = $blockdata->id;
 834          $blockdata->timecreated = 12345;
 835          $blockdata->timemodified = 67890;
 836          $DB->update_record('block_instances', $blockdata);
 837  
 838          // Do backup and restore.
 839          $newcourseid = $this->backup_and_restore($course);
 840  
 841          // Confirm that values were transferred correctly into HTML block on new course.
 842          $newcontext = \context_course::instance($newcourseid);
 843          $blockdata = $DB->get_record('block_instances',
 844                  ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
 845          $this->assertEquals(12345, $blockdata->timecreated);
 846          $this->assertEquals(67890, $blockdata->timemodified);
 847  
 848          // Simulate what happens with an older backup that doesn't have those fields, by removing
 849          // them from the backup before doing a restore.
 850          $before = time();
 851          $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) {
 852              global $CFG;
 853              $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' .
 854                      $originalblockid . '/block.xml';
 855              $xml = file_get_contents($path);
 856              $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml);
 857              file_put_contents($path, $xml);
 858          });
 859          $after = time();
 860  
 861          // The fields not specified should default to current time.
 862          $newcontext = \context_course::instance($newcourseid);
 863          $blockdata = $DB->get_record('block_instances',
 864                  ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
 865          $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated);
 866          $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified);
 867      }
 868  
 869      /**
 870       * When you restore a site with global search (or search indexing) turned on, then it should
 871       * add entries to the search index requests table so that the data gets indexed.
 872       */
 873      public function test_restore_search_index_requests() {
 874          global $DB, $CFG, $USER;
 875  
 876          $this->resetAfterTest(true);
 877          $this->setAdminUser();
 878          $CFG->enableglobalsearch = true;
 879  
 880          // Create a course.
 881          $generator = $this->getDataGenerator();
 882          $course = $generator->create_course();
 883  
 884          // Add a forum.
 885          $forum = $generator->create_module('forum', ['course' => $course->id]);
 886  
 887          // Add a block.
 888          $context = \context_course::instance($course->id);
 889          $page = new \moodle_page();
 890          $page->set_context($context);
 891          $page->set_course($course);
 892          $page->set_pagelayout('standard');
 893          $page->set_pagetype('course-view');
 894          $page->blocks->load_blocks();
 895          $page->blocks->add_block_at_end_of_default_region('html');
 896  
 897          // Initially there should be no search index requests.
 898          $this->assertEquals(0, $DB->count_records('search_index_requests'));
 899  
 900          // Do backup and restore.
 901          $newcourseid = $this->backup_and_restore($course);
 902  
 903          // Now the course should be requested for index (all search areas).
 904          $newcontext = \context_course::instance($newcourseid);
 905          $requests = array_values($DB->get_records('search_index_requests'));
 906          $this->assertCount(1, $requests);
 907          $this->assertEquals($newcontext->id, $requests[0]->contextid);
 908          $this->assertEquals('', $requests[0]->searcharea);
 909  
 910          get_fast_modinfo($newcourseid);
 911  
 912          // Backup the new course...
 913          $CFG->backup_file_logger_level = backup::LOG_NONE;
 914          $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid,
 915                  backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 916                  $USER->id);
 917          $backupid = $bc->get_backupid();
 918          $bc->execute_plan();
 919          $bc->destroy();
 920  
 921          // Restore it on top of old course (should duplicate the forum).
 922          $rc = new restore_controller($backupid, $course->id,
 923                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 924                  backup::TARGET_EXISTING_ADDING);
 925          $this->assertTrue($rc->execute_precheck());
 926          $rc->execute_plan();
 927          $rc->destroy();
 928  
 929          // Get the forums now on the old course.
 930          $modinfo = get_fast_modinfo($course->id);
 931          $forums = $modinfo->get_instances_of('forum');
 932          $this->assertCount(2, $forums);
 933  
 934          // The newer one will be the one with larger ID. (Safe to assume for unit test.)
 935          $biggest = null;
 936          foreach ($forums as $forum) {
 937              if ($biggest === null || $biggest->id < $forum->id) {
 938                  $biggest = $forum;
 939              }
 940          }
 941          $restoredforumcontext = \context_module::instance($biggest->id);
 942  
 943          // Get the HTML blocks now on the old course.
 944          $blockdata = array_values($DB->get_records('block_instances',
 945                  ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC'));
 946          $restoredblockcontext = \context_block::instance($blockdata[0]->id);
 947  
 948          // Check that we have requested index update on both the module and the block.
 949          $requests = array_values($DB->get_records('search_index_requests', null, 'id'));
 950          $this->assertCount(3, $requests);
 951          $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid);
 952          $this->assertEquals('', $requests[1]->searcharea);
 953          $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid);
 954          $this->assertEquals('', $requests[2]->searcharea);
 955      }
 956  
 957      /**
 958       * Test restoring courses based on the backup plan. Primarily used with
 959       * the import functionality
 960       */
 961      public function test_restore_course_using_plan_defaults() {
 962          global $DB, $CFG, $USER;
 963  
 964          $this->resetAfterTest(true);
 965          $this->setAdminUser();
 966          $CFG->enableglobalsearch = true;
 967  
 968          // Set admin config setting so that activities are not restored by default.
 969          set_config('restore_general_activities', 0, 'restore');
 970  
 971          // Create a course.
 972          $generator = $this->getDataGenerator();
 973          $course = $generator->create_course();
 974          $course2 = $generator->create_course();
 975          $course3 = $generator->create_course();
 976  
 977          // Add a forum.
 978          $forum = $generator->create_module('forum', ['course' => $course->id]);
 979  
 980          // Backup course...
 981          $CFG->backup_file_logger_level = backup::LOG_NONE;
 982          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 983              backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 984              $USER->id);
 985          $backupid = $bc->get_backupid();
 986          $bc->execute_plan();
 987          $bc->destroy();
 988  
 989          // Restore it on top of course2 (should duplicate the forum).
 990          $rc = new restore_controller($backupid, $course2->id,
 991              backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id,
 992              backup::TARGET_EXISTING_ADDING, null, backup::RELEASESESSION_NO);
 993          $this->assertTrue($rc->execute_precheck());
 994          $rc->execute_plan();
 995          $rc->destroy();
 996  
 997          // Get the forums now on the old course.
 998          $modinfo = get_fast_modinfo($course2->id);
 999          $forums = $modinfo->get_instances_of('forum');
1000          $this->assertCount(0, $forums);
1001      }
1002  
1003      /**
1004       * The Question category hierarchical structure was changed in Moodle 3.5.
1005       * From 3.5, all question categories in each context are a child of a single top level question category for that context.
1006       * This test ensures that both Moodle 3.4 and 3.5 backups can still be correctly restored.
1007       */
1008      public function test_restore_question_category_34_35() {
1009          global $DB, $USER, $CFG;
1010  
1011          $this->resetAfterTest(true);
1012          $this->setAdminUser();
1013  
1014          $backupfiles = array('question_category_34_format', 'question_category_35_format');
1015  
1016          foreach ($backupfiles as $backupfile) {
1017              // Extract backup file.
1018              $backupid = $backupfile;
1019              $backuppath = make_backup_temp_directory($backupid);
1020              get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
1021                      __DIR__ . "/fixtures/$backupfile.mbz", $backuppath);
1022  
1023              // Do restore to new course with default settings.
1024              $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
1025              $newcourseid = restore_dbops::create_new_course(
1026                      'Test fullname', 'Test shortname', $categoryid);
1027              $rc = new restore_controller($backupid, $newcourseid,
1028                      backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
1029                      backup::TARGET_NEW_COURSE);
1030  
1031              $this->assertTrue($rc->execute_precheck());
1032              $rc->execute_plan();
1033              $rc->destroy();
1034  
1035              // Get information about the resulting course and check that it is set up correctly.
1036              $modinfo = get_fast_modinfo($newcourseid);
1037              $quizzes = array_values($modinfo->get_instances_of('quiz'));
1038              $contexts = $quizzes[0]->context->get_parent_contexts(true);
1039  
1040              $topcategorycount = [];
1041              foreach ($contexts as $context) {
1042                  $cats = $DB->get_records('question_categories', array('contextid' => $context->id), 'parent', 'id, name, parent');
1043  
1044                  // Make sure all question categories that were inside the backup file were restored correctly.
1045                  if ($context->contextlevel == CONTEXT_COURSE) {
1046                      $this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name'));
1047                  } else if ($context->contextlevel == CONTEXT_MODULE) {
1048                      $this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name'));
1049                  }
1050  
1051                  $topcategorycount[$context->id] = 0;
1052                  foreach ($cats as $cat) {
1053                      if (!$cat->parent) {
1054                          $topcategorycount[$context->id]++;
1055                      }
1056                  }
1057  
1058                  // Make sure there is a single top level category in this context.
1059                  if ($cats) {
1060                      $this->assertEquals(1, $topcategorycount[$context->id]);
1061                  }
1062              }
1063          }
1064      }
1065  
1066      /**
1067       * Test the content bank content through a backup and restore.
1068       */
1069      public function test_contentbank_content_backup() {
1070          global $DB, $USER, $CFG;
1071          $this->resetAfterTest();
1072  
1073          $this->setAdminUser();
1074          $generator = $this->getDataGenerator();
1075          $cbgenerator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
1076  
1077          // Create course and add content bank content.
1078          $course = $generator->create_course();
1079          $context = \context_course::instance($course->id);
1080          $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
1081          $contents = $cbgenerator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $context, true, $filepath);
1082          $this->assertEquals(2, $DB->count_records('contentbank_content'));
1083  
1084          // Do backup and restore.
1085          $newcourseid = $this->backup_and_restore($course);
1086  
1087          // Confirm that values were transferred correctly into content bank on new course.
1088          $newcontext = \context_course::instance($newcourseid);
1089  
1090          $this->assertEquals(4, $DB->count_records('contentbank_content'));
1091          $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id]));
1092      }
1093  }