Proyectos de Subversion Moodle

Rev

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