52310825a21497e2f806faea9455b6b2b6c7e7b2
[moodle.git] / backup / moodle2 / tests / moodle2_test.php
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/>.
17 /**
18  * Tests for Moodle 2 format backup operation.
19  *
20  * @package core_backup
21  * @copyright 2014 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
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');
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  */
39 class core_backup_moodle2_testcase extends advanced_testcase {
41     /**
42      * Tests the availability field on modules and sections is correctly
43      * backed up and restored.
44      */
45     public function test_backup_availability() {
46         global $DB, $CFG;
48         $this->resetAfterTest(true);
49         $this->setAdminUser();
50         $CFG->enableavailability = true;
51         $CFG->enablecompletion = true;
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));
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();
70         // Make a test grouping as well.
71         $grouping = $generator->create_grouping(array('courseid' => $course->id,
72                 'name' => 'Grouping!'));
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));
84         // Backup and restore it.
85         $newcourseid = $this->backup_and_restore($course);
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));
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)));
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     }
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      */
118     public function test_restore_legacy_availability() {
119         global $DB, $USER, $CFG;
120         require_once($CFG->dirroot . '/grade/querylib.php');
121         require_once($CFG->libdir . '/completionlib.php');
123         $this->resetAfterTest(true);
124         $this->setAdminUser();
125         $CFG->enableavailability = true;
126         $CFG->enablecompletion = true;
128         // Extract backup file.
129         $backupid = 'abc';
130         $backuppath = $CFG->tempdir . '/backup/' . $backupid;
131         check_dir_exists($backuppath);
132         get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
133                 __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath);
135         // Do restore to new course with default settings.
136         $generator = $this->getDataGenerator();
137         $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
138         $newcourseid = restore_dbops::create_new_course(
139                 'Test fullname', 'Test shortname', $categoryid);
140         $rc = new restore_controller($backupid, $newcourseid,
141                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
142                 backup::TARGET_NEW_COURSE);
143         $thrown = null;
144         try {
145             $this->assertTrue($rc->execute_precheck());
146             $rc->execute_plan();
147             $rc->destroy();
148         } catch (Exception $e) {
149             $thrown = $e;
150             // Because of the PHPUnit exception behaviour in this situation, we
151             // will not see this message unless it is explicitly echoed (just
152             // using it in a fail() call or similar will not work).
153             echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' .
154                     $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n";
155         }
157         $this->assertNull($thrown);
159         // Get information about the resulting course and check that it is set
160         // up correctly.
161         $modinfo = get_fast_modinfo($newcourseid);
162         $pages = array_values($modinfo->get_instances_of('page'));
163         $forums = array_values($modinfo->get_instances_of('forum'));
164         $quizzes = array_values($modinfo->get_instances_of('quiz'));
165         $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid));
167         // FROM date.
168         $this->assertEquals(
169                 '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}',
170                 $pages[1]->availability);
171         // UNTIL date.
172         $this->assertEquals(
173                 '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}',
174                 $pages[2]->availability);
175         // FROM and UNTIL.
176         $this->assertEquals(
177                 '{"op":"&","showc":[true,false],"c":[' .
178                 '{"type":"date","d":">=","t":1449705600},' .
179                 '{"type":"date","d":"<","t":1893456000}' .
180                 ']}',
181                 $pages[3]->availability);
182         // Grade >= 75%.
183         $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true));
184         $gradeid = $grades[0]->id;
185         $coursegrade = grade_item::fetch_course_item($newcourseid);
186         $this->assertEquals(
187                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}',
188                 $pages[4]->availability);
189         // Grade < 25%.
190         $this->assertEquals(
191                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}',
192                 $pages[5]->availability);
193         // Grade 90-100%.
194         $this->assertEquals(
195                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}',
196                 $pages[6]->availability);
197         // Email contains frog.
198         $this->assertEquals(
199                 '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}',
200                 $pages[7]->availability);
201         // Page marked complete..
202         $this->assertEquals(
203                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id .
204                 ',"e":' . COMPLETION_COMPLETE . '}]}',
205                 $pages[8]->availability);
206         // Quiz complete but failed.
207         $this->assertEquals(
208                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
209                 ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}',
210                 $pages[9]->availability);
211         // Quiz complete and succeeded.
212         $this->assertEquals(
213                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
214                 ',"e":' . COMPLETION_COMPLETE_PASS. '}]}',
215                 $pages[10]->availability);
216         // Quiz not complete.
217         $this->assertEquals(
218                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
219                 ',"e":' . COMPLETION_INCOMPLETE . '}]}',
220                 $pages[11]->availability);
221         // Grouping.
222         $this->assertEquals(
223                 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
224                 $pages[12]->availability);
226         // All the options.
227         $this->assertEquals('{"op":"&",' .
228                 '"showc":[false,true,false,true,true,true,true,true,true],' .
229                 '"c":[' .
230                 '{"type":"grouping","id":' . $grouping->id . '},' .
231                 '{"type":"date","d":">=","t":1488585600},' .
232                 '{"type":"date","d":"<","t":1709510400},' .
233                 '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
234                 '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' .
235                 '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' .
236                 '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' .
237                 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' .
238                 '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' .
239                 ']}', $pages[13]->availability);
241         // Group members only forum.
242         $this->assertEquals(
243                 '{"op":"&","showc":[false],"c":[{"type":"group"}]}',
244                 $forums[0]->availability);
246         // Section with lots of conditions.
247         $this->assertEquals(
248                 '{"op":"&","showc":[false,false,false,false],"c":[' .
249                 '{"type":"date","d":">=","t":1417737600},' .
250                 '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
251                 '{"type":"grade","id":' . $gradeid . ',"min":20},' .
252                 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}',
253                 $modinfo->get_section_info(3)->availability);
255         // Section with grouping.
256         $this->assertEquals(
257                 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
258                 $modinfo->get_section_info(4)->availability);
259     }
261     /**
262      * Tests the backup and restore of single activity to same course (duplicate)
263      * when it contains availability conditions that depend on other items in
264      * course.
265      */
266     public function test_duplicate_availability() {
267         global $DB, $CFG;
269         $this->resetAfterTest(true);
270         $this->setAdminUser();
271         $CFG->enableavailability = true;
272         $CFG->enablecompletion = true;
274         // Create a course with completion enabled and 2 forums.
275         $generator = $this->getDataGenerator();
276         $course = $generator->create_course(
277                 array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED));
278         $forum = $generator->create_module('forum', array(
279                 'course' => $course->id));
280         $forum2 = $generator->create_module('forum', array(
281                 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
283         // We need a grade, easiest is to add an assignment.
284         $assignrow = $generator->create_module('assign', array(
285                 'course' => $course->id));
286         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
287         $item = $assign->get_grade_item();
289         // Make a test group and grouping as well.
290         $group = $generator->create_group(array('courseid' => $course->id,
291                 'name' => 'Group!'));
292         $grouping = $generator->create_grouping(array('courseid' => $course->id,
293                 'name' => 'Grouping!'));
295         // Set the forum to have availability conditions on all those things,
296         // plus some that don't exist or are special values.
297         $availability = '{"op":"|","show":false,"c":[' .
298                 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
299                 '{"type":"completion","cm":99999999,"e":1},' .
300                 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
301                 '{"type":"grade","id":99999998,"min":4,"max":94},' .
302                 '{"type":"grouping","id":' . $grouping->id . '},' .
303                 '{"type":"grouping","id":99999997},' .
304                 '{"type":"group","id":' . $group->id . '},' .
305                 '{"type":"group"},' .
306                 '{"type":"group","id":99999996}' .
307                 ']}';
308         $DB->set_field('course_modules', 'availability', $availability, array(
309                 'id' => $forum->cmid));
311         // Duplicate it.
312         $newcmid = $this->duplicate($course, $forum->cmid);
314         // For those which still exist on the course we expect it to keep using
315         // the real ID. For those which do not exist on the course any more
316         // (e.g. simulating backup/restore of single activity between 2 courses)
317         // we expect the IDs to be replaced with marker value: 0 for cmid
318         // and grade, -1 for group/grouping.
319         $expected = str_replace(
320                 array('99999999', '99999998', '99999997', '99999996'),
321                 array(0, 0, -1, -1),
322                 $availability);
324         // Check settings in new activity.
325         $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid));
326         $this->assertEquals($expected, $actual);
327     }
329     /**
330      * When restoring a course, you can change the start date, which shifts other
331      * dates. This test checks that certain dates are correctly modified.
332      */
333     public function test_restore_dates() {
334         global $DB, $CFG;
336         $this->resetAfterTest(true);
337         $this->setAdminUser();
338         $CFG->enableavailability = true;
340         // Create a course with specific start date.
341         $generator = $this->getDataGenerator();
342         $course = $generator->create_course(array(
343                 'startdate' => strtotime('1 Jan 2014 00:00 GMT')));
345         // Add a forum with conditional availability date restriction, including
346         // one of them nested inside a tree.
347         $availability = '{"op":"&","showc":[true,true],"c":[' .
348                 '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' .
349                 '{"type":"date","d":"<","t":DATE2}]}';
350         $before = str_replace(
351                 array('DATE1', 'DATE2'),
352                 array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')),
353                 $availability);
354         $forum = $generator->create_module('forum', array('course' => $course->id,
355                 'availability' => $before));
357         // Add an assign with defined start date.
358         $assign = $generator->create_module('assign', array('course' => $course->id,
359                 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT')));
361         // Do backup and restore.
362         $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT'));
364         $modinfo = get_fast_modinfo($newcourseid);
366         // Check forum dates are modified by the same amount as the course start.
367         $newforums = $modinfo->get_instances_of('forum');
368         $newforum = reset($newforums);
369         $after = str_replace(
370             array('DATE1', 'DATE2'),
371             array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')),
372             $availability);
373         $this->assertEquals($after, $newforum->availability);
375         // Check assign date.
376         $newassigns = $modinfo->get_instances_of('assign');
377         $newassign = reset($newassigns);
378         $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field(
379                 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance)));
380     }
382     /**
383      * Test front page backup/restore and duplicate activities
384      * @return void
385      */
386     public function test_restore_frontpage() {
387         global $DB, $CFG, $USER;
389         $this->resetAfterTest(true);
390         $this->setAdminUser();
391         $generator = $this->getDataGenerator();
393         $frontpage = $DB->get_record('course', array('id' => SITEID));
394         $forum = $generator->create_module('forum', array('course' => $frontpage->id));
396         // Activities can be duplicated.
397         $this->duplicate($frontpage, $forum->cmid);
399         $modinfo = get_fast_modinfo($frontpage);
400         $this->assertEquals(2, count($modinfo->get_instances_of('forum')));
402         // Front page backup.
403         $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id,
404                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
405                 $USER->id);
406         $frontpagebackupid = $frontpagebc->get_backupid();
407         $frontpagebc->execute_plan();
408         $frontpagebc->destroy();
410         $course = $generator->create_course();
411         $newcourseid = restore_dbops::create_new_course(
412                 $course->fullname . ' 2', $course->shortname . '_2', $course->category);
414         // Other course backup.
415         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
416                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
417                 $USER->id);
418         $otherbackupid = $bc->get_backupid();
419         $bc->execute_plan();
420         $bc->destroy();
422         // We can only restore a front page over the front page.
423         $rc = new restore_controller($frontpagebackupid, $course->id,
424                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
425                 backup::TARGET_CURRENT_ADDING);
426         $this->assertFalse($rc->execute_precheck());
427         $rc->destroy();
429         $rc = new restore_controller($frontpagebackupid, $newcourseid,
430                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
431                 backup::TARGET_NEW_COURSE);
432         $this->assertFalse($rc->execute_precheck());
433         $rc->destroy();
435         $rc = new restore_controller($frontpagebackupid, $frontpage->id,
436                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
437                 backup::TARGET_CURRENT_ADDING);
438         $this->assertTrue($rc->execute_precheck());
439         $rc->execute_plan();
440         $rc->destroy();
442         // We can't restore a non-front page course on the front page course.
443         $rc = new restore_controller($otherbackupid, $frontpage->id,
444                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
445                 backup::TARGET_CURRENT_ADDING);
446         $this->assertFalse($rc->execute_precheck());
447         $rc->destroy();
449         $rc = new restore_controller($otherbackupid, $newcourseid,
450                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
451                 backup::TARGET_NEW_COURSE);
452         $this->assertTrue($rc->execute_precheck());
453         $rc->execute_plan();
454         $rc->destroy();
455     }
457     /**
458      * Backs a course up and restores it.
459      *
460      * @param stdClass $course Course object to backup
461      * @param int $newdate If non-zero, specifies custom date for new course
462      * @return int ID of newly restored course
463      */
464     protected function backup_and_restore($course, $newdate = 0) {
465         global $USER, $CFG;
467         // Turn off file logging, otherwise it can't delete the file (Windows).
468         $CFG->backup_file_logger_level = backup::LOG_NONE;
470         // Do backup with default settings. MODE_IMPORT means it will just
471         // create the directory and not zip it.
472         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
473                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
474                 $USER->id);
475         $backupid = $bc->get_backupid();
476         $bc->execute_plan();
477         $bc->destroy();
479         // Do restore to new course with default settings.
480         $newcourseid = restore_dbops::create_new_course(
481                 $course->fullname, $course->shortname . '_2', $course->category);
482         $rc = new restore_controller($backupid, $newcourseid,
483                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
484                 backup::TARGET_NEW_COURSE);
485         if ($newdate) {
486             $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
487         }
488         $this->assertTrue($rc->execute_precheck());
489         $rc->execute_plan();
490         $rc->destroy();
492         return $newcourseid;
493     }
495     /**
496      * Duplicates a single activity within a course.
497      *
498      * This is based on the code from course/modduplicate.php, but reduced for
499      * simplicity.
500      *
501      * @param stdClass $course Course object
502      * @param int $cmid Activity to duplicate
503      * @return int ID of new activity
504      */
505     protected function duplicate($course, $cmid) {
506         global $USER;
508         // Do backup.
509         $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE,
510                 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
511         $backupid = $bc->get_backupid();
512         $bc->execute_plan();
513         $bc->destroy();
515         // Do restore.
516         $rc = new restore_controller($backupid, $course->id,
517                 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
518         $this->assertTrue($rc->execute_precheck());
519         $rc->execute_plan();
521         // Find cmid.
522         $tasks = $rc->get_plan()->get_tasks();
523         $cmcontext = context_module::instance($cmid);
524         $newcmid = 0;
525         foreach ($tasks as $task) {
526             if (is_subclass_of($task, 'restore_activity_task')) {
527                 if ($task->get_old_contextid() == $cmcontext->id) {
528                     $newcmid = $task->get_moduleid();
529                     break;
530                 }
531             }
532         }
533         $rc->destroy();
534         if (!$newcmid) {
535             throw new coding_exception('Unexpected: failure to find restored cmid');
536         }
537         return $newcmid;
538     }