Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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