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 * Automated backup tests.
19 *
20 * @package    core_backup
21 * @copyright  2019 John Yao <johnyao@catalyst-au.net>
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27global $CFG;
28require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
29require_once($CFG->libdir.'/cronlib.php');
30require_once($CFG->libdir . '/completionlib.php');
31
32/**
33 * Automated backup tests.
34 *
35 * @copyright  2019 John Yao <johnyao@catalyst-au.net>
36 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class core_backup_automated_backup_testcase extends advanced_testcase {
39    /**
40     * @var \backup_cron_automated_helper
41     */
42    protected $backupcronautomatedhelper;
43
44    /**
45     * @var stdClass $course
46     */
47    protected $course;
48
49    protected function setUp(): void {
50        global $DB, $CFG;
51
52        $this->resetAfterTest(true);
53        $this->setAdminUser();
54        $CFG->enableavailability = true;
55        $CFG->enablecompletion = true;
56
57        // Getting a testable backup_cron_automated_helper class.
58        $this->backupcronautomatedhelper = new test_backup_cron_automated_helper();
59
60        $generator = $this->getDataGenerator();
61        $this->course = $generator->create_course(
62                array('format' => 'topics', 'numsections' => 3,
63                        'enablecompletion' => COMPLETION_ENABLED),
64                array('createsections' => true));
65        $forum = $generator->create_module('forum', array(
66                'course' => $this->course->id));
67        $forum2 = $generator->create_module('forum', array(
68                'course' => $this->course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
69
70        // We need a grade, easiest is to add an assignment.
71        $assignrow = $generator->create_module('assign', array(
72                'course' => $this->course->id));
73        $assign = new assign(context_module::instance($assignrow->cmid), false, false);
74        $item = $assign->get_grade_item();
75
76        // Make a test grouping as well.
77        $grouping = $generator->create_grouping(array('courseid' => $this->course->id,
78                'name' => 'Grouping!'));
79
80        $availability = '{"op":"|","show":false,"c":[' .
81                '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
82                '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
83                '{"type":"grouping","id":' . $grouping->id . '}' .
84                ']}';
85        $DB->set_field('course_modules', 'availability', $availability, array(
86                'id' => $forum->cmid));
87        $DB->set_field('course_sections', 'availability', $availability, array(
88                'course' => $this->course->id, 'section' => 1));
89    }
90
91    /**
92     * Tests the automated backup run when the there is course backup should be skipped.
93     */
94    public function test_automated_backup_skipped_run() {
95        global $DB;
96
97        // Enable automated back up.
98        set_config('backup_auto_active', true, 'backup');
99        set_config('backup_auto_weekdays', '1111111', 'backup');
100
101        // Start backup process.
102        $admin = get_admin();
103
104        // Backup entry should not exist.
105        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
106        $this->assertFalse($backupcourse);
107        $this->assertInstanceOf(
108            backup_cron_automated_helper::class,
109            $this->backupcronautomatedhelper->return_this()
110        );
111
112        $classobject = $this->backupcronautomatedhelper->return_this();
113
114        $method = new ReflectionMethod('\backup_cron_automated_helper', 'get_courses');
115        $method->setAccessible(true); // Allow accessing of private method.
116        $courses = $method->invoke($classobject);
117
118        $method = new ReflectionMethod('\backup_cron_automated_helper', 'check_and_push_automated_backups');
119        $method->setAccessible(true); // Allow accessing of private method.
120        $emailpending = $method->invokeArgs($classobject, [$courses, $admin]);
121
122        $coursename = $this->course->fullname;
123        $this->expectOutputRegex("/Skipping $coursename \(Not scheduled for backup until/");
124        $this->assertFalse($emailpending);
125
126        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
127        $this->assertNotNull($backupcourse->laststatus);
128    }
129
130    /**
131     * Tests the automated backup run when the there is course backup can be pushed to adhoc task.
132     */
133    public function test_automated_backup_push_run() {
134        global $DB;
135
136        // Enable automated back up.
137        set_config('backup_auto_active', true, 'backup');
138        set_config('backup_auto_weekdays', '1111111', 'backup');
139
140        $admin = get_admin();
141
142        $classobject = $this->backupcronautomatedhelper->return_this();
143
144        $method = new ReflectionMethod('\backup_cron_automated_helper', 'get_courses');
145        $method->setAccessible(true); // Allow accessing of private method.
146        $courses = $method->invoke($classobject);
147
148        // Create this backup course.
149        $backupcourse = new stdClass;
150        $backupcourse->courseid = $this->course->id;
151        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
152        $DB->insert_record('backup_courses', $backupcourse);
153        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
154
155        // We now manually trigger a backup pushed to adhoc task.
156        // Make sure is in the past, which means should run now.
157        $backupcourse->nextstarttime = time() - 10;
158        $DB->update_record('backup_courses', $backupcourse);
159
160        $method = new ReflectionMethod('\backup_cron_automated_helper', 'check_and_push_automated_backups');
161        $method->setAccessible(true); // Allow accessing of private method.
162        $emailpending = $method->invokeArgs($classobject, [$courses, $admin]);
163        $this->assertTrue($emailpending);
164
165        $coursename = $this->course->fullname;
166        $this->expectOutputRegex("/Putting backup of $coursename in adhoc task queue/");
167
168        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
169        // Now this backup course status should be queued.
170        $this->assertEquals(backup_cron_automated_helper::BACKUP_STATUS_QUEUED, $backupcourse->laststatus);
171    }
172
173    /**
174     * Tests the automated backup inactive run.
175     */
176    public function test_inactive_run() {
177        backup_cron_automated_helper::run_automated_backup();
178        $this->expectOutputString("Checking automated backup status...INACTIVE\n");
179    }
180
181    /**
182     * Tests the invisible course being skipped.
183     */
184    public function test_should_skip_invisible_course() {
185        global $DB;
186
187        set_config('backup_auto_active', true, 'backup');
188        set_config('backup_auto_skip_hidden', true, 'backup');
189        set_config('backup_auto_weekdays', '1111111', 'backup');
190        // Create this backup course.
191        $backupcourse = new stdClass;
192        $backupcourse->courseid = $this->course->id;
193        // This is the status we believe last run was OK.
194        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
195        $DB->insert_record('backup_courses', $backupcourse);
196        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
197
198        $this->assertTrue(course_change_visibility($this->course->id, false));
199        $course = $DB->get_record('course', array('id' => $this->course->id));
200        $this->assertEquals('0', $course->visible);
201        $classobject = $this->backupcronautomatedhelper->return_this();
202        $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, time());
203
204        $method = new ReflectionMethod('\backup_cron_automated_helper', 'should_skip_course_backup');
205        $method->setAccessible(true); // Allow accessing of private method.
206        $skipped = $method->invokeArgs($classobject, [$backupcourse, $course, $nextstarttime]);
207
208        $this->assertTrue($skipped);
209        $this->expectOutputRegex("/Skipping $course->fullname \(Not visible\)/");
210    }
211
212    /**
213     * Tests the not modified course being skipped.
214     */
215    public function test_should_skip_not_modified_course_in_days() {
216        global $DB;
217
218        set_config('backup_auto_active', true, 'backup');
219        // Skip if not modified in two days.
220        set_config('backup_auto_skip_modif_days', 2, 'backup');
221        set_config('backup_auto_weekdays', '1111111', 'backup');
222
223        // Create this backup course.
224        $backupcourse = new stdClass;
225        $backupcourse->courseid = $this->course->id;
226        // This is the status we believe last run was OK.
227        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
228        $backupcourse->laststarttime = time() - 2 * DAYSECS;
229        $backupcourse->lastendtime = time() - 1 * DAYSECS;
230        $DB->insert_record('backup_courses', $backupcourse);
231        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
232        $course = $DB->get_record('course', array('id' => $this->course->id));
233
234        $course->timemodified = time() - 2 * DAYSECS - 1;
235
236        $classobject = $this->backupcronautomatedhelper->return_this();
237        $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, time());
238
239        $method = new ReflectionMethod('\backup_cron_automated_helper', 'should_skip_course_backup');
240        $method->setAccessible(true); // Allow accessing of private method.
241        $skipped = $method->invokeArgs($classobject, [$backupcourse, $course, $nextstarttime]);
242
243        $this->assertTrue($skipped);
244        $this->expectOutputRegex("/Skipping $course->fullname \(Not modified in the past 2 days\)/");
245    }
246
247    /**
248     * Tests the backup not modified course being skipped.
249     */
250    public function test_should_skip_not_modified_course_since_prev() {
251        global $DB;
252
253        set_config('backup_auto_active', true, 'backup');
254        // Skip if not modified in two days.
255        set_config('backup_auto_skip_modif_prev', 2, 'backup');
256        set_config('backup_auto_weekdays', '1111111', 'backup');
257
258        // Create this backup course.
259        $backupcourse = new stdClass;
260        $backupcourse->courseid = $this->course->id;
261        // This is the status we believe last run was OK.
262        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
263        $backupcourse->laststarttime = time() - 2 * DAYSECS;
264        $backupcourse->lastendtime = time() - 1 * DAYSECS;
265        $DB->insert_record('backup_courses', $backupcourse);
266        $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id));
267        $course = $DB->get_record('course', array('id' => $this->course->id));
268
269        $course->timemodified = time() - 2 * DAYSECS - 1;
270
271        $classobject = $this->backupcronautomatedhelper->return_this();
272        $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, time());
273
274        $method = new ReflectionMethod('\backup_cron_automated_helper', 'should_skip_course_backup');
275        $method->setAccessible(true); // Allow accessing of private method.
276        $skipped = $method->invokeArgs($classobject, [$backupcourse, $course, $nextstarttime]);
277
278        $this->assertTrue($skipped);
279        $this->expectOutputRegex("/Skipping $course->fullname \(Not modified since previous backup\)/");
280    }
281
282    /**
283     * Test the task completes when coureid is missing.
284     */
285    public function test_task_complete_when_courseid_is_missing() {
286        global $DB;
287        $admin = get_admin();
288        $classobject = $this->backupcronautomatedhelper->return_this();
289
290        // Create this backup course.
291        $backupcourse = new stdClass;
292        $backupcourse->courseid = $this->course->id;
293        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
294        $DB->insert_record('backup_courses', $backupcourse);
295        $backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
296
297        // Create a backup task.
298        $method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
299        $method->setAccessible(true); // Allow accessing of private method.
300        $method->invokeArgs($classobject, [$backupcourse, $admin]);
301
302        // Delete course for this test.
303        delete_course($this->course->id, false);
304
305        $task = core\task\manager::get_next_adhoc_task(time());
306
307        ob_start();
308        $task->execute();
309        $output = ob_get_clean();
310
311        $this->assertStringContainsString('Invalid course id: ' . $this->course->id . ', task aborted.', $output);
312        core\task\manager::adhoc_task_complete($task);
313    }
314
315    /**
316     * Test the task completes when backup course is missing.
317     */
318    public function test_task_complete_when_backup_course_is_missing() {
319        global $DB;
320        $admin = get_admin();
321        $classobject = $this->backupcronautomatedhelper->return_this();
322
323        // Create this backup course.
324        $backupcourse = new stdClass;
325        $backupcourse->courseid = $this->course->id;
326        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
327        $DB->insert_record('backup_courses', $backupcourse);
328        $backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
329
330        // Create a backup task.
331        $method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
332        $method->setAccessible(true); // Allow accessing of private method.
333        $method->invokeArgs($classobject, [$backupcourse, $admin]);
334
335        // Delete backup course for this test.
336        $DB->delete_records('backup_courses', ['courseid' => $this->course->id]);
337
338        $task = core\task\manager::get_next_adhoc_task(time());
339
340        ob_start();
341        $task->execute();
342        $output = ob_get_clean();
343
344        $this->assertStringContainsString('Automated backup for course: ' . $this->course->fullname . ' encounters an error.',
345            $output);
346        core\task\manager::adhoc_task_complete($task);
347    }
348}
349
350/**
351 * New backup_cron_automated_helper class for testing.
352 *
353 * This class extends the helper backup_cron_automated_helper class
354 * in order to utilise abstract class for testing.
355 *
356 * @package    core
357 * @copyright  2019 John Yao <johnyao@catalyst-au.net>
358 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
359 */
360class test_backup_cron_automated_helper extends backup_cron_automated_helper {
361    /**
362     * Returning this for testing.
363     */
364    public function return_this() {
365        return $this;
366    }
367}
368