f5a4dba208f8fa1181f032c4c1cb67abca26e771
[moodle.git] / question / tests / backup_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  * Unit tests for question backup and restore.
19  *
20  * @package    core_question
21  * @category   test
22  * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
29 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
30 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
32 /**
33  * Class core_question_backup_testcase
34  *
35  * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class core_question_backup_testcase extends advanced_testcase {
40     /**
41      * Makes a backup of the course.
42      *
43      * @param stdClass $course The course object.
44      * @return string Unique identifier for this backup.
45      */
46     protected function backup_course($course) {
47         global $CFG, $USER;
49         // Turn off file logging, otherwise it can't delete the file (Windows).
50         $CFG->backup_file_logger_level = backup::LOG_NONE;
52         // Do backup with default settings. MODE_IMPORT means it will just
53         // create the directory and not zip it.
54         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
55                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
56                 $USER->id);
57         $backupid = $bc->get_backupid();
58         $bc->execute_plan();
59         $bc->destroy();
61         return $backupid;
62     }
64     /**
65      * Restores a backup that has been made earlier.
66      *
67      * @param string $backupid The unique identifier of the backup.
68      * @param string $fullname Full name of the new course that is going to be created.
69      * @param string $shortname Short name of the new course that is going to be created.
70      * @param int $categoryid The course category the backup is going to be restored in.
71      * @param string[] $expectedprecheckwarning
72      * @return int The new course id.
73      */
74     protected function restore_course($backupid, $fullname, $shortname, $categoryid, $expectedprecheckwarning = []) {
75         global $CFG, $USER;
77         // Turn off file logging, otherwise it can't delete the file (Windows).
78         $CFG->backup_file_logger_level = backup::LOG_NONE;
80         // Do restore to new course with default settings.
81         $newcourseid = restore_dbops::create_new_course($fullname, $shortname, $categoryid);
82         $rc = new restore_controller($backupid, $newcourseid,
83                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
84                 backup::TARGET_NEW_COURSE);
86         $precheck = $rc->execute_precheck();
87         if (!$expectedprecheckwarning) {
88             $this->assertTrue($precheck);
89         } else {
90             $precheckresults = $rc->get_precheck_results();
91             $this->assertEquals(['warnings' => $expectedprecheckwarning], $precheckresults);
92         }
93         $rc->execute_plan();
94         $rc->destroy();
96         return $newcourseid;
97     }
99     /**
100      * This function tests backup and restore of question tags and course level question tags.
101      */
102     public function test_backup_question_tags() {
103         global $DB;
105         $this->resetAfterTest();
106         $this->setAdminUser();
108         // Create a new course category and and a new course in that.
109         $category1 = $this->getDataGenerator()->create_category();
110         $course = $this->getDataGenerator()->create_course(array('category' => $category1->id));
111         $courseshortname = $course->shortname;
112         $coursefullname = $course->fullname;
114         // Create 2 questions.
115         $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
116         $context = context_coursecat::instance($category1->id);
117         $qcat = $qgen->create_question_category(array('contextid' => $context->id));
118         $question1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id, 'idnumber' => 'q1'));
119         $question2 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id, 'idnumber' => 'q2'));
121         // Tag the questions with 2 question tags and 2 course level question tags.
122         $qcontext = context::instance_by_id($qcat->contextid);
123         $coursecontext = context_course::instance($course->id);
124         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['qtag1', 'qtag2']);
125         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['qtag3', 'qtag4']);
126         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag1', 'ctag2']);
127         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag3', 'ctag4']);
129         // Create a quiz and add one of the questions to that.
130         $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
131         quiz_add_quiz_question($question1->id, $quiz);
133         // Backup the course twice for future use.
134         $backupid1 = $this->backup_course($course);
135         $backupid2 = $this->backup_course($course);
137         // Now delete almost everything.
138         delete_course($course, false);
139         question_delete_question($question1->id);
140         question_delete_question($question2->id);
142         // Restore the backup we had made earlier into a new course.
143         $courseid2 = $this->restore_course($backupid1, $coursefullname, $courseshortname . '_2', $category1->id);
145         // The questions should remain in the question category they were which is
146         // a question category belonging to a course category context.
147         $questions = $DB->get_records('question', array('category' => $qcat->id), 'idnumber');
148         $this->assertCount(2, $questions);
150         // Retrieve tags for each question and check if they are assigned at the right context.
151         $qcount = 1;
152         foreach ($questions as $question) {
153             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
155             // Each question is tagged with 4 tags (2 question tags + 2 course tags).
156             $this->assertCount(4, $tags);
158             foreach ($tags as $tag) {
159                 if (in_array($tag->name, ['ctag1', 'ctag2', 'ctag3', 'ctag4'])) {
160                     $expected = context_course::instance($courseid2)->id;
161                 } else if (in_array($tag->name, ['qtag1', 'qtag2', 'qtag3', 'qtag4'])) {
162                     $expected = $qcontext->id;
163                 }
164                 $this->assertEquals($expected, $tag->taginstancecontextid);
165             }
167             // Also check idnumbers have been backed up and restored.
168             $this->assertEquals('q' . $qcount, $question->idnumber);
169             $qcount++;
170         }
172         // Now, again, delete everything including the course category.
173         delete_course($courseid2, false);
174         foreach ($questions as $question) {
175             question_delete_question($question->id);
176         }
177         $category1->delete_full(false);
179         // Create a new course category to restore the backup file into it.
180         $category2 = $this->getDataGenerator()->create_category();
182         $expectedwarnings = array(
183                 get_string('qcategory2coursefallback', 'backup', (object) ['name' => 'top']),
184                 get_string('qcategory2coursefallback', 'backup', (object) ['name' => $qcat->name])
185         );
187         // Restore to a new course in the new course category.
188         $courseid3 = $this->restore_course($backupid2, $coursefullname, $courseshortname . '_3', $category2->id, $expectedwarnings);
189         $coursecontext3 = context_course::instance($courseid3);
191         // The questions should have been moved to a question category that belongs to a course context.
192         $questions = $DB->get_records_sql("SELECT q.*
193                                              FROM {question} q
194                                              JOIN {question_categories} qc ON q.category = qc.id
195                                             WHERE qc.contextid = ?", array($coursecontext3->id));
196         $this->assertCount(2, $questions);
198         // Now, retrieve tags for each question and check if they are assigned at the right context.
199         foreach ($questions as $question) {
200             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
202             // Each question is tagged with 4 tags (all are course tags now).
203             $this->assertCount(4, $tags);
205             foreach ($tags as $tag) {
206                 $this->assertEquals($coursecontext3->id, $tag->taginstancecontextid);
207             }
208         }
210     }
212     /**
213      * Test that the question author is retained when they are enrolled in to the course.
214      */
215     public function test_backup_question_author_retained_when_enrolled() {
216         global $DB, $USER, $CFG;
217         $this->resetAfterTest();
218         $this->setAdminUser();
220         // Create a course, a category and a user.
221         $course = $this->getDataGenerator()->create_course();
222         $category = $this->getDataGenerator()->create_category();
223         $user = $this->getDataGenerator()->create_user();
225         // Create a question.
226         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
227         $questioncategory = $questiongenerator->create_question_category();
228         $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
229         $question = $questiongenerator->create_question('truefalse', null, $overrides);
231         // Create a quiz and a questions.
232         $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
233         quiz_add_quiz_question($question->id, $quiz);
235         // Enrol user with a teacher role.
236         $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
237         $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id, 'manual');
239         // Backup the course.
240         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
241             backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id);
242         $backupid = $bc->get_backupid();
243         $bc->execute_plan();
244         $results = $bc->get_results();
245         $file = $results['backup_destination'];
246         $fp = get_file_packer('application/vnd.moodle.backup');
247         $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
248         $file->extract_to_pathname($fp, $filepath);
249         $bc->destroy();
251         // Delete the original course and related question.
252         delete_course($course, false);
253         question_delete_question($question->id);
255         // Restore the course.
256         $restoredcourseid = restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
257         $rc = new restore_controller($backupid, $restoredcourseid, backup::INTERACTIVE_NO,
258             backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE);
259         $rc->execute_precheck();
260         $rc->execute_plan();
261         $rc->destroy();
263         // Test the question author.
264         $questions = $DB->get_records('question');
265         $this->assertCount(1, $questions);
266         $question3 = array_shift($questions);
267         $this->assertEquals($user->id, $question3->createdby);
268         $this->assertEquals($user->id, $question3->modifiedby);
269     }
271     /**
272      * Test that the question author is retained when they are not enrolled in to the course,
273      * but we are restoring the backup at the same site.
274      */
275     public function test_backup_question_author_retained_when_not_enrolled() {
276         global $DB, $USER, $CFG;
277         $this->resetAfterTest();
278         $this->setAdminUser();
280         // Create a course, a category and a user.
281         $course = $this->getDataGenerator()->create_course();
282         $category = $this->getDataGenerator()->create_category();
283         $user = $this->getDataGenerator()->create_user();
285         // Create a question.
286         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
287         $questioncategory = $questiongenerator->create_question_category();
288         $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
289         $question = $questiongenerator->create_question('truefalse', null, $overrides);
291         // Create a quiz and a questions.
292         $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
293         quiz_add_quiz_question($question->id, $quiz);
295         // Backup the course.
296         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
297             backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id);
298         $backupid = $bc->get_backupid();
299         $bc->execute_plan();
300         $results = $bc->get_results();
301         $file = $results['backup_destination'];
302         $fp = get_file_packer('application/vnd.moodle.backup');
303         $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
304         $file->extract_to_pathname($fp, $filepath);
305         $bc->destroy();
307         // Delete the original course and related question.
308         delete_course($course, false);
309         question_delete_question($question->id);
311         // Restore the course.
312         $restoredcourseid = restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
313         $rc = new restore_controller($backupid, $restoredcourseid, backup::INTERACTIVE_NO,
314             backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE);
315         $rc->execute_precheck();
316         $rc->execute_plan();
317         $rc->destroy();
319         // Test the question author.
320         $questions = $DB->get_records('question');
321         $this->assertCount(1, $questions);
322         $question = array_shift($questions);
323         $this->assertEquals($user->id, $question->createdby);
324         $this->assertEquals($user->id, $question->modifiedby);
325     }
327     /**
328      * Test that the current user is set as a question author when we are restoring the backup
329      * at the another site and the question author is not enrolled in to the course.
330      */
331     public function test_backup_question_author_reset() {
332         global $DB, $USER, $CFG;
333         $this->resetAfterTest();
334         $this->setAdminUser();
336         // Create a course, a category and a user.
337         $course = $this->getDataGenerator()->create_course();
338         $category = $this->getDataGenerator()->create_category();
339         $user = $this->getDataGenerator()->create_user();
341         // Create a question.
342         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
343         $questioncategory = $questiongenerator->create_question_category();
344         $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
345         $question = $questiongenerator->create_question('truefalse', null, $overrides);
347         // Create a quiz and a questions.
348         $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
349         quiz_add_quiz_question($question->id, $quiz);
351         // Backup the course.
352         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
353             backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id);
354         $backupid = $bc->get_backupid();
355         $bc->execute_plan();
356         $results = $bc->get_results();
357         $file = $results['backup_destination'];
358         $fp = get_file_packer('application/vnd.moodle.backup');
359         $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
360         $file->extract_to_pathname($fp, $filepath);
361         $bc->destroy();
363         // Delete the original course and related question.
364         delete_course($course, false);
365         question_delete_question($question->id);
367         // Emulate restoring to a different site.
368         set_config('siteidentifier', random_string(32) . 'not the same site');
370         // Restore the course.
371         $restoredcourseid = restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
372         $rc = new restore_controller($backupid, $restoredcourseid, backup::INTERACTIVE_NO,
373             backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE);
374         $rc->execute_precheck();
375         $rc->execute_plan();
376         $rc->destroy();
378         // Test the question author.
379         $questions = $DB->get_records('question');
380         $this->assertCount(1, $questions);
381         $question = array_shift($questions);
382         $this->assertEquals($USER->id, $question->createdby);
383         $this->assertEquals($USER->id, $question->modifiedby);
384     }