2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Course restore tests.
20 * @package core_course
21 * @copyright 2016 Frédéric Massart - FMCorz.net
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') ||
die();
28 require_once($CFG->dirroot
. '/backup/util/includes/backup_includes.php');
29 require_once($CFG->dirroot
. '/backup/util/includes/restore_includes.php');
32 * Course restore testcase.
34 * @package core_course
35 * @copyright 2016 Frédéric Massart - FMCorz.net
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 class core_course_restore_testcase
extends advanced_testcase
{
41 * Backup a course and return its backup ID.
43 * @param int $courseid The course ID.
44 * @param int $userid The user doing the backup.
47 protected function backup_course($courseid, $userid = 2) {
48 $backuptempdir = make_backup_temp_directory('');
49 $packer = get_file_packer('application/vnd.moodle.backup');
51 $bc = new backup_controller(backup
::TYPE_1COURSE
, $courseid, backup
::FORMAT_MOODLE
, backup
::INTERACTIVE_NO
,
52 backup
::MODE_GENERAL
, $userid);
55 $results = $bc->get_results();
56 $results['backup_destination']->extract_to_pathname($packer, "$backuptempdir/core_course_testcase");
60 return 'core_course_testcase';
64 * Create a role with capabilities and permissions.
66 * @param string|array $caps Capability names.
67 * @param int $perm Constant CAP_* to apply to the capabilities.
68 * @return int The new role ID.
70 protected function create_role_with_caps($caps, $perm) {
71 $caps = (array) $caps;
72 $dg = $this->getDataGenerator();
73 $roleid = $dg->create_role();
74 foreach ($caps as $cap) {
75 assign_capability($cap, $perm, $roleid, context_system
::instance()->id
, true);
77 accesslib_clear_all_caches_for_unit_testing();
84 * @param int $backupid The backup ID.
85 * @param int $courseid The course ID to restore in, or 0.
86 * @param int $userid The ID of the user performing the restore.
87 * @return stdClass The updated course object.
89 protected function restore_course($backupid, $courseid, $userid) {
92 $target = backup
::TARGET_CURRENT_ADDING
;
94 $target = backup
::TARGET_NEW_COURSE
;
95 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
96 $courseid = restore_dbops
::create_new_course('Tmp', 'tmp', $categoryid);
99 $rc = new restore_controller($backupid, $courseid, backup
::INTERACTIVE_NO
, backup
::MODE_GENERAL
, $userid, $target);
100 $target == backup
::TARGET_NEW_COURSE ?
: $rc->get_plan()->get_setting('overwrite_conf')->set_value(true);
101 $this->assertTrue($rc->execute_precheck());
104 $course = $DB->get_record('course', array('id' => $rc->get_courseid()));
112 * Restore a course to an existing course.
114 * @param int $backupid The backup ID.
115 * @param int $courseid The course ID to restore in.
116 * @param int $userid The ID of the user performing the restore.
117 * @return stdClass The updated course object.
119 protected function restore_to_existing_course($backupid, $courseid, $userid = 2) {
120 return $this->restore_course($backupid, $courseid, $userid);
124 * Restore a course to a new course.
126 * @param int $backupid The backup ID.
127 * @param int $userid The ID of the user performing the restore.
128 * @return stdClass The new course object.
130 protected function restore_to_new_course($backupid, $userid = 2) {
131 return $this->restore_course($backupid, 0, $userid);
137 * @param int $backupid The backup ID.
138 * @param int $courseid The course ID to restore in, or 0.
139 * @param int $userid The ID of the user performing the restore.
140 * @param int $target THe target of the restore.
142 * @return stdClass The updated course object.
144 protected function async_restore_course($backupid, $courseid, $userid, $target) {
148 $target = backup
::TARGET_NEW_COURSE
;
149 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
150 $courseid = restore_dbops
::create_new_course('Tmp', 'tmp', $categoryid);
153 $rc = new restore_controller($backupid, $courseid, backup
::INTERACTIVE_NO
, backup
::MODE_ASYNC
, $userid, $target);
154 $target == backup
::TARGET_NEW_COURSE ?
: $rc->get_plan()->get_setting('overwrite_conf')->set_value(true);
155 $this->assertTrue($rc->execute_precheck());
157 $restoreid = $rc->get_restoreid();
160 // Create the adhoc task.
161 $asynctask = new \core\task\asynchronous_restore_task
();
162 $asynctask->set_blocking(false);
163 $asynctask->set_custom_data(array('backupid' => $restoreid));
164 \core\task\manager
::queue_adhoc_task($asynctask);
166 // We are expecting trace output during this test.
167 $this->expectOutputRegex("/$restoreid/");
169 // Execute adhoc task.
171 $task = \core\task\manager
::get_next_adhoc_task($now);
172 $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
174 \core\task\manager
::adhoc_task_complete($task);
176 $course = $DB->get_record('course', array('id' => $rc->get_courseid()));
182 * Restore a course to an existing course.
184 * @param int $backupid The backup ID.
185 * @param int $courseid The course ID to restore in.
186 * @param int $userid The ID of the user performing the restore.
187 * @param int $target The type of restore we are performing.
188 * @return stdClass The updated course object.
190 protected function async_restore_to_existing_course($backupid, $courseid,
191 $userid = 2, $target = backup
::TARGET_CURRENT_ADDING
) {
192 return $this->async_restore_course($backupid, $courseid, $userid, $target);
196 * Restore a course to a new course.
198 * @param int $backupid The backup ID.
199 * @param int $userid The ID of the user performing the restore.
200 * @return stdClass The new course object.
202 protected function async_restore_to_new_course($backupid, $userid = 2) {
203 return $this->async_restore_course($backupid, 0, $userid, 0);
206 public function test_async_restore_existing_idnumber_in_new_course() {
207 $this->resetAfterTest();
209 $dg = $this->getDataGenerator();
210 $c1 = $dg->create_course(['idnumber' => 'ABC']);
211 $backupid = $this->backup_course($c1->id
);
212 $c2 = $this->async_restore_to_new_course($backupid);
214 // The ID number is set empty.
215 $this->assertEquals('', $c2->idnumber
);
218 public function test_async_restore_course_info_in_existing_course() {
220 $this->resetAfterTest();
221 $dg = $this->getDataGenerator();
223 $this->assertEquals(1, get_config('restore', 'restore_merge_course_shortname'));
224 $this->assertEquals(1, get_config('restore', 'restore_merge_course_fullname'));
225 $this->assertEquals(1, get_config('restore', 'restore_merge_course_startdate'));
227 $startdate = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016.
229 // Create two courses with different start dates,in each course create a chat that opens 1 week after the course start date.
230 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
,
231 'startdate' => $startdate]);
232 $chat1 = $dg->create_module('chat', ['name' => 'First', 'course' => $c1->id
, 'chattime' => $c1->startdate +
1 * WEEKSECS
]);
233 $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN
,
234 'startdate' => $startdate +
2 * WEEKSECS
]);
235 $chat2 = $dg->create_module('chat', ['name' => 'Second', 'course' => $c2->id
, 'chattime' => $c2->startdate +
1 * WEEKSECS
]);
236 $backupid = $this->backup_course($c1->id
);
238 // The information is restored but adapted because names are already taken.
239 $c2 = $this->async_restore_to_existing_course($backupid, $c2->id
);
240 $this->assertEquals('SN_1', $c2->shortname
);
241 $this->assertEquals('FN copy 1', $c2->fullname
);
242 $this->assertEquals('DESC', $c2->summary
);
243 $this->assertEquals(FORMAT_MOODLE
, $c2->summaryformat
);
244 $this->assertEquals($startdate, $c2->startdate
);
246 // Now course c2 has two chats - one ('Second') was already there and one ('First') was restored from the backup.
247 // Their dates are exactly the same as they were in the original modules.
248 $restoredchat1 = $DB->get_record('chat', ['name' => 'First', 'course' => $c2->id
]);
249 $restoredchat2 = $DB->get_record('chat', ['name' => 'Second', 'course' => $c2->id
]);
250 $this->assertEquals($chat1->chattime
, $restoredchat1->chattime
);
251 $this->assertEquals($chat2->chattime
, $restoredchat2->chattime
);
254 public function test_async_restore_course_info_in_existing_course_delete_first() {
256 $this->resetAfterTest();
257 $dg = $this->getDataGenerator();
259 $this->assertEquals(1, get_config('restore', 'restore_merge_course_shortname'));
260 $this->assertEquals(1, get_config('restore', 'restore_merge_course_fullname'));
261 $this->assertEquals(1, get_config('restore', 'restore_merge_course_startdate'));
263 $startdate = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016.
265 // Create two courses with different start dates,in each course create a chat that opens 1 week after the course start date.
266 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
,
267 'startdate' => $startdate]);
268 $chat1 = $dg->create_module('chat', ['name' => 'First', 'course' => $c1->id
, 'chattime' => $c1->startdate +
1 * WEEKSECS
]);
269 $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN
,
270 'startdate' => $startdate +
2 * WEEKSECS
]);
271 $chat2 = $dg->create_module('chat', ['name' => 'Second', 'course' => $c2->id
, 'chattime' => $c2->startdate +
1 * WEEKSECS
]);
272 $backupid = $this->backup_course($c1->id
);
274 // The information is restored and the existing course settings is modified.
275 $c2 = $this->async_restore_to_existing_course($backupid, $c2->id
, 2, backup
::TARGET_CURRENT_DELETING
);
276 $this->assertEquals(FORMAT_MOODLE
, $c2->summaryformat
);
278 // Now course2 should have a new forum with the original forum deleted.
279 $restoredchat1 = $DB->get_record('chat', ['name' => 'First', 'course' => $c2->id
]);
280 $restoredchat2 = $DB->get_record('chat', ['name' => 'Second', 'course' => $c2->id
]);
281 $this->assertEquals($chat1->chattime
, $restoredchat1->chattime
);
282 $this->assertEmpty($restoredchat2);
285 public function test_restore_existing_idnumber_in_new_course() {
286 $this->resetAfterTest();
288 $dg = $this->getDataGenerator();
289 $c1 = $dg->create_course(['idnumber' => 'ABC']);
290 $backupid = $this->backup_course($c1->id
);
291 $c2 = $this->restore_to_new_course($backupid);
293 // The ID number is set empty.
294 $this->assertEquals('', $c2->idnumber
);
297 public function test_restore_non_existing_idnumber_in_new_course() {
299 $this->resetAfterTest();
301 $dg = $this->getDataGenerator();
302 $c1 = $dg->create_course(['idnumber' => 'ABC']);
303 $backupid = $this->backup_course($c1->id
);
305 $c1->idnumber
= 'BCD';
306 $DB->update_record('course', $c1);
308 // The ID number changed.
309 $c2 = $this->restore_to_new_course($backupid);
310 $this->assertEquals('ABC', $c2->idnumber
);
313 public function test_restore_existing_idnumber_in_existing_course() {
315 $this->resetAfterTest();
317 $dg = $this->getDataGenerator();
318 $c1 = $dg->create_course(['idnumber' => 'ABC']);
319 $c2 = $dg->create_course(['idnumber' => 'DEF']);
320 $backupid = $this->backup_course($c1->id
);
322 // The ID number does not change.
323 $c2 = $this->restore_to_existing_course($backupid, $c2->id
);
324 $this->assertEquals('DEF', $c2->idnumber
);
326 $c1 = $DB->get_record('course', array('id' => $c1->id
));
327 $this->assertEquals('ABC', $c1->idnumber
);
330 public function test_restore_non_existing_idnumber_in_existing_course() {
332 $this->resetAfterTest();
334 $dg = $this->getDataGenerator();
335 $c1 = $dg->create_course(['idnumber' => 'ABC']);
336 $c2 = $dg->create_course(['idnumber' => 'DEF']);
337 $backupid = $this->backup_course($c1->id
);
339 $c1->idnumber
= 'XXX';
340 $DB->update_record('course', $c1);
342 // The ID number has changed.
343 $c2 = $this->restore_to_existing_course($backupid, $c2->id
);
344 $this->assertEquals('ABC', $c2->idnumber
);
347 public function test_restore_idnumber_in_existing_course_without_permissions() {
349 $this->resetAfterTest();
350 $dg = $this->getDataGenerator();
351 $u1 = $dg->create_user();
353 $managers = get_archetype_roles('manager');
354 $manager = array_shift($managers);
355 $roleid = $this->create_role_with_caps('moodle/course:changeidnumber', CAP_PROHIBIT
);
356 $dg->role_assign($manager->id
, $u1->id
);
357 $dg->role_assign($roleid, $u1->id
);
359 $c1 = $dg->create_course(['idnumber' => 'ABC']);
360 $c2 = $dg->create_course(['idnumber' => 'DEF']);
361 $backupid = $this->backup_course($c1->id
);
363 $c1->idnumber
= 'XXX';
364 $DB->update_record('course', $c1);
366 // The ID number does not change.
367 $c2 = $this->restore_to_existing_course($backupid, $c2->id
, $u1->id
);
368 $this->assertEquals('DEF', $c2->idnumber
);
371 public function test_restore_course_info_in_new_course() {
373 $this->resetAfterTest();
374 $dg = $this->getDataGenerator();
376 $startdate = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016.
378 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'startdate' => $startdate,
379 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
]);
380 $backupid = $this->backup_course($c1->id
);
382 // The information is restored but adapted because names are already taken.
383 $c2 = $this->restore_to_new_course($backupid);
384 $this->assertEquals('SN_1', $c2->shortname
);
385 $this->assertEquals('FN copy 1', $c2->fullname
);
386 $this->assertEquals('DESC', $c2->summary
);
387 $this->assertEquals(FORMAT_MOODLE
, $c2->summaryformat
);
388 $this->assertEquals($startdate, $c2->startdate
);
391 public function test_restore_course_info_in_existing_course() {
393 $this->resetAfterTest();
394 $dg = $this->getDataGenerator();
396 $this->assertEquals(1, get_config('restore', 'restore_merge_course_shortname'));
397 $this->assertEquals(1, get_config('restore', 'restore_merge_course_fullname'));
398 $this->assertEquals(1, get_config('restore', 'restore_merge_course_startdate'));
400 $startdate = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016.
402 // Create two courses with different start dates,in each course create a chat that opens 1 week after the course start date.
403 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
,
404 'startdate' => $startdate]);
405 $chat1 = $dg->create_module('chat', ['name' => 'First', 'course' => $c1->id
, 'chattime' => $c1->startdate +
1 * WEEKSECS
]);
406 $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN
,
407 'startdate' => $startdate +
2 * WEEKSECS
]);
408 $chat2 = $dg->create_module('chat', ['name' => 'Second', 'course' => $c2->id
, 'chattime' => $c2->startdate +
1 * WEEKSECS
]);
409 $backupid = $this->backup_course($c1->id
);
411 // The information is restored but adapted because names are already taken.
412 $c2 = $this->restore_to_existing_course($backupid, $c2->id
);
413 $this->assertEquals('SN_1', $c2->shortname
);
414 $this->assertEquals('FN copy 1', $c2->fullname
);
415 $this->assertEquals('DESC', $c2->summary
);
416 $this->assertEquals(FORMAT_MOODLE
, $c2->summaryformat
);
417 $this->assertEquals($startdate, $c2->startdate
);
419 // Now course c2 has two chats - one ('Second') was already there and one ('First') was restored from the backup.
420 // Their dates are exactly the same as they were in the original modules.
421 $restoredchat1 = $DB->get_record('chat', ['name' => 'First', 'course' => $c2->id
]);
422 $restoredchat2 = $DB->get_record('chat', ['name' => 'Second', 'course' => $c2->id
]);
423 $this->assertEquals($chat1->chattime
, $restoredchat1->chattime
);
424 $this->assertEquals($chat2->chattime
, $restoredchat2->chattime
);
427 public function test_restore_course_shortname_in_existing_course_without_permissions() {
429 $this->resetAfterTest();
430 $dg = $this->getDataGenerator();
431 $u1 = $dg->create_user();
433 $managers = get_archetype_roles('manager');
434 $manager = array_shift($managers);
435 $roleid = $this->create_role_with_caps('moodle/course:changeshortname', CAP_PROHIBIT
);
436 $dg->role_assign($manager->id
, $u1->id
);
437 $dg->role_assign($roleid, $u1->id
);
439 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
]);
440 $c2 = $dg->create_course(['shortname' => 'A1', 'fullname' => 'B1', 'summary' => 'C1', 'summaryformat' => FORMAT_PLAIN
]);
442 // The shortname does not change.
443 $backupid = $this->backup_course($c1->id
);
444 $restored = $this->restore_to_existing_course($backupid, $c2->id
, $u1->id
);
445 $this->assertEquals($c2->shortname
, $restored->shortname
);
446 $this->assertEquals('FN copy 1', $restored->fullname
);
447 $this->assertEquals('DESC', $restored->summary
);
448 $this->assertEquals(FORMAT_MOODLE
, $restored->summaryformat
);
451 public function test_restore_course_fullname_in_existing_course_without_permissions() {
453 $this->resetAfterTest();
454 $dg = $this->getDataGenerator();
455 $u1 = $dg->create_user();
457 $managers = get_archetype_roles('manager');
458 $manager = array_shift($managers);
459 $roleid = $this->create_role_with_caps('moodle/course:changefullname', CAP_PROHIBIT
);
460 $dg->role_assign($manager->id
, $u1->id
);
461 $dg->role_assign($roleid, $u1->id
);
463 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
]);
464 $c2 = $dg->create_course(['shortname' => 'A1', 'fullname' => 'B1', 'summary' => 'C1', 'summaryformat' => FORMAT_PLAIN
]);
466 // The fullname does not change.
467 $backupid = $this->backup_course($c1->id
);
468 $restored = $this->restore_to_existing_course($backupid, $c2->id
, $u1->id
);
469 $this->assertEquals('SN_1', $restored->shortname
);
470 $this->assertEquals($c2->fullname
, $restored->fullname
);
471 $this->assertEquals('DESC', $restored->summary
);
472 $this->assertEquals(FORMAT_MOODLE
, $restored->summaryformat
);
475 public function test_restore_course_summary_in_existing_course_without_permissions() {
477 $this->resetAfterTest();
478 $dg = $this->getDataGenerator();
479 $u1 = $dg->create_user();
481 $managers = get_archetype_roles('manager');
482 $manager = array_shift($managers);
483 $roleid = $this->create_role_with_caps('moodle/course:changesummary', CAP_PROHIBIT
);
484 $dg->role_assign($manager->id
, $u1->id
);
485 $dg->role_assign($roleid, $u1->id
);
487 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
]);
488 $c2 = $dg->create_course(['shortname' => 'A1', 'fullname' => 'B1', 'summary' => 'C1', 'summaryformat' => FORMAT_PLAIN
]);
490 // The summary and format do not change.
491 $backupid = $this->backup_course($c1->id
);
492 $restored = $this->restore_to_existing_course($backupid, $c2->id
, $u1->id
);
493 $this->assertEquals('SN_1', $restored->shortname
);
494 $this->assertEquals('FN copy 1', $restored->fullname
);
495 $this->assertEquals($c2->summary
, $restored->summary
);
496 $this->assertEquals($c2->summaryformat
, $restored->summaryformat
);
499 public function test_restore_course_startdate_in_existing_course_without_permissions() {
501 $this->resetAfterTest();
502 $dg = $this->getDataGenerator();
504 $u1 = $dg->create_user();
505 $managers = get_archetype_roles('manager');
506 $manager = array_shift($managers);
507 $roleid = $this->create_role_with_caps('moodle/restore:rolldates', CAP_PROHIBIT
);
508 $dg->role_assign($manager->id
, $u1->id
);
509 $dg->role_assign($roleid, $u1->id
);
511 // Create two courses with different start dates,in each course create a chat that opens 1 week after the course start date.
512 $startdate1 = mktime(12, 0, 0, 7, 1, 2016); // 01-Jul-2016.
513 $startdate2 = mktime(12, 0, 0, 1, 13, 2000); // 13-Jan-2000.
514 $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE
,
515 'startdate' => $startdate1]);
516 $chat1 = $dg->create_module('chat', ['name' => 'First', 'course' => $c1->id
, 'chattime' => $c1->startdate +
1 * WEEKSECS
]);
517 $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN
,
518 'startdate' => $startdate2]);
519 $chat2 = $dg->create_module('chat', ['name' => 'Second', 'course' => $c2->id
, 'chattime' => $c2->startdate +
1 * WEEKSECS
]);
521 // The startdate does not change.
522 $backupid = $this->backup_course($c1->id
);
523 $restored = $this->restore_to_existing_course($backupid, $c2->id
, $u1->id
);
524 $this->assertEquals('SN_1', $restored->shortname
);
525 $this->assertEquals('FN copy 1', $restored->fullname
);
526 $this->assertEquals('DESC', $restored->summary
);
527 $this->assertEquals(FORMAT_MOODLE
, $restored->summaryformat
);
528 $this->assertEquals($startdate2, $restored->startdate
);
530 // Now course c2 has two chats - one ('Second') was already there and one ('First') was restored from the backup.
531 // Start date of the restored chat ('First') was changed to be 1 week after the c2 start date.
532 $restoredchat1 = $DB->get_record('chat', ['name' => 'First', 'course' => $c2->id
]);
533 $restoredchat2 = $DB->get_record('chat', ['name' => 'Second', 'course' => $c2->id
]);
534 $this->assertNotEquals($chat1->chattime
, $restoredchat1->chattime
);
535 $this->assertEquals($chat2->chattime
, $restoredchat2->chattime
);
536 $this->assertEquals($c2->startdate +
1 * WEEKSECS
, $restoredchat2->chattime
);