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/**
18 * Course copy tests.
19 *
20 * @package    core_backup
21 * @copyright  2020 onward The Moodle Users Association <https://moodleassociation.org/>
22 * @author     Matt Porritt <mattp@catalyst-au.net>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25defined('MOODLE_INTERNAL') || die();
26
27global $CFG;
28require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
29require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
30require_once($CFG->libdir . '/completionlib.php');
31
32/**
33 * Course copy tests.
34 *
35 * @package    core_backup
36 * @copyright  2020 onward The Moodle Users Association <https://moodleassociation.org/>
37 * @author     Matt Porritt <mattp@catalyst-au.net>
38 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
40class core_backup_course_copy_testcase extends advanced_testcase {
41
42    /**
43     *
44     * @var \stdClass Course used for testing.
45     */
46    protected $course;
47
48    /**
49     *
50     * @var int User used to perform backups.
51     */
52    protected $userid;
53
54    /**
55     *
56     * @var array Ids of users in test course.
57     */
58    protected $courseusers;
59
60    /**
61     *
62     * @var array Names of the created activities.
63     */
64    protected $activitynames;
65
66    /**
67     * Set up tasks for all tests.
68     */
69    protected function setUp() {
70        global $DB, $CFG, $USER;
71
72        $this->resetAfterTest(true);
73
74        $CFG->enableavailability = true;
75        $CFG->enablecompletion = true;
76
77        // Create a course with some availability data set.
78        $generator = $this->getDataGenerator();
79        $course = $generator->create_course(
80            array('format' => 'topics', 'numsections' => 3,
81                'enablecompletion' => COMPLETION_ENABLED),
82            array('createsections' => true));
83        $forum = $generator->create_module('forum', array(
84            'course' => $course->id));
85        $forum2 = $generator->create_module('forum', array(
86            'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
87
88        // We need a grade, easiest is to add an assignment.
89        $assignrow = $generator->create_module('assign', array(
90            'course' => $course->id));
91        $assign = new assign(context_module::instance($assignrow->cmid), false, false);
92        $item = $assign->get_grade_item();
93
94        // Make a test grouping as well.
95        $grouping = $generator->create_grouping(array('courseid' => $course->id,
96            'name' => 'Grouping!'));
97
98        // Create some users.
99        $user1 = $generator->create_user();
100        $user2 = $generator->create_user();
101        $user3 = $generator->create_user();
102        $user4 = $generator->create_user();
103        $this->courseusers = array(
104            $user1->id, $user2->id, $user3->id, $user4->id
105        );
106
107        // Enrol users into the course.
108        $generator->enrol_user($user1->id, $course->id, 'student');
109        $generator->enrol_user($user2->id, $course->id, 'editingteacher');
110        $generator->enrol_user($user3->id, $course->id, 'manager');
111        $generator->enrol_user($user4->id, $course->id, 'editingteacher');
112        $generator->enrol_user($user4->id, $course->id, 'manager');
113
114        $availability = '{"op":"|","show":false,"c":[' .
115            '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
116            '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
117            '{"type":"grouping","id":' . $grouping->id . '}' .
118            ']}';
119        $DB->set_field('course_modules', 'availability', $availability, array(
120            'id' => $forum->cmid));
121        $DB->set_field('course_sections', 'availability', $availability, array(
122            'course' => $course->id, 'section' => 1));
123
124        // Add some user data to the course.
125        $discussion = $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
126            'forum' => $forum->id, 'userid' => $user1->id, 'timemodified' => time(),
127            'name' => 'Frog']);
128        $generator->get_plugin_generator('mod_forum')->create_post(['discussion' => $discussion->id, 'userid' => $user1->id]);
129
130        $this->course  = $course;
131        $this->userid = $USER->id; // Admin.
132        $this->activitynames = array(
133            $forum->name,
134            $forum2->name,
135            $assignrow->name
136        );
137
138        // Set the user doing the backup to be a manager in the course.
139        // By default Managers can restore courses AND users, teachers can only do users.
140        $this->setUser($user3);
141
142        // Disable all loggers.
143        $CFG->backup_error_log_logger_level = backup::LOG_NONE;
144        $CFG->backup_output_indented_logger_level = backup::LOG_NONE;
145        $CFG->backup_file_logger_level = backup::LOG_NONE;
146        $CFG->backup_database_logger_level = backup::LOG_NONE;
147        $CFG->backup_file_logger_level_extra = backup::LOG_NONE;
148    }
149
150    /**
151     * Test creating a course copy.
152     */
153    public function test_create_copy() {
154
155        // Mock up the form data.
156        $formdata = new \stdClass;
157        $formdata->courseid = $this->course->id;
158        $formdata->fullname = 'foo';
159        $formdata->shortname = 'bar';
160        $formdata->category = 1;
161        $formdata->visible = 1;
162        $formdata->startdate = 1582376400;
163        $formdata->enddate = 0;
164        $formdata->idnumber = 123;
165        $formdata->userdata = 1;
166        $formdata->role_1 = 1;
167        $formdata->role_3 = 3;
168        $formdata->role_5 = 5;
169
170        $coursecopy = new \core_backup\copy\copy($formdata);
171        $result = $coursecopy->create_copy();
172
173        // Load the controllers, to extract the data we need.
174        $bc = \backup_controller::load_controller($result['backupid']);
175        $rc = \restore_controller::load_controller($result['restoreid']);
176
177        // Check the backup controller.
178        $this->assertEquals($result, $bc->get_copy()->copyids);
179        $this->assertEquals(backup::MODE_COPY, $bc->get_mode());
180        $this->assertEquals($this->course->id, $bc->get_courseid());
181        $this->assertEquals(backup::TYPE_1COURSE, $bc->get_type());
182
183        // Check the restore controller.
184        $newcourseid = $rc->get_courseid();
185        $newcourse = get_course($newcourseid);
186
187        $this->assertEquals($result, $rc->get_copy()->copyids);
188        $this->assertEquals(get_string('copyingcourse', 'backup'), $newcourse->fullname);
189        $this->assertEquals(get_string('copyingcourseshortname', 'backup'), $newcourse->shortname);
190        $this->assertEquals(backup::MODE_COPY, $rc->get_mode());
191        $this->assertEquals($newcourseid, $rc->get_courseid());
192
193        // Check the created ad-hoc task.
194        $now = time();
195        $task = \core\task\manager::get_next_adhoc_task($now);
196
197        $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task);
198        $this->assertEquals($result, (array)$task->get_custom_data());
199        $this->assertFalse($task->is_blocking());
200
201        \core\task\manager::adhoc_task_complete($task);
202    }
203
204    /**
205     * Test getting the current copies.
206     */
207    public function test_get_copies() {
208        global $USER;
209
210        // Mock up the form data.
211        $formdata = new \stdClass;
212        $formdata->courseid = $this->course->id;
213        $formdata->fullname = 'foo';
214        $formdata->shortname = 'bar';
215        $formdata->category = 1;
216        $formdata->visible = 1;
217        $formdata->startdate = 1582376400;
218        $formdata->enddate = 0;
219        $formdata->idnumber = '';
220        $formdata->userdata = 1;
221        $formdata->role_1 = 1;
222        $formdata->role_3 = 3;
223        $formdata->role_5 = 5;
224
225        $formdata2 = clone($formdata);
226        $formdata2->shortname = 'tree';
227
228        // Create some copies.
229        $coursecopy = new \core_backup\copy\copy($formdata);
230        $result = $coursecopy->create_copy();
231
232        // Backup, awaiting.
233        $copies = \core_backup\copy\copy::get_copies($USER->id);
234        $this->assertEquals($result['backupid'], $copies[0]->backupid);
235        $this->assertEquals($result['restoreid'], $copies[0]->restoreid);
236        $this->assertEquals(\backup::STATUS_AWAITING, $copies[0]->status);
237        $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation);
238
239        $bc = \backup_controller::load_controller($result['backupid']);
240
241        // Backup, in progress.
242        $bc->set_status(\backup::STATUS_EXECUTING);
243        $copies = \core_backup\copy\copy::get_copies($USER->id);
244        $this->assertEquals($result['backupid'], $copies[0]->backupid);
245        $this->assertEquals($result['restoreid'], $copies[0]->restoreid);
246        $this->assertEquals(\backup::STATUS_EXECUTING, $copies[0]->status);
247        $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation);
248
249        // Restore, ready to process.
250        $bc->set_status(\backup::STATUS_FINISHED_OK);
251        $copies = \core_backup\copy\copy::get_copies($USER->id);
252        $this->assertEquals($result['backupid'], $copies[0]->backupid);
253        $this->assertEquals($result['restoreid'], $copies[0]->restoreid);
254        $this->assertEquals(\backup::STATUS_REQUIRE_CONV, $copies[0]->status);
255        $this->assertEquals(\backup::OPERATION_RESTORE, $copies[0]->operation);
256
257        // No records.
258        $bc->set_status(\backup::STATUS_FINISHED_ERR);
259        $copies = \core_backup\copy\copy::get_copies($USER->id);
260        $this->assertEmpty($copies);
261
262        $coursecopy2 = new \core_backup\copy\copy($formdata2);
263        $result2 = $coursecopy2->create_copy();
264        // Set the second copy to be complete.
265        $bc = \backup_controller::load_controller($result2['backupid']);
266        $bc->set_status(\backup::STATUS_FINISHED_OK);
267        // Set the restore to be finished.
268        $rc = \backup_controller::load_controller($result2['restoreid']);
269        $rc->set_status(\backup::STATUS_FINISHED_OK);
270
271        // No records.
272        $copies = \core_backup\copy\copy::get_copies($USER->id);
273        $this->assertEmpty($copies);
274    }
275
276    /**
277     * Test getting the current copies for specific course.
278     */
279    public function test_get_copies_course() {
280        global $USER;
281
282        // Mock up the form data.
283        $formdata = new \stdClass;
284        $formdata->courseid = $this->course->id;
285        $formdata->fullname = 'foo';
286        $formdata->shortname = 'bar';
287        $formdata->category = 1;
288        $formdata->visible = 1;
289        $formdata->startdate = 1582376400;
290        $formdata->enddate = 0;
291        $formdata->idnumber = '';
292        $formdata->userdata = 1;
293        $formdata->role_1 = 1;
294        $formdata->role_3 = 3;
295        $formdata->role_5 = 5;
296
297        // Create some copies.
298        $coursecopy = new \core_backup\copy\copy($formdata);
299        $coursecopy->create_copy();
300
301        // No copies match this course id.
302        $copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id + 1));
303        $this->assertEmpty($copies);
304    }
305
306    /**
307     * Test getting the current copies if course has been deleted.
308     */
309    public function test_get_copies_course_deleted() {
310        global $USER;
311
312        // Mock up the form data.
313        $formdata = new \stdClass;
314        $formdata->courseid = $this->course->id;
315        $formdata->fullname = 'foo';
316        $formdata->shortname = 'bar';
317        $formdata->category = 1;
318        $formdata->visible = 1;
319        $formdata->startdate = 1582376400;
320        $formdata->enddate = 0;
321        $formdata->idnumber = '';
322        $formdata->userdata = 1;
323        $formdata->role_1 = 1;
324        $formdata->role_3 = 3;
325        $formdata->role_5 = 5;
326
327        // Create some copies.
328        $coursecopy = new \core_backup\copy\copy($formdata);
329        $coursecopy->create_copy();
330
331        delete_course($this->course->id, false);
332
333        // No copies match this course id as it has been deleted.
334        $copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id));
335        $this->assertEmpty($copies);
336    }
337
338    /*
339     * Test course copy.
340     */
341    public function test_course_copy() {
342        global $DB;
343
344        // Mock up the form data.
345        $formdata = new \stdClass;
346        $formdata->courseid = $this->course->id;
347        $formdata->fullname = 'copy course';
348        $formdata->shortname = 'copy course short';
349        $formdata->category = 1;
350        $formdata->visible = 0;
351        $formdata->startdate = 1582376400;
352        $formdata->enddate = 1582386400;
353        $formdata->idnumber = 123;
354        $formdata->userdata = 1;
355        $formdata->role_1 = 1;
356        $formdata->role_3 = 3;
357        $formdata->role_5 = 5;
358
359        // Create the course copy records and associated ad-hoc task.
360        $coursecopy = new \core_backup\copy\copy($formdata);
361        $copyids = $coursecopy->create_copy();
362
363        $courseid = $this->course->id;
364
365        // We are expecting trace output during this test.
366        $this->expectOutputRegex("/$courseid/");
367
368        // Execute adhoc task.
369        $now = time();
370        $task = \core\task\manager::get_next_adhoc_task($now);
371        $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task);
372        $task->execute();
373        \core\task\manager::adhoc_task_complete($task);
374
375        $postbackuprec = $DB->get_record('backup_controllers', array('backupid' => $copyids['backupid']));
376        $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid']));
377
378        // Check backup was completed successfully.
379        $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status);
380        $this->assertEquals(1.0, $postbackuprec->progress);
381
382        // Check restore was completed successfully.
383        $this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status);
384        $this->assertEquals(1.0, $postrestorerec->progress);
385
386        // Check the restored course itself.
387        $coursecontext = context_course::instance($postrestorerec->itemid);
388        $users = get_enrolled_users($coursecontext);
389
390        $modinfo = get_fast_modinfo($postrestorerec->itemid);
391        $forums = $modinfo->get_instances_of('forum');
392        $forum = reset($forums);
393        $discussions = forum_get_discussions($forum);
394        $course = $modinfo->get_course();
395
396        $this->assertEquals($formdata->startdate, $course->startdate);
397        $this->assertEquals($formdata->enddate, $course->enddate);
398        $this->assertEquals('copy course', $course->fullname);
399        $this->assertEquals('copy course short',  $course->shortname);
400        $this->assertEquals(0,  $course->visible);
401        $this->assertEquals(123,  $course->idnumber);
402
403        foreach ($modinfo->get_cms() as $cm) {
404            $this->assertContains($cm->get_formatted_name(), $this->activitynames);
405        }
406
407        foreach ($this->courseusers as $user) {
408            $this->assertEquals($user, $users[$user]->id);
409        }
410
411        $this->assertEquals(count($this->courseusers), count($users));
412        $this->assertEquals(2, count($discussions));
413    }
414
415    /*
416     * Test course copy, not including any users (or data).
417     */
418    public function test_course_copy_no_users() {
419        global $DB;
420
421        // Mock up the form data.
422        $formdata = new \stdClass;
423        $formdata->courseid = $this->course->id;
424        $formdata->fullname = 'copy course';
425        $formdata->shortname = 'copy course short';
426        $formdata->category = 1;
427        $formdata->visible = 0;
428        $formdata->startdate = 1582376400;
429        $formdata->enddate = 1582386400;
430        $formdata->idnumber = 123;
431        $formdata->userdata = 1;
432        $formdata->role_1 = 0;
433        $formdata->role_3 = 0;
434        $formdata->role_5 = 0;
435
436        // Create the course copy records and associated ad-hoc task.
437        $coursecopy = new \core_backup\copy\copy($formdata);
438        $copyids = $coursecopy->create_copy();
439
440        $courseid = $this->course->id;
441
442        // We are expecting trace output during this test.
443        $this->expectOutputRegex("/$courseid/");
444
445        // Execute adhoc task.
446        $now = time();
447        $task = \core\task\manager::get_next_adhoc_task($now);
448        $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task);
449        $task->execute();
450        \core\task\manager::adhoc_task_complete($task);
451
452        $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid']));
453
454        // Check the restored course itself.
455        $coursecontext = context_course::instance($postrestorerec->itemid);
456        $users = get_enrolled_users($coursecontext);
457
458        $modinfo = get_fast_modinfo($postrestorerec->itemid);
459        $forums = $modinfo->get_instances_of('forum');
460        $forum = reset($forums);
461        $discussions = forum_get_discussions($forum);
462        $course = $modinfo->get_course();
463
464        $this->assertEquals($formdata->startdate, $course->startdate);
465        $this->assertEquals($formdata->enddate, $course->enddate);
466        $this->assertEquals('copy course', $course->fullname);
467        $this->assertEquals('copy course short',  $course->shortname);
468        $this->assertEquals(0,  $course->visible);
469        $this->assertEquals(123,  $course->idnumber);
470
471        foreach ($modinfo->get_cms() as $cm) {
472            $this->assertContains($cm->get_formatted_name(), $this->activitynames);
473        }
474
475        // Should be no discussions as the user that made them wasn't included.
476        $this->assertEquals(0, count($discussions));
477
478        // There should only be one user in the new course, and that's the user who did the copy.
479        $this->assertEquals(1, count($users));
480        $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id);
481
482    }
483
484    /*
485     * Test course copy, including students and their data.
486     */
487    public function test_course_copy_students_data() {
488        global $DB;
489
490        // Mock up the form data.
491        $formdata = new \stdClass;
492        $formdata->courseid = $this->course->id;
493        $formdata->fullname = 'copy course';
494        $formdata->shortname = 'copy course short';
495        $formdata->category = 1;
496        $formdata->visible = 0;
497        $formdata->startdate = 1582376400;
498        $formdata->enddate = 1582386400;
499        $formdata->idnumber = 123;
500        $formdata->userdata = 1;
501        $formdata->role_1 = 0;
502        $formdata->role_3 = 0;
503        $formdata->role_5 = 5;
504
505        // Create the course copy records and associated ad-hoc task.
506        $coursecopy = new \core_backup\copy\copy($formdata);
507        $copyids = $coursecopy->create_copy();
508
509        $courseid = $this->course->id;
510
511        // We are expecting trace output during this test.
512        $this->expectOutputRegex("/$courseid/");
513
514        // Execute adhoc task.
515        $now = time();
516        $task = \core\task\manager::get_next_adhoc_task($now);
517        $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task);
518        $task->execute();
519        \core\task\manager::adhoc_task_complete($task);
520
521        $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid']));
522
523        // Check the restored course itself.
524        $coursecontext = context_course::instance($postrestorerec->itemid);
525        $users = get_enrolled_users($coursecontext);
526
527        $modinfo = get_fast_modinfo($postrestorerec->itemid);
528        $forums = $modinfo->get_instances_of('forum');
529        $forum = reset($forums);
530        $discussions = forum_get_discussions($forum);
531        $course = $modinfo->get_course();
532
533        $this->assertEquals($formdata->startdate, $course->startdate);
534        $this->assertEquals($formdata->enddate, $course->enddate);
535        $this->assertEquals('copy course', $course->fullname);
536        $this->assertEquals('copy course short',  $course->shortname);
537        $this->assertEquals(0,  $course->visible);
538        $this->assertEquals(123,  $course->idnumber);
539
540        foreach ($modinfo->get_cms() as $cm) {
541            $this->assertContains($cm->get_formatted_name(), $this->activitynames);
542        }
543
544        // Should be no discussions as the user that made them wasn't included.
545        $this->assertEquals(2, count($discussions));
546
547        // There should only be two users in the new course. The copier and one student.
548        $this->assertEquals(2, count($users));
549        $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id);
550        $this->assertEquals($this->courseusers[0], $users[$this->courseusers[0]]->id);
551    }
552
553    /*
554     * Test course copy, not including any users (or data).
555     */
556    public function test_course_copy_no_data() {
557        global $DB;
558
559        // Mock up the form data.
560        $formdata = new \stdClass;
561        $formdata->courseid = $this->course->id;
562        $formdata->fullname = 'copy course';
563        $formdata->shortname = 'copy course short';
564        $formdata->category = 1;
565        $formdata->visible = 0;
566        $formdata->startdate = 1582376400;
567        $formdata->enddate = 1582386400;
568        $formdata->idnumber = 123;
569        $formdata->userdata = 0;
570        $formdata->role_1 = 1;
571        $formdata->role_3 = 3;
572        $formdata->role_5 = 5;
573
574        // Create the course copy records and associated ad-hoc task.
575        $coursecopy = new \core_backup\copy\copy($formdata);
576        $copyids = $coursecopy->create_copy();
577
578        $courseid = $this->course->id;
579
580        // We are expecting trace output during this test.
581        $this->expectOutputRegex("/$courseid/");
582
583        // Execute adhoc task.
584        $now = time();
585        $task = \core\task\manager::get_next_adhoc_task($now);
586        $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task);
587        $task->execute();
588        \core\task\manager::adhoc_task_complete($task);
589
590        $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid']));
591
592        // Check the restored course itself.
593        $coursecontext = context_course::instance($postrestorerec->itemid);
594        $users = get_enrolled_users($coursecontext);
595
596        get_fast_modinfo($postrestorerec->itemid, 0, true);
597        $modinfo = get_fast_modinfo($postrestorerec->itemid);
598        $forums = $modinfo->get_instances_of('forum');
599        $forum = reset($forums);
600        $discussions = forum_get_discussions($forum);
601        $course = $modinfo->get_course();
602
603        $this->assertEquals($formdata->startdate, $course->startdate);
604        $this->assertEquals($formdata->enddate, $course->enddate);
605        $this->assertEquals('copy course', $course->fullname);
606        $this->assertEquals('copy course short',  $course->shortname);
607        $this->assertEquals(0,  $course->visible);
608        $this->assertEquals(123,  $course->idnumber);
609
610        foreach ($modinfo->get_cms() as $cm) {
611            $this->assertContains($cm->get_formatted_name(), $this->activitynames);
612        }
613
614        // Should be no discussions as the user data wasn't included.
615        $this->assertEquals(0, count($discussions));
616
617        // There should only be all users in the new course.
618        $this->assertEquals(count($this->courseusers), count($users));
619    }
620
621    /*
622     * Test instantiation with incomplete formdata.
623     */
624    public function test_malformed_instantiation() {
625        // Mock up the form data, missing things so we get an exception.
626        $formdata = new \stdClass;
627        $formdata->courseid = $this->course->id;
628        $formdata->fullname = 'copy course';
629        $formdata->shortname = 'copy course short';
630        $formdata->category = 1;
631
632        // Expect and exception as form data is incomplete.
633        $this->expectException(\moodle_exception::class);
634        new \core_backup\copy\copy($formdata);
635    }
636}