MDL-63902 dataprivacy: Check course children not the course
[moodle.git] / admin / tool / dataprivacy / tests / expired_contexts_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  * Expired contexts tests.
19  *
20  * @package    tool_dataprivacy
21  * @copyright  2018 David Monllao
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 use tool_dataprivacy\api;
26 use tool_dataprivacy\data_registry;
27 use tool_dataprivacy\expired_context;
28 use tool_dataprivacy\purpose;
29 use tool_dataprivacy\purpose_override;
30 use tool_dataprivacy\category;
31 use tool_dataprivacy\contextlevel;
32 use tool_dataprivacy\expired_contexts_manager;
34 defined('MOODLE_INTERNAL') || die();
35 global $CFG;
37 /**
38  * Expired contexts tests.
39  *
40  * @package    tool_dataprivacy
41  * @copyright  2018 David Monllao
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
46     /**
47      * Setup the basics with the specified retention period.
48      *
49      * @param   string  $system Retention policy for the system.
50      * @param   string  $user Retention policy for users.
51      * @param   string  $course Retention policy for courses.
52      * @param   string  $activity Retention policy for activities.
53      */
54     protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass {
55         $this->resetAfterTest();
57         $purposes = (object) [
58             'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM),
59             'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER),
60         ];
62         if (null !== $course) {
63             $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
64         }
66         if (null !== $activity) {
67             $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
68         }
70         return $purposes;
71     }
73     /**
74      * Create a retention period and set it for the specified context level.
75      *
76      * @param   string  $retention
77      * @param   int     $contextlevel
78      * @return  purpose
79      */
80     protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
81         $purpose = new purpose(0, (object) [
82             'name' => 'Test purpose ' . rand(1, 1000),
83             'retentionperiod' => $retention,
84             'lawfulbases' => 'gdpr_art_6_1_a',
85         ]);
86         $purpose->create();
88         $cat = new category(0, (object) ['name' => 'Test category']);
89         $cat->create();
91         if ($contextlevel <= CONTEXT_USER) {
92             $record = (object) [
93                 'purposeid'     => $purpose->get('id'),
94                 'categoryid'    => $cat->get('id'),
95                 'contextlevel'  => $contextlevel,
96             ];
97             api::set_contextlevel($record);
98         } else {
99             list($purposevar, ) = data_registry::var_names_from_context(
100                     \context_helper::get_class_for_level($contextlevel)
101                 );
102             set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
103         }
105         return $purpose;
106     }
108     /**
109      * Ensure that a user with no lastaccess is not flagged for deletion.
110      */
111     public function test_flag_not_setup() {
112         $this->resetAfterTest();
114         $user = $this->getDataGenerator()->create_user();
116         $this->setUser($user);
117         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
118         $context = \context_block::instance($block->instance->id);
119         $this->setUser();
121         // Flag all expired contexts.
122         $manager = new \tool_dataprivacy\expired_contexts_manager();
123         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
125         $this->assertEquals(0, $flaggedcourses);
126         $this->assertEquals(0, $flaggedusers);
127     }
129     /**
130      * Ensure that a user with no lastaccess is not flagged for deletion.
131      */
132     public function test_flag_user_no_lastaccess() {
133         $this->resetAfterTest();
135         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
137         $user = $this->getDataGenerator()->create_user();
139         $this->setUser($user);
140         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
141         $context = \context_block::instance($block->instance->id);
142         $this->setUser();
144         // Flag all expired contexts.
145         $manager = new \tool_dataprivacy\expired_contexts_manager();
146         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
148         $this->assertEquals(0, $flaggedcourses);
149         $this->assertEquals(0, $flaggedusers);
150     }
152     /**
153      * Ensure that a user with a recent lastaccess is not flagged for deletion.
154      */
155     public function test_flag_user_recent_lastaccess() {
156         $this->resetAfterTest();
158         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
160         $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
162         $this->setUser($user);
163         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
164         $context = \context_block::instance($block->instance->id);
165         $this->setUser();
167         // Flag all expired contexts.
168         $manager = new \tool_dataprivacy\expired_contexts_manager();
169         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
171         $this->assertEquals(0, $flaggedcourses);
172         $this->assertEquals(0, $flaggedusers);
173     }
175     /**
176      * Ensure that a user with a lastaccess in the past is flagged for deletion.
177      */
178     public function test_flag_user_past_lastaccess() {
179         $this->resetAfterTest();
181         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
183         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
185         $this->setUser($user);
186         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
187         $context = \context_block::instance($block->instance->id);
188         $this->setUser();
190         // Flag all expired contexts.
191         $manager = new \tool_dataprivacy\expired_contexts_manager();
192         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
194         // Although there is a block in the user context, everything in the user context is regarded as one.
195         $this->assertEquals(0, $flaggedcourses);
196         $this->assertEquals(1, $flaggedusers);
197     }
199     /**
200      * Ensure that a user with a lastaccess in the past but active enrolments is not flagged for deletion.
201      */
202     public function test_flag_user_past_lastaccess_still_enrolled() {
203         $this->resetAfterTest();
205         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
207         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
208         $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enddate' => time() + YEARSECS]);
209         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
211         $otheruser = $this->getDataGenerator()->create_user();
212         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
214         $this->setUser($user);
215         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
216         $context = \context_block::instance($block->instance->id);
217         $this->setUser();
219         // Flag all expired contexts.
220         $manager = new \tool_dataprivacy\expired_contexts_manager();
221         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
223         $this->assertEquals(0, $flaggedcourses);
224         $this->assertEquals(0, $flaggedusers);
225     }
227     /**
228      * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion.
229      */
230     public function test_flag_user_update_existing() {
231         $this->resetAfterTest();
233         $this->setup_basics('PT1H', 'PT1H', 'P5Y');
235         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
236         $usercontext = \context_user::instance($user->id);
238         // Create an existing expired_context.
239         $expiredcontext = new expired_context(0, (object) [
240                 'contextid' => $usercontext->id,
241                 'defaultexpired' => 0,
242                 'status' => expired_context::STATUS_EXPIRED,
243             ]);
244         $expiredcontext->save();
245         $this->assertEquals(0, $expiredcontext->get('defaultexpired'));
247         // Flag all expired contexts.
248         $manager = new \tool_dataprivacy\expired_contexts_manager();
249         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
251         $this->assertEquals(0, $flaggedcourses);
252         $this->assertEquals(1, $flaggedusers);
254         // The user context will now have expired.
255         $updatedcontext = new expired_context($expiredcontext->get('id'));
256         $this->assertEquals(1, $updatedcontext->get('defaultexpired'));
257     }
259     /**
260      * Ensure that a user with a lastaccess in the past and expired enrolments.
261      */
262     public function test_flag_user_past_lastaccess_unexpired_past_enrolment() {
263         $this->resetAfterTest();
265         $this->setup_basics('PT1H', 'PT1H', 'P1Y');
267         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
268         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
269         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
271         $otheruser = $this->getDataGenerator()->create_user();
272         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
274         $this->setUser($user);
275         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
276         $context = \context_block::instance($block->instance->id);
277         $this->setUser();
279         // Flag all expired contexts.
280         $manager = new \tool_dataprivacy\expired_contexts_manager();
281         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
283         $this->assertEquals(0, $flaggedcourses);
284         $this->assertEquals(0, $flaggedusers);
285     }
287     /**
288      * Ensure that a user with a lastaccess in the past and expired enrolments.
289      */
290     public function test_flag_user_past_override_role() {
291         global $DB;
292         $this->resetAfterTest();
294         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
296         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
297         $usercontext = \context_user::instance($user->id);
298         $systemcontext = \context_system::instance();
300         $role = $DB->get_record('role', ['shortname' => 'manager']);
302         $override = new purpose_override(0, (object) [
303                 'purposeid' => $purposes->user->get('id'),
304                 'roleid' => $role->id,
305                 'retentionperiod' => 'P5Y',
306             ]);
307         $override->save();
308         role_assign($role->id, $user->id, $systemcontext->id);
310         // Flag all expired contexts.
311         $manager = new \tool_dataprivacy\expired_contexts_manager();
312         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
314         $this->assertEquals(0, $flaggedcourses);
315         $this->assertEquals(0, $flaggedusers);
317         $expiredrecord = expired_context::get_record(['contextid' => $usercontext->id]);
318         $this->assertFalse($expiredrecord);
319     }
321     /**
322      * Ensure that a user with a lastaccess in the past and expired enrolments.
323      */
324     public function test_flag_user_past_lastaccess_expired_enrolled() {
325         $this->resetAfterTest();
327         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
329         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
330         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
331         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
333         $otheruser = $this->getDataGenerator()->create_user();
334         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
336         $this->setUser($user);
337         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
338         $context = \context_block::instance($block->instance->id);
339         $this->setUser();
341         // Flag all expired contexts.
342         $manager = new \tool_dataprivacy\expired_contexts_manager();
343         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
345         $this->assertEquals(1, $flaggedcourses);
346         $this->assertEquals(1, $flaggedusers);
347     }
349     /**
350      * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
351      * correctly.
352      */
353     public function test_flag_user_past_lastaccess_missing_enddate_required() {
354         $this->resetAfterTest();
356         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
358         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
359         $course = $this->getDataGenerator()->create_course();
360         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
362         $otheruser = $this->getDataGenerator()->create_user();
363         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
365         $this->setUser($user);
366         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
367         $context = \context_block::instance($block->instance->id);
368         $this->setUser();
370         // Ensure that course end dates are not required.
371         set_config('requireallenddatesforuserdeletion', 1, 'tool_dataprivacy');
373         // Flag all expired contexts.
374         $manager = new \tool_dataprivacy\expired_contexts_manager();
375         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
377         $this->assertEquals(0, $flaggedcourses);
378         $this->assertEquals(0, $flaggedusers);
379     }
381     /**
382      * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
383      * correctly when the end date is not required.
384      */
385     public function test_flag_user_past_lastaccess_missing_enddate_not_required() {
386         $this->resetAfterTest();
388         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
390         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
391         $course = $this->getDataGenerator()->create_course();
392         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
394         $otheruser = $this->getDataGenerator()->create_user();
395         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
397         $this->setUser($user);
398         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
399         $context = \context_block::instance($block->instance->id);
400         $this->setUser();
402         // Ensure that course end dates are required.
403         set_config('requireallenddatesforuserdeletion', 0, 'tool_dataprivacy');
405         // Flag all expired contexts.
406         $manager = new \tool_dataprivacy\expired_contexts_manager();
407         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
409         $this->assertEquals(0, $flaggedcourses);
410         $this->assertEquals(1, $flaggedusers);
411     }
413     /**
414      * Ensure that a user with a recent lastaccess is not flagged for deletion.
415      */
416     public function test_flag_user_recent_lastaccess_existing_record() {
417         $this->resetAfterTest();
419         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
421         $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
422         $usercontext = \context_user::instance($user->id);
424         // Create an existing expired_context.
425         $expiredcontext = new expired_context(0, (object) [
426                 'contextid' => $usercontext->id,
427                 'status' => expired_context::STATUS_EXPIRED,
428             ]);
429         $expiredcontext->save();
431         $this->setUser($user);
432         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
433         $context = \context_block::instance($block->instance->id);
434         $this->setUser();
436         // Flag all expired contexts.
437         $manager = new \tool_dataprivacy\expired_contexts_manager();
438         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
440         $this->assertEquals(0, $flaggedcourses);
441         $this->assertEquals(0, $flaggedusers);
443         $this->expectException('dml_missing_record_exception');
444         new expired_context($expiredcontext->get('id'));
445     }
447     /**
448      * Ensure that a user with a recent lastaccess is not flagged for deletion.
449      */
450     public function test_flag_user_retention_changed() {
451         $this->resetAfterTest();
453         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
455         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
456         $usercontext = \context_user::instance($user->id);
458         $this->setUser($user);
459         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
460         $context = \context_block::instance($block->instance->id);
461         $this->setUser();
463         // Flag all expired contexts.
464         $manager = new \tool_dataprivacy\expired_contexts_manager();
465         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
467         $this->assertEquals(0, $flaggedcourses);
468         $this->assertEquals(1, $flaggedusers);
470         $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
471         $this->assertNotFalse($expiredcontext);
473         // Increase the retention period to 5 years.
474         $purposes->user->set('retentionperiod', 'P5Y');
475         $purposes->user->save();
477         // Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased.
478         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
479         $this->assertEquals(0, $flaggedcourses);
480         $this->assertEquals(0, $flaggedusers);
482         // The expiry record will now have been removed.
483         $this->expectException('dml_missing_record_exception');
484         new expired_context($expiredcontext->get('id'));
485     }
487     /**
488      * Ensure that a user with a historically expired expired block record child is cleaned up.
489      */
490     public function test_flag_user_historic_block_unapproved() {
491         $this->resetAfterTest();
493         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
495         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
496         $usercontext = \context_user::instance($user->id);
498         $this->setUser($user);
499         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
500         $blockcontext = \context_block::instance($block->instance->id);
501         $this->setUser();
503         // Create an existing expired_context which has not been approved for the block.
504         $expiredcontext = new expired_context(0, (object) [
505                 'contextid' => $blockcontext->id,
506                 'status' => expired_context::STATUS_EXPIRED,
507             ]);
508         $expiredcontext->save();
510         // Flag all expired contexts.
511         $manager = new \tool_dataprivacy\expired_contexts_manager();
512         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
514         $this->assertEquals(0, $flaggedcourses);
515         $this->assertEquals(1, $flaggedusers);
517         $expiredblockcontext = expired_context::get_record(['contextid' => $blockcontext->id]);
518         $this->assertFalse($expiredblockcontext);
520         $expiredusercontext = expired_context::get_record(['contextid' => $usercontext->id]);
521         $this->assertNotFalse($expiredusercontext);
522     }
524     /**
525      * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
526      */
527     public function test_flag_user_historic_unexpired_child() {
528         $this->resetAfterTest();
530         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
531         $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
533         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
534         $usercontext = \context_user::instance($user->id);
536         $this->setUser($user);
537         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
538         $blockcontext = \context_block::instance($block->instance->id);
539         $this->setUser();
541         // Flag all expired contexts.
542         $manager = new \tool_dataprivacy\expired_contexts_manager();
543         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
545         $this->assertEquals(0, $flaggedcourses);
546         $this->assertEquals(1, $flaggedusers);
548         $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
549         $this->assertNotFalse($expiredcontext);
550     }
552     /**
553      * Ensure that a course with no end date is not flagged.
554      */
555     public function test_flag_course_no_enddate() {
556         $this->resetAfterTest();
558         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
560         $course = $this->getDataGenerator()->create_course();
561         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
563         // Flag all expired contexts.
564         $manager = new \tool_dataprivacy\expired_contexts_manager();
565         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
567         $this->assertEquals(0, $flaggedcourses);
568         $this->assertEquals(0, $flaggedusers);
569     }
571     /**
572      * Ensure that a course with an end date in the distant past, but a child which is unexpired is not flagged.
573      */
574     public function test_flag_course_past_enddate_future_child() {
575         $this->resetAfterTest();
577         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'P5Y');
579         $course = $this->getDataGenerator()->create_course([
580                 'startdate' => time() - (2 * YEARSECS),
581                 'enddate' => time() - YEARSECS,
582             ]);
583         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
585         // Flag all expired contexts.
586         $manager = new \tool_dataprivacy\expired_contexts_manager();
587         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
589         $this->assertEquals(0, $flaggedcourses);
590         $this->assertEquals(0, $flaggedusers);
591     }
593     /**
594      * Ensure that a course with an end date in the distant past is flagged.
595      */
596     public function test_flag_course_past_enddate() {
597         $this->resetAfterTest();
599         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
601         $course = $this->getDataGenerator()->create_course([
602                 'startdate' => time() - (2 * YEARSECS),
603                 'enddate' => time() - YEARSECS,
604             ]);
605         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
607         // Flag all expired contexts.
608         $manager = new \tool_dataprivacy\expired_contexts_manager();
609         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
611         $this->assertEquals(2, $flaggedcourses);
612         $this->assertEquals(0, $flaggedusers);
613     }
615     /**
616      * Ensure that a course with an end date in the distant past is flagged.
617      */
618     public function test_flag_course_past_enddate_multiple() {
619         $this->resetAfterTest();
621         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
623         $course1 = $this->getDataGenerator()->create_course([
624                 'startdate' => time() - (2 * YEARSECS),
625                 'enddate' => time() - YEARSECS,
626             ]);
627         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
629         $course2 = $this->getDataGenerator()->create_course([
630                 'startdate' => time() - (2 * YEARSECS),
631                 'enddate' => time() - YEARSECS,
632             ]);
633         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
635         // Flag all expired contexts.
636         $manager = new \tool_dataprivacy\expired_contexts_manager();
637         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
639         $this->assertEquals(4, $flaggedcourses);
640         $this->assertEquals(0, $flaggedusers);
641     }
643     /**
644      * Ensure that a course with an end date in the future is not flagged.
645      */
646     public function test_flag_course_future_enddate() {
647         $this->resetAfterTest();
649         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
651         $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
652         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
654         // Flag all expired contexts.
655         $manager = new \tool_dataprivacy\expired_contexts_manager();
656         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
658         $this->assertEquals(0, $flaggedcourses);
659         $this->assertEquals(0, $flaggedusers);
660     }
662     /**
663      * Ensure that a course with an end date in the future is not flagged.
664      */
665     public function test_flag_course_recent_unexpired_enddate() {
666         $this->resetAfterTest();
668         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
670         $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);
672         // Flag all expired contexts.
673         $manager = new \tool_dataprivacy\expired_contexts_manager();
674         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
676         $this->assertEquals(0, $flaggedcourses);
677         $this->assertEquals(0, $flaggedusers);
678     }
680     /**
681      * Ensure that a course with an end date in the distant past is flagged, taking into account any purpose override
682      */
683     public function test_flag_course_past_enddate_with_override_unexpired_role() {
684         global $DB;
685         $this->resetAfterTest();
687         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
689         $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
691         $override = new purpose_override(0, (object) [
692                 'purposeid' => $purposes->course->get('id'),
693                 'roleid' => $role->id,
694                 'retentionperiod' => 'P5Y',
695             ]);
696         $override->save();
698         $course = $this->getDataGenerator()->create_course([
699                 'startdate' => time() - (2 * DAYSECS),
700                 'enddate' => time() - DAYSECS,
701             ]);
702         $coursecontext = \context_course::instance($course->id);
704         // Flag all expired contexts.
705         $manager = new \tool_dataprivacy\expired_contexts_manager();
706         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
708         $this->assertEquals(1, $flaggedcourses);
709         $this->assertEquals(0, $flaggedusers);
711         $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
712         $this->assertEmpty($expiredrecord->get('expiredroles'));
714         $unexpiredroles = $expiredrecord->get('unexpiredroles');
715         $this->assertCount(1, $unexpiredroles);
716         $this->assertContains($role->id, $unexpiredroles);
717     }
719     /**
720      * Ensure that a course with an end date in the distant past is flagged, and any expired role is ignored.
721      */
722     public function test_flag_course_past_enddate_with_override_expired_role() {
723         global $DB;
724         $this->resetAfterTest();
726         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
728         $role = $DB->get_record('role', ['shortname' => 'student']);
730         // The role has a much shorter retention, but both should match.
731         $override = new purpose_override(0, (object) [
732                 'purposeid' => $purposes->course->get('id'),
733                 'roleid' => $role->id,
734                 'retentionperiod' => 'PT1M',
735             ]);
736         $override->save();
738         $course = $this->getDataGenerator()->create_course([
739                 'startdate' => time() - (2 * DAYSECS),
740                 'enddate' => time() - DAYSECS,
741             ]);
742         $coursecontext = \context_course::instance($course->id);
744         // Flag all expired contexts.
745         $manager = new \tool_dataprivacy\expired_contexts_manager();
746         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
748         $this->assertEquals(1, $flaggedcourses);
749         $this->assertEquals(0, $flaggedusers);
751         $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
752         $this->assertEmpty($expiredrecord->get('expiredroles'));
753         $this->assertEmpty($expiredrecord->get('unexpiredroles'));
754         $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
755     }
757     /**
758      * Ensure that where a course has explicitly expired one role, but that role is explicitly not expired in a child
759      * context, does not have the parent context role expired.
760      */
761     public function test_flag_course_override_expiredwith_override_unexpired_on_child() {
762         global $DB;
763         $this->resetAfterTest();
765         $purposes = $this->setup_basics('P1Y', 'P1Y', 'P1Y');
767         $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
769         (new purpose_override(0, (object) [
770                 'purposeid' => $purposes->course->get('id'),
771                 'roleid' => $role->id,
772                 'retentionperiod' => 'PT1S',
773             ]))->save();
775         $modpurpose = new purpose(0, (object) [
776             'name' => 'Module purpose',
777             'retentionperiod' => 'PT1S',
778             'lawfulbases' => 'gdpr_art_6_1_a',
779         ]);
780         $modpurpose->create();
782         (new purpose_override(0, (object) [
783                 'purposeid' => $modpurpose->get('id'),
784                 'roleid' => $role->id,
785                 'retentionperiod' => 'P5Y',
786             ]))->save();
788         $course = $this->getDataGenerator()->create_course([
789                 'startdate' => time() - (2 * DAYSECS),
790                 'enddate' => time() - DAYSECS,
791             ]);
792         $coursecontext = \context_course::instance($course->id);
794         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
795         $cm = get_coursemodule_from_instance('forum', $forum->id);
796         $forumcontext = \context_module::instance($cm->id);
798         api::set_context_instance((object) [
799                 'contextid' => $forumcontext->id,
800                 'purposeid' => $modpurpose->get('id'),
801                 'categoryid' => 0,
802             ]);
804         // Flag all expired contexts.
805         $manager = new \tool_dataprivacy\expired_contexts_manager();
806         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
808         $this->assertEquals(1, $flaggedcourses);
809         $this->assertEquals(0, $flaggedusers);
811         // The course will not be expired as the default expiry has not passed, and the explicit role override has been
812         // removed due to the child non-expiry.
813         $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
814         $this->assertFalse($expiredrecord);
816         // The forum will have an expiry for all _but_ the overridden role.
817         $expiredrecord = expired_context::get_record(['contextid' => $forumcontext->id]);
818         $this->assertEmpty($expiredrecord->get('expiredroles'));
820         // The teacher is not expired.
821         $unexpiredroles = $expiredrecord->get('unexpiredroles');
822         $this->assertCount(1, $unexpiredroles);
823         $this->assertContains($role->id, $unexpiredroles);
824         $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
825     }
827     /**
828      * Ensure that a user context previously flagged as approved is not removed if the user has any unexpired roles.
829      */
830     public function test_process_user_context_with_override_unexpired_role() {
831         global $DB;
832         $this->resetAfterTest();
834         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
836         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
837         $usercontext = \context_user::instance($user->id);
838         $systemcontext = \context_system::instance();
840         $role = $DB->get_record('role', ['shortname' => 'manager']);
842         $override = new purpose_override(0, (object) [
843                 'purposeid' => $purposes->user->get('id'),
844                 'roleid' => $role->id,
845                 'retentionperiod' => 'P5Y',
846             ]);
847         $override->save();
848         role_assign($role->id, $user->id, $systemcontext->id);
850         // Create an existing expired_context.
851         $expiredcontext = new expired_context(0, (object) [
852                 'contextid' => $usercontext->id,
853                 'defaultexpired' => 1,
854                 'status' => expired_context::STATUS_APPROVED,
855             ]);
856         $expiredcontext->add_unexpiredroles([$role->id]);
857         $expiredcontext->save();
859         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
860             ->setMethods([
861                 'delete_data_for_user',
862                 'delete_data_for_users_in_context',
863                 'delete_data_for_all_users_in_context',
864             ])
865             ->getMock();
866         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
867         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
868         $mockprivacymanager->expects($this->never())->method('delete_data_for_users_in_context');
870         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
871             ->setMethods(['get_privacy_manager'])
872             ->getMock();
874         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
875         $manager->set_progress(new \null_progress_trace());
876         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
878         $this->assertEquals(0, $processedcourses);
879         $this->assertEquals(0, $processedusers);
881         $this->expectException('dml_missing_record_exception');
882         $updatedcontext = new expired_context($expiredcontext->get('id'));
883     }
885     /**
886      * Ensure that a module context previously flagged as approved is removed with appropriate unexpiredroles kept.
887      */
888     public function test_process_course_context_with_override_unexpired_role() {
889         global $DB;
890         $this->resetAfterTest();
892         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
894         $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
896         $override = new purpose_override(0, (object) [
897                 'purposeid' => $purposes->course->get('id'),
898                 'roleid' => $role->id,
899                 'retentionperiod' => 'P5Y',
900             ]);
901         $override->save();
903         $course = $this->getDataGenerator()->create_course([
904                 'startdate' => time() - (2 * YEARSECS),
905                 'enddate' => time() - YEARSECS,
906             ]);
907         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
908         $cm = get_coursemodule_from_instance('forum', $forum->id);
909         $forumcontext = \context_module::instance($cm->id);
910         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
912         $student = $this->getDataGenerator()->create_user();
913         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
914         $generator->create_discussion((object) [
915             'course' => $forum->course,
916             'forum' => $forum->id,
917             'userid' => $student->id,
918         ]);
920         $teacher = $this->getDataGenerator()->create_user();
921         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
922         $generator->create_discussion((object) [
923             'course' => $forum->course,
924             'forum' => $forum->id,
925             'userid' => $teacher->id,
926         ]);
928         // Create an existing expired_context.
929         $expiredcontext = new expired_context(0, (object) [
930                 'contextid' => $forumcontext->id,
931                 'defaultexpired' => 1,
932                 'status' => expired_context::STATUS_APPROVED,
933             ]);
934         $expiredcontext->add_unexpiredroles([$role->id]);
935         $expiredcontext->save();
937         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
938             ->setMethods([
939                 'delete_data_for_user',
940                 'delete_data_for_users_in_context',
941                 'delete_data_for_all_users_in_context',
942             ])
943             ->getMock();
944         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
945         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
946         $mockprivacymanager
947             ->expects($this->once())
948             ->method('delete_data_for_users_in_context')
949             ->with($this->callback(function($userlist) use ($student, $teacher) {
950                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
951                 $userids = $forumlist->get_userids();
952                 $this->assertCount(1, $userids);
953                 $this->assertContains($student->id, $userids);
954                 $this->assertNotContains($teacher->id, $userids);
955                 return true;
956             }));
958         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
959             ->setMethods(['get_privacy_manager'])
960             ->getMock();
962         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
963         $manager->set_progress(new \null_progress_trace());
964         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
966         $this->assertEquals(1, $processedcourses);
967         $this->assertEquals(0, $processedusers);
969         $updatedcontext = new expired_context($expiredcontext->get('id'));
970         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
971     }
973     /**
974      * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
975      */
976     public function test_process_course_context_with_override_expired_role() {
977         global $DB;
978         $this->resetAfterTest();
980         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
982         $role = $DB->get_record('role', ['shortname' => 'student']);
984         $override = new purpose_override(0, (object) [
985                 'purposeid' => $purposes->course->get('id'),
986                 'roleid' => $role->id,
987                 'retentionperiod' => 'PT1M',
988             ]);
989         $override->save();
991         $course = $this->getDataGenerator()->create_course([
992                 'startdate' => time() - (2 * YEARSECS),
993                 'enddate' => time() - YEARSECS,
994             ]);
995         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
996         $cm = get_coursemodule_from_instance('forum', $forum->id);
997         $forumcontext = \context_module::instance($cm->id);
998         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
1000         $student = $this->getDataGenerator()->create_user();
1001         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1002         $generator->create_discussion((object) [
1003             'course' => $forum->course,
1004             'forum' => $forum->id,
1005             'userid' => $student->id,
1006         ]);
1008         $teacher = $this->getDataGenerator()->create_user();
1009         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1010         $generator->create_discussion((object) [
1011             'course' => $forum->course,
1012             'forum' => $forum->id,
1013             'userid' => $teacher->id,
1014         ]);
1016         // Create an existing expired_context.
1017         $expiredcontext = new expired_context(0, (object) [
1018                 'contextid' => $forumcontext->id,
1019                 'defaultexpired' => 0,
1020                 'status' => expired_context::STATUS_APPROVED,
1021             ]);
1022         $expiredcontext->add_expiredroles([$role->id]);
1023         $expiredcontext->save();
1025         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1026             ->setMethods([
1027                 'delete_data_for_user',
1028                 'delete_data_for_users_in_context',
1029                 'delete_data_for_all_users_in_context',
1030             ])
1031             ->getMock();
1032         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1033         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1034         $mockprivacymanager
1035             ->expects($this->once())
1036             ->method('delete_data_for_users_in_context')
1037             ->with($this->callback(function($userlist) use ($student, $teacher) {
1038                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
1039                 $userids = $forumlist->get_userids();
1040                 $this->assertCount(1, $userids);
1041                 $this->assertContains($student->id, $userids);
1042                 $this->assertNotContains($teacher->id, $userids);
1043                 return true;
1044             }));
1046         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1047             ->setMethods(['get_privacy_manager'])
1048             ->getMock();
1050         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1051         $manager->set_progress(new \null_progress_trace());
1052         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1054         $this->assertEquals(1, $processedcourses);
1055         $this->assertEquals(0, $processedusers);
1057         $updatedcontext = new expired_context($expiredcontext->get('id'));
1058         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1059     }
1061     /**
1062      * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
1063      */
1064     public function test_process_course_context_with_user_in_both_lists() {
1065         global $DB;
1066         $this->resetAfterTest();
1068         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
1070         $role = $DB->get_record('role', ['shortname' => 'student']);
1072         $override = new purpose_override(0, (object) [
1073                 'purposeid' => $purposes->course->get('id'),
1074                 'roleid' => $role->id,
1075                 'retentionperiod' => 'PT1M',
1076             ]);
1077         $override->save();
1079         $course = $this->getDataGenerator()->create_course([
1080                 'startdate' => time() - (2 * YEARSECS),
1081                 'enddate' => time() - YEARSECS,
1082             ]);
1083         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1084         $cm = get_coursemodule_from_instance('forum', $forum->id);
1085         $forumcontext = \context_module::instance($cm->id);
1086         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
1088         $teacher = $this->getDataGenerator()->create_user();
1089         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1090         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
1091         $generator->create_discussion((object) [
1092             'course' => $forum->course,
1093             'forum' => $forum->id,
1094             'userid' => $teacher->id,
1095         ]);
1097         $student = $this->getDataGenerator()->create_user();
1098         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1099         $generator->create_discussion((object) [
1100             'course' => $forum->course,
1101             'forum' => $forum->id,
1102             'userid' => $student->id,
1103         ]);
1105         // Create an existing expired_context.
1106         $expiredcontext = new expired_context(0, (object) [
1107                 'contextid' => $forumcontext->id,
1108                 'defaultexpired' => 0,
1109                 'status' => expired_context::STATUS_APPROVED,
1110             ]);
1111         $expiredcontext->add_expiredroles([$role->id]);
1112         $expiredcontext->save();
1114         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1115             ->setMethods([
1116                 'delete_data_for_user',
1117                 'delete_data_for_users_in_context',
1118                 'delete_data_for_all_users_in_context',
1119             ])
1120             ->getMock();
1121         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1122         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1123         $mockprivacymanager
1124             ->expects($this->once())
1125             ->method('delete_data_for_users_in_context')
1126             ->with($this->callback(function($userlist) use ($student, $teacher) {
1127                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
1128                 $userids = $forumlist->get_userids();
1129                 $this->assertCount(1, $userids);
1130                 $this->assertContains($student->id, $userids);
1131                 $this->assertNotContains($teacher->id, $userids);
1132                 return true;
1133             }));
1135         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1136             ->setMethods(['get_privacy_manager'])
1137             ->getMock();
1139         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1140         $manager->set_progress(new \null_progress_trace());
1141         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1143         $this->assertEquals(1, $processedcourses);
1144         $this->assertEquals(0, $processedusers);
1146         $updatedcontext = new expired_context($expiredcontext->get('id'));
1147         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1148     }
1150     /**
1151      * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
1152      */
1153     public function test_process_course_context_with_user_in_both_lists_expired() {
1154         global $DB;
1155         $this->resetAfterTest();
1157         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
1159         $studentrole = $DB->get_record('role', ['shortname' => 'student']);
1160         $override = new purpose_override(0, (object) [
1161                 'purposeid' => $purposes->course->get('id'),
1162                 'roleid' => $studentrole->id,
1163                 'retentionperiod' => 'PT1M',
1164             ]);
1165         $override->save();
1167         $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
1168         $override = new purpose_override(0, (object) [
1169                 'purposeid' => $purposes->course->get('id'),
1170                 'roleid' => $teacherrole->id,
1171                 'retentionperiod' => 'PT1M',
1172             ]);
1173         $override->save();
1175         $course = $this->getDataGenerator()->create_course([
1176                 'startdate' => time() - (2 * YEARSECS),
1177                 'enddate' => time() - YEARSECS,
1178             ]);
1179         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1180         $cm = get_coursemodule_from_instance('forum', $forum->id);
1181         $forumcontext = \context_module::instance($cm->id);
1182         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
1184         $teacher = $this->getDataGenerator()->create_user();
1185         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1186         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
1187         $generator->create_discussion((object) [
1188             'course' => $forum->course,
1189             'forum' => $forum->id,
1190             'userid' => $teacher->id,
1191         ]);
1193         $student = $this->getDataGenerator()->create_user();
1194         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1195         $generator->create_discussion((object) [
1196             'course' => $forum->course,
1197             'forum' => $forum->id,
1198             'userid' => $student->id,
1199         ]);
1201         // Create an existing expired_context.
1202         $expiredcontext = new expired_context(0, (object) [
1203                 'contextid' => $forumcontext->id,
1204                 'defaultexpired' => 0,
1205                 'status' => expired_context::STATUS_APPROVED,
1206             ]);
1207         $expiredcontext->add_expiredroles([$studentrole->id, $teacherrole->id]);
1208         $expiredcontext->save();
1210         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1211             ->setMethods([
1212                 'delete_data_for_user',
1213                 'delete_data_for_users_in_context',
1214                 'delete_data_for_all_users_in_context',
1215             ])
1216             ->getMock();
1217         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1218         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1219         $mockprivacymanager
1220             ->expects($this->once())
1221             ->method('delete_data_for_users_in_context')
1222             ->with($this->callback(function($userlist) use ($student, $teacher) {
1223                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
1224                 $userids = $forumlist->get_userids();
1225                 $this->assertCount(2, $userids);
1226                 $this->assertContains($student->id, $userids);
1227                 $this->assertContains($teacher->id, $userids);
1228                 return true;
1229             }));
1231         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1232             ->setMethods(['get_privacy_manager'])
1233             ->getMock();
1235         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1236         $manager->set_progress(new \null_progress_trace());
1237         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1239         $this->assertEquals(1, $processedcourses);
1240         $this->assertEquals(0, $processedusers);
1242         $updatedcontext = new expired_context($expiredcontext->get('id'));
1243         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1244     }
1246     /**
1247      * Ensure that a site not setup will not process anything.
1248      */
1249     public function test_process_not_setup() {
1250         $this->resetAfterTest();
1252         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1253         $usercontext = \context_user::instance($user->id);
1255         // Create an existing expired_context.
1256         $expiredcontext = new expired_context(0, (object) [
1257                 'contextid' => $usercontext->id,
1258                 'status' => expired_context::STATUS_EXPIRED,
1259             ]);
1260         $expiredcontext->save();
1262         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1263             ->setMethods([
1264                 'delete_data_for_user',
1265                 'delete_data_for_all_users_in_context',
1266             ])
1267             ->getMock();
1268         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1269         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1271         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1272             ->setMethods(['get_privacy_manager'])
1273             ->getMock();
1274         $manager->set_progress(new \null_progress_trace());
1276         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1277         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1279         $this->assertEquals(0, $processedcourses);
1280         $this->assertEquals(0, $processedusers);
1281     }
1283     /**
1284      * Ensure that a user with no lastaccess is not flagged for deletion.
1285      */
1286     public function test_process_none_approved() {
1287         $this->resetAfterTest();
1289         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1291         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1292         $usercontext = \context_user::instance($user->id);
1294         // Create an existing expired_context.
1295         $expiredcontext = new expired_context(0, (object) [
1296                 'contextid' => $usercontext->id,
1297                 'status' => expired_context::STATUS_EXPIRED,
1298             ]);
1299         $expiredcontext->save();
1301         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1302             ->setMethods([
1303                 'delete_data_for_user',
1304                 'delete_data_for_all_users_in_context',
1305             ])
1306             ->getMock();
1307         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1308         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1310         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1311             ->setMethods(['get_privacy_manager'])
1312             ->getMock();
1313         $manager->set_progress(new \null_progress_trace());
1315         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1316         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1318         $this->assertEquals(0, $processedcourses);
1319         $this->assertEquals(0, $processedusers);
1320     }
1322     /**
1323      * Ensure that a user with no lastaccess is not flagged for deletion.
1324      */
1325     public function test_process_no_context() {
1326         $this->resetAfterTest();
1328         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1330         // Create an existing expired_context.
1331         $expiredcontext = new expired_context(0, (object) [
1332                 'contextid' => -1,
1333                 'status' => expired_context::STATUS_APPROVED,
1334             ]);
1335         $expiredcontext->save();
1337         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1338             ->setMethods([
1339                 'delete_data_for_user',
1340                 'delete_data_for_all_users_in_context',
1341             ])
1342             ->getMock();
1343         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1344         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1346         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1347             ->setMethods(['get_privacy_manager'])
1348             ->getMock();
1349         $manager->set_progress(new \null_progress_trace());
1351         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1352         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1354         $this->assertEquals(0, $processedcourses);
1355         $this->assertEquals(0, $processedusers);
1357         $this->expectException('dml_missing_record_exception');
1358         new expired_context($expiredcontext->get('id'));
1359     }
1361     /**
1362      * Ensure that a user context previously flagged as approved is removed.
1363      */
1364     public function test_process_user_context() {
1365         $this->resetAfterTest();
1367         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1369         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1370         $usercontext = \context_user::instance($user->id);
1372         $this->setUser($user);
1373         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1374         $blockcontext = \context_block::instance($block->instance->id);
1375         $this->setUser();
1377         // Create an existing expired_context.
1378         $expiredcontext = new expired_context(0, (object) [
1379                 'contextid' => $usercontext->id,
1380                 'status' => expired_context::STATUS_APPROVED,
1381             ]);
1382         $expiredcontext->save();
1384         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1385             ->setMethods([
1386                 'delete_data_for_user',
1387                 'delete_data_for_all_users_in_context',
1388             ])
1389             ->getMock();
1390         $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
1391         $mockprivacymanager->expects($this->exactly(2))
1392             ->method('delete_data_for_all_users_in_context')
1393             ->withConsecutive(
1394                 [$blockcontext],
1395                 [$usercontext]
1396             );
1398         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1399             ->setMethods(['get_privacy_manager'])
1400             ->getMock();
1401         $manager->set_progress(new \null_progress_trace());
1403         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1404         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1406         $this->assertEquals(0, $processedcourses);
1407         $this->assertEquals(1, $processedusers);
1409         $updatedcontext = new expired_context($expiredcontext->get('id'));
1410         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1412         // Flag all expired contexts again.
1413         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
1415         $this->assertEquals(0, $flaggedcourses);
1416         $this->assertEquals(0, $flaggedusers);
1418         // Ensure that the deleted context record is still present.
1419         $updatedcontext = new expired_context($expiredcontext->get('id'));
1420         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1421     }
1423     /**
1424      * Ensure that a course context previously flagged as approved is removed.
1425      */
1426     public function test_process_course_context() {
1427         $this->resetAfterTest();
1429         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1431         $course = $this->getDataGenerator()->create_course([
1432                 'startdate' => time() - (2 * YEARSECS),
1433                 'enddate' => time() - YEARSECS,
1434             ]);
1435         $coursecontext = \context_course::instance($course->id);
1437         // Create an existing expired_context.
1438         $expiredcontext = new expired_context(0, (object) [
1439                 'contextid' => $coursecontext->id,
1440                 'status' => expired_context::STATUS_APPROVED,
1441             ]);
1442         $expiredcontext->save();
1444         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1445             ->setMethods([
1446                 'delete_data_for_user',
1447                 'delete_data_for_all_users_in_context',
1448             ])
1449             ->getMock();
1450         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1451         $mockprivacymanager->expects($this->once())->method('delete_data_for_all_users_in_context');
1453         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1454             ->setMethods(['get_privacy_manager'])
1455             ->getMock();
1456         $manager->set_progress(new \null_progress_trace());
1458         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1459         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1461         $this->assertEquals(1, $processedcourses);
1462         $this->assertEquals(0, $processedusers);
1464         $updatedcontext = new expired_context($expiredcontext->get('id'));
1465         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1466     }
1468     /**
1469      * Ensure that a user context previously flagged as approved is not removed if the user then logs in.
1470      */
1471     public function test_process_user_context_logged_in_after_approval() {
1472         $this->resetAfterTest();
1474         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1476         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1477         $usercontext = \context_user::instance($user->id);
1479         $this->setUser($user);
1480         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1481         $context = \context_block::instance($block->instance->id);
1482         $this->setUser();
1484         // Create an existing expired_context.
1485         $expiredcontext = new expired_context(0, (object) [
1486                 'contextid' => $usercontext->id,
1487                 'status' => expired_context::STATUS_APPROVED,
1488             ]);
1489         $expiredcontext->save();
1491         // Now bump the user's last login time.
1492         $this->setUser($user);
1493         user_accesstime_log();
1494         $this->setUser();
1496         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1497             ->setMethods([
1498                 'delete_data_for_user',
1499                 'delete_data_for_all_users_in_context',
1500             ])
1501             ->getMock();
1502         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1503         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1505         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1506             ->setMethods(['get_privacy_manager'])
1507             ->getMock();
1508         $manager->set_progress(new \null_progress_trace());
1510         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1511         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1513         $this->assertEquals(0, $processedcourses);
1514         $this->assertEquals(0, $processedusers);
1516         $this->expectException('dml_missing_record_exception');
1517         new expired_context($expiredcontext->get('id'));
1518     }
1520     /**
1521      * Ensure that a user context previously flagged as approved is not removed if the purpose has changed.
1522      */
1523     public function test_process_user_context_changed_after_approved() {
1524         $this->resetAfterTest();
1526         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1528         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1529         $usercontext = \context_user::instance($user->id);
1531         $this->setUser($user);
1532         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1533         $context = \context_block::instance($block->instance->id);
1534         $this->setUser();
1536         // Create an existing expired_context.
1537         $expiredcontext = new expired_context(0, (object) [
1538                 'contextid' => $usercontext->id,
1539                 'status' => expired_context::STATUS_APPROVED,
1540             ]);
1541         $expiredcontext->save();
1543         // Now make the user a site admin.
1544         $admins = explode(',', get_config('moodle', 'siteadmins'));
1545         $admins[] = $user->id;
1546         set_config('siteadmins', implode(',', $admins));
1548         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1549             ->setMethods([
1550                 'delete_data_for_user',
1551                 'delete_data_for_all_users_in_context',
1552             ])
1553             ->getMock();
1554         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1555         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1557         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1558             ->setMethods(['get_privacy_manager'])
1559             ->getMock();
1560         $manager->set_progress(new \null_progress_trace());
1562         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1563         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1565         $this->assertEquals(0, $processedcourses);
1566         $this->assertEquals(0, $processedusers);
1568         $this->expectException('dml_missing_record_exception');
1569         new expired_context($expiredcontext->get('id'));
1570     }
1572     /**
1573      * Ensure that a user with a historically expired expired block record child is cleaned up.
1574      */
1575     public function test_process_user_historic_block_unapproved() {
1576         $this->resetAfterTest();
1578         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1580         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
1581         $usercontext = \context_user::instance($user->id);
1583         $this->setUser($user);
1584         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1585         $blockcontext = \context_block::instance($block->instance->id);
1586         $this->setUser();
1588         // Create an expired_context for the user.
1589         $expiredusercontext = new expired_context(0, (object) [
1590                 'contextid' => $usercontext->id,
1591                 'status' => expired_context::STATUS_APPROVED,
1592             ]);
1593         $expiredusercontext->save();
1595         // Create an existing expired_context which has not been approved for the block.
1596         $expiredblockcontext = new expired_context(0, (object) [
1597                 'contextid' => $blockcontext->id,
1598                 'status' => expired_context::STATUS_EXPIRED,
1599             ]);
1600         $expiredblockcontext->save();
1602         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1603             ->setMethods([
1604                 'delete_data_for_user',
1605                 'delete_data_for_all_users_in_context',
1606             ])
1607             ->getMock();
1608         $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
1609         $mockprivacymanager->expects($this->exactly(2))
1610             ->method('delete_data_for_all_users_in_context')
1611             ->withConsecutive(
1612                 [$blockcontext],
1613                 [$usercontext]
1614             );
1616         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1617             ->setMethods(['get_privacy_manager'])
1618             ->getMock();
1619         $manager->set_progress(new \null_progress_trace());
1621         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1622         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1624         $this->assertEquals(0, $processedcourses);
1625         $this->assertEquals(1, $processedusers);
1627         $updatedcontext = new expired_context($expiredusercontext->get('id'));
1628         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1629     }
1631     /**
1632      * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
1633      */
1634     public function test_process_user_historic_unexpired_child() {
1635         $this->resetAfterTest();
1637         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1638         $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
1640         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
1641         $usercontext = \context_user::instance($user->id);
1643         $this->setUser($user);
1644         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1645         $blockcontext = \context_block::instance($block->instance->id);
1646         $this->setUser();
1648         // Create an expired_context for the user.
1649         $expiredusercontext = new expired_context(0, (object) [
1650                 'contextid' => $usercontext->id,
1651                 'status' => expired_context::STATUS_APPROVED,
1652             ]);
1653         $expiredusercontext->save();
1655         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1656             ->setMethods([
1657                 'delete_data_for_user',
1658                 'delete_data_for_all_users_in_context',
1659             ])
1660             ->getMock();
1661         $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
1662         $mockprivacymanager->expects($this->exactly(2))
1663             ->method('delete_data_for_all_users_in_context')
1664             ->withConsecutive(
1665                 [$blockcontext],
1666                 [$usercontext]
1667             );
1669         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1670             ->setMethods(['get_privacy_manager'])
1671             ->getMock();
1672         $manager->set_progress(new \null_progress_trace());
1674         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1675         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1677         $this->assertEquals(0, $processedcourses);
1678         $this->assertEquals(1, $processedusers);
1680         $updatedcontext = new expired_context($expiredusercontext->get('id'));
1681         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1682     }
1684     /**
1685      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1686      * updated.
1687      */
1688     public function test_process_course_context_updated() {
1689         $this->resetAfterTest();
1691         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1693         $course = $this->getDataGenerator()->create_course([
1694                 'startdate' => time() - (2 * YEARSECS),
1695                 'enddate' => time() - YEARSECS,
1696             ]);
1697         $coursecontext = \context_course::instance($course->id);
1698         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1700         // Create an existing expired_context.
1701         $expiredcontext = new expired_context(0, (object) [
1702                 'contextid' => $coursecontext->id,
1703                 'status' => expired_context::STATUS_APPROVED,
1704             ]);
1705         $expiredcontext->save();
1707         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1708             ->setMethods([
1709                 'delete_data_for_user',
1710                 'delete_data_for_all_users_in_context',
1711             ])
1712             ->getMock();
1713         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1714         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1716         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1717             ->setMethods(['get_privacy_manager'])
1718             ->getMock();
1719         $manager->set_progress(new \null_progress_trace());
1720         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1722         // Changing the retention period to a longer period will remove the expired_context record.
1723         $purposes->activity->set('retentionperiod', 'P5Y');
1724         $purposes->activity->save();
1726         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1728         $this->assertEquals(0, $processedcourses);
1729         $this->assertEquals(0, $processedusers);
1731         $this->expectException('dml_missing_record_exception');
1732         $updatedcontext = new expired_context($expiredcontext->get('id'));
1733     }
1735     /**
1736      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1737      * updated.
1738      */
1739     public function test_process_course_context_outstanding_children() {
1740         $this->resetAfterTest();
1742         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1744         $course = $this->getDataGenerator()->create_course([
1745                 'startdate' => time() - (2 * YEARSECS),
1746                 'enddate' => time() - YEARSECS,
1747             ]);
1748         $coursecontext = \context_course::instance($course->id);
1749         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1751         // Create an existing expired_context.
1752         $expiredcontext = new expired_context(0, (object) [
1753                 'contextid' => $coursecontext->id,
1754                 'status' => expired_context::STATUS_APPROVED,
1755             ]);
1756         $expiredcontext->save();
1758         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1759             ->setMethods([
1760                 'delete_data_for_user',
1761                 'delete_data_for_all_users_in_context',
1762             ])
1763             ->getMock();
1764         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1765         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1767         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1768             ->setMethods(['get_privacy_manager'])
1769             ->getMock();
1770         $manager->set_progress(new \null_progress_trace());
1772         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1773         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1775         $this->assertEquals(0, $processedcourses);
1776         $this->assertEquals(0, $processedusers);
1778         $updatedcontext = new expired_context($expiredcontext->get('id'));
1780         // No change - we just can't process it until the children have finished.
1781         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1782     }
1784     /**
1785      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1786      * updated.
1787      */
1788     public function test_process_course_context_pending_children() {
1789         $this->resetAfterTest();
1791         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1793         $course = $this->getDataGenerator()->create_course([
1794                 'startdate' => time() - (2 * YEARSECS),
1795                 'enddate' => time() - YEARSECS,
1796             ]);
1797         $coursecontext = \context_course::instance($course->id);
1798         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1799         $cm = get_coursemodule_from_instance('forum', $forum->id);
1800         $forumcontext = \context_module::instance($cm->id);
1802         // Create an existing expired_context for the course.
1803         $expiredcoursecontext = new expired_context(0, (object) [
1804                 'contextid' => $coursecontext->id,
1805                 'status' => expired_context::STATUS_APPROVED,
1806             ]);
1807         $expiredcoursecontext->save();
1809         // And for the forum.
1810         $expiredforumcontext = new expired_context(0, (object) [
1811                 'contextid' => $forumcontext->id,
1812                 'status' => expired_context::STATUS_EXPIRED,
1813             ]);
1814         $expiredforumcontext->save();
1816         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1817             ->setMethods([
1818                 'delete_data_for_user',
1819                 'delete_data_for_all_users_in_context',
1820             ])
1821             ->getMock();
1822         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1823         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1825         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1826             ->setMethods(['get_privacy_manager'])
1827             ->getMock();
1828         $manager->set_progress(new \null_progress_trace());
1830         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1831         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1833         $this->assertEquals(0, $processedcourses);
1834         $this->assertEquals(0, $processedusers);
1836         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1838         // No change - we just can't process it until the children have finished.
1839         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1840     }
1842     /**
1843      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1844      * updated.
1845      */
1846     public function test_process_course_context_approved_children() {
1847         $this->resetAfterTest();
1849         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1851         $course = $this->getDataGenerator()->create_course([
1852                 'startdate' => time() - (2 * YEARSECS),
1853                 'enddate' => time() - YEARSECS,
1854             ]);
1855         $coursecontext = \context_course::instance($course->id);
1856         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1857         $cm = get_coursemodule_from_instance('forum', $forum->id);
1858         $forumcontext = \context_module::instance($cm->id);
1860         // Create an existing expired_context for the course.
1861         $expiredcoursecontext = new expired_context(0, (object) [
1862                 'contextid' => $coursecontext->id,
1863                 'status' => expired_context::STATUS_APPROVED,
1864             ]);
1865         $expiredcoursecontext->save();
1867         // And for the forum.
1868         $expiredforumcontext = new expired_context(0, (object) [
1869                 'contextid' => $forumcontext->id,
1870                 'status' => expired_context::STATUS_APPROVED,
1871             ]);
1872         $expiredforumcontext->save();
1874         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1875             ->setMethods([
1876                 'delete_data_for_user',
1877                 'delete_data_for_all_users_in_context',
1878             ])
1879             ->getMock();
1880         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1881         $mockprivacymanager->expects($this->exactly(2))
1882             ->method('delete_data_for_all_users_in_context')
1883             ->withConsecutive(
1884                 [$forumcontext],
1885                 [$coursecontext]
1886             );
1888         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1889             ->setMethods(['get_privacy_manager'])
1890             ->getMock();
1891         $manager->set_progress(new \null_progress_trace());
1893         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1895         // Initially only the forum will be processed.
1896         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1898         $this->assertEquals(1, $processedcourses);
1899         $this->assertEquals(0, $processedusers);
1901         $updatedcontext = new expired_context($expiredforumcontext->get('id'));
1902         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1904         // The course won't have been processed yet.
1905         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1906         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1908         // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts.
1909         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1911         $this->assertEquals(1, $processedcourses);
1912         $this->assertEquals(0, $processedusers);
1913         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1914         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1915     }
1917     /**
1918      * Test that the can_process_deletion function returns expected results.
1919      *
1920      * @dataProvider    can_process_deletion_provider
1921      * @param       int     $status
1922      * @param       bool    $expected
1923      */
1924     public function test_can_process_deletion($status, $expected) {
1925         $purpose = new expired_context(0, (object) [
1926             'status' => $status,
1928             'contextid' => \context_system::instance()->id,
1929         ]);
1931         $this->assertEquals($expected, $purpose->can_process_deletion());
1932     }
1934     /**
1935      * Data provider for the can_process_deletion tests.
1936      *
1937      * @return  array
1938      */
1939     public function can_process_deletion_provider() : array {
1940         return [
1941             'Pending' => [
1942                 expired_context::STATUS_EXPIRED,
1943                 false,
1944             ],
1945             'Approved' => [
1946                 expired_context::STATUS_APPROVED,
1947                 true,
1948             ],
1949             'Complete' => [
1950                 expired_context::STATUS_CLEANED,
1951                 false,
1952             ],
1953         ];
1954     }
1956     /**
1957      * Test that the is_complete function returns expected results.
1958      *
1959      * @dataProvider        is_complete_provider
1960      * @param       int     $status
1961      * @param       bool    $expected
1962      */
1963     public function test_is_complete($status, $expected) {
1964         $purpose = new expired_context(0, (object) [
1965             'status' => $status,
1966             'contextid' => \context_system::instance()->id,
1967         ]);
1969         $this->assertEquals($expected, $purpose->is_complete());
1970     }
1972     /**
1973      * Data provider for the is_complete tests.
1974      *
1975      * @return  array
1976      */
1977     public function is_complete_provider() : array {
1978         return [
1979             'Pending' => [
1980                 expired_context::STATUS_EXPIRED,
1981                 false,
1982             ],
1983             'Approved' => [
1984                 expired_context::STATUS_APPROVED,
1985                 false,
1986             ],
1987             'Complete' => [
1988                 expired_context::STATUS_CLEANED,
1989                 true,
1990             ],
1991         ];
1992     }
1994     /**
1995      * Test that the is_fully_expired function returns expected results.
1996      *
1997      * @dataProvider        is_fully_expired_provider
1998      * @param       array   $record
1999      * @param       bool    $expected
2000      */
2001     public function test_is_fully_expired($record, $expected) {
2002         $purpose = new expired_context(0, (object) $record);
2004         $this->assertEquals($expected, $purpose->is_fully_expired());
2005     }
2007     /**
2008      * Data provider for the is_fully_expired tests.
2009      *
2010      * @return  array
2011      */
2012     public function is_fully_expired_provider() : array {
2013         return [
2014             'Fully expired' => [
2015                 [
2016                     'status' => expired_context::STATUS_APPROVED,
2017                     'defaultexpired' => 1,
2018                 ],
2019                 true,
2020             ],
2021             'Unexpired roles present' => [
2022                 [
2023                     'status' => expired_context::STATUS_APPROVED,
2024                     'defaultexpired' => 1,
2025                     'unexpiredroles' => json_encode([1]),
2026                 ],
2027                 false,
2028             ],
2029             'Only some expired roles present' => [
2030                 [
2031                     'status' => expired_context::STATUS_APPROVED,
2032                     'defaultexpired' => 0,
2033                     'expiredroles' => json_encode([1]),
2034                 ],
2035                 false,
2036             ],
2037         ];
2038     }
2040     /**
2041      * Ensure that any orphaned records are removed once the context has been removed.
2042      */
2043     public function test_orphaned_records_are_cleared() {
2044         $this->resetAfterTest();
2046         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
2048         $course = $this->getDataGenerator()->create_course([
2049                 'startdate' => time() - (2 * YEARSECS),
2050                 'enddate' => time() - YEARSECS,
2051             ]);
2052         $context = \context_course::instance($course->id);
2054         // Flag all expired contexts.
2055         $manager = new \tool_dataprivacy\expired_contexts_manager();
2056         $manager->set_progress(new \null_progress_trace());
2057         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
2059         $this->assertEquals(1, $flaggedcourses);
2060         $this->assertEquals(0, $flaggedusers);
2062         // Ensure that the record currently exists.
2063         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2064         $this->assertNotFalse($expiredcontext);
2066         // Approve it.
2067         $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save();
2069         // Process deletions.
2070         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
2072         $this->assertEquals(1, $processedcourses);
2073         $this->assertEquals(0, $processedusers);
2075         // Ensure that the record still exists.
2076         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2077         $this->assertNotFalse($expiredcontext);
2079         // Remove the actual course.
2080         delete_course($course->id, false);
2082         // The record will still exist until we flag it again.
2083         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2084         $this->assertNotFalse($expiredcontext);
2086         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
2087         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2088         $this->assertFalse($expiredcontext);
2089     }
2091     /**
2092      * Ensure that the progres tracer works as expected out of the box.
2093      */
2094     public function test_progress_tracer_default() {
2095         $manager = new \tool_dataprivacy\expired_contexts_manager();
2097         $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
2098         $rcm = $rc->getMethod('get_progress');
2100         $rcm->setAccessible(true);
2101         $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager));
2102     }
2104     /**
2105      * Ensure that the progres tracer works as expected when given a specific traer.
2106      */
2107     public function test_progress_tracer_set() {
2108         $manager = new \tool_dataprivacy\expired_contexts_manager();
2109         $mytrace = new \null_progress_trace();
2110         $manager->set_progress($mytrace);
2112         $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
2113         $rcm = $rc->getMethod('get_progress');
2115         $rcm->setAccessible(true);
2116         $this->assertSame($mytrace, $rcm->invoke($manager));
2117     }
2119     /**
2120      * Creates an HTML block on a user.
2121      *
2122      * @param   string  $title
2123      * @param   string  $body
2124      * @param   string  $format
2125      * @return  \block_instance
2126      */
2127     protected function create_user_block($title, $body, $format) {
2128         global $USER;
2130         $configdata = (object) [
2131             'title' => $title,
2132             'text' => [
2133                 'itemid' => 19,
2134                 'text' => $body,
2135                 'format' => $format,
2136             ],
2137         ];
2139         $this->create_block($this->construct_user_page($USER));
2140         $block = $this->get_last_block_on_page($this->construct_user_page($USER));
2141         $block = block_instance('html', $block->instance);
2142         $block->instance_config_save((object) $configdata);
2144         return $block;
2145     }
2147     /**
2148      * Creates an HTML block on a page.
2149      *
2150      * @param \page $page Page
2151      */
2152     protected function create_block($page) {
2153         $page->blocks->add_block_at_end_of_default_region('html');
2154     }
2156     /**
2157      * Constructs a Page object for the User Dashboard.
2158      *
2159      * @param   \stdClass       $user User to create Dashboard for.
2160      * @return  \moodle_page
2161      */
2162     protected function construct_user_page(\stdClass $user) {
2163         $page = new \moodle_page();
2164         $page->set_context(\context_user::instance($user->id));
2165         $page->set_pagelayout('mydashboard');
2166         $page->set_pagetype('my-index');
2167         $page->blocks->load_blocks();
2168         return $page;
2169     }
2171     /**
2172      * Get the last block on the page.
2173      *
2174      * @param \page $page Page
2175      * @return \block_html Block instance object
2176      */
2177     protected function get_last_block_on_page($page) {
2178         $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
2179         $block = end($blocks);
2181         return $block;
2182     }
2184     /**
2185      * Test the is_context_expired functions when supplied with the system context.
2186      */
2187     public function test_is_context_expired_system() {
2188         $this->resetAfterTest();
2189         $this->setup_basics('PT1H', 'PT1H', 'P1D');
2190         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2192         $this->assertFalse(expired_contexts_manager::is_context_expired(\context_system::instance()));
2193         $this->assertFalse(
2194                 expired_contexts_manager::is_context_expired_or_unprotected_for_user(\context_system::instance(), $user));
2195     }
2197     /**
2198      * Test the is_context_expired functions when supplied with a block in the user context.
2199      *
2200      * Children of a user context always follow the user expiry rather than any context level defaults (e.g. at the
2201      * block level.
2202      */
2203     public function test_is_context_expired_user_block() {
2204         $this->resetAfterTest();
2206         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
2207         $purposes->block = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
2209         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2210         $this->setUser($user);
2211         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
2212         $blockcontext = \context_block::instance($block->instance->id);
2213         $this->setUser();
2215         // Protected flags have no bearing on expiry of user subcontexts.
2216         $this->assertTrue(expired_contexts_manager::is_context_expired($blockcontext));
2218         $purposes->block->set('protected', 1)->save();
2219         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
2221         $purposes->block->set('protected', 0)->save();
2222         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
2223     }
2225     /**
2226      * Test the is_context_expired functions when supplied with an expired course.
2227      */
2228     public function test_is_context_expired_course_expired() {
2229         $this->resetAfterTest();
2231         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
2233         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2234         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
2235         $coursecontext = \context_course::instance($course->id);
2237         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2239         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2241         $purposes->course->set('protected', 1)->save();
2242         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2244         $purposes->course->set('protected', 0)->save();
2245         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2246     }
2248     /**
2249      * Test the is_context_expired functions when supplied with an unexpired course.
2250      */
2251     public function test_is_context_expired_course_unexpired() {
2252         $this->resetAfterTest();
2254         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
2256         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2257         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2258         $coursecontext = \context_course::instance($course->id);
2260         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2262         $this->assertTrue(expired_contexts_manager::is_context_expired($coursecontext));
2264         $purposes->course->set('protected', 1)->save();
2265         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2267         $purposes->course->set('protected', 0)->save();
2268         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2269     }
2271     /**
2272      * Test the is_context_expired functions when supplied with an unexpired course and a child context in the course which is protected.
2273      *
2274      * When a child context has a specific purpose set, then that purpose should be respected with respect to the
2275      * course.
2276      *
2277      * If the course is still within the expiry period for the child context, then that child's protected flag should be
2278      * respected, even when the course may have expired.
2279      */
2280     public function test_is_child_context_expired_course_unexpired_with_child() {
2281         $this->resetAfterTest();
2283         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D', 'P1D');
2284         $purposes->course->set('protected', 0)->save();
2285         $purposes->activity->set('protected', 1)->save();
2287         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2288         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() + WEEKSECS]);
2289         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
2291         $coursecontext = \context_course::instance($course->id);
2292         $cm = get_coursemodule_from_instance('forum', $forum->id);
2293         $forumcontext = \context_module::instance($cm->id);
2295         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2297         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2298         $this->assertFalse(expired_contexts_manager::is_context_expired($forumcontext));
2300         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2301         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user));
2303         $purposes->activity->set('protected', 0)->save();
2304         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user));
2305     }
2307     /**
2308      * Test the is_context_expired functions when supplied with an expired course which has role overrides.
2309      */
2310     public function test_is_context_expired_course_expired_override() {
2311         global $DB;
2313         $this->resetAfterTest();
2315         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
2317         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2318         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2319         $coursecontext = \context_course::instance($course->id);
2320         $systemcontext = \context_system::instance();
2322         $role = $DB->get_record('role', ['shortname' => 'manager']);
2323         $override = new purpose_override(0, (object) [
2324                 'purposeid' => $purposes->course->get('id'),
2325                 'roleid' => $role->id,
2326                 'retentionperiod' => 'P5Y',
2327             ]);
2328         $override->save();
2329         role_assign($role->id, $user->id, $systemcontext->id);
2331         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2333         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2335         $purposes->course->set('protected', 1)->save();
2336         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2338         $purposes->course->set('protected', 0)->save();
2339         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2340     }
2342     /**
2343      * Test the is_context_expired functions when supplied with an expired course which has role overrides.
2344      */
2345     public function test_is_context_expired_course_expired_override_parent() {
2346         global $DB;
2348         $this->resetAfterTest();
2350         $purposes = $this->setup_basics('PT1H', 'PT1H');
2352         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2353         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2354         $coursecontext = \context_course::instance($course->id);
2355         $systemcontext = \context_system::instance();
2357         $role = $DB->get_record('role', ['shortname' => 'manager']);
2358         $override = new purpose_override(0, (object) [
2359                 'purposeid' => $purposes->system->get('id'),
2360                 'roleid' => $role->id,
2361                 'retentionperiod' => 'P5Y',
2362             ]);
2363         $override->save();
2364         role_assign($role->id, $user->id, $systemcontext->id);
2366         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2368         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2370         // The user override applies to this user. THIs means that the default expiry has no effect.
2371         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2373         $purposes->system->set('protected', 1)->save();
2374         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2376         $purposes->system->set('protected', 0)->save();
2377         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2379         $override->set('protected', 1)->save();
2380         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2382         $purposes->system->set('protected', 1)->save();
2383         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2385         $purposes->system->set('protected', 0)->save();
2386         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2388     }
2390     /**
2391      * Test the is_context_expired functions when supplied with an expired course which has role overrides but the user
2392      * does not hold the role.
2393      */
2394     public function test_is_context_expired_course_expired_override_parent_no_role() {
2395         global $DB;
2397         $this->resetAfterTest();
2399         $purposes = $this->setup_basics('PT1H', 'PT1H');
2401         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2402         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2403         $coursecontext = \context_course::instance($course->id);
2404         $systemcontext = \context_system::instance();
2406         $role = $DB->get_record('role', ['shortname' => 'manager']);
2407         $override = new purpose_override(0, (object) [
2408                 'purposeid' => $purposes->system->get('id'),
2409                 'roleid' => $role->id,
2410                 'retentionperiod' => 'P5Y',
2411             ]);
2412         $override->save();
2414         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2416         // This context is not _fully _ expired.
2417         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2418     }
2420     /**
2421      * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
2422      */
2423     public function test_is_context_expired_course_expired_override_inverse() {
2424         global $DB;
2426         $this->resetAfterTest();
2428         $purposes = $this->setup_basics('P1Y', 'P1Y');
2430         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2431         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2432         $coursecontext = \context_course::instance($course->id);
2433         $systemcontext = \context_system::instance();
2435         $role = $DB->get_record('role', ['shortname' => 'student']);
2436         $override = new purpose_override(0, (object) [
2437                 'purposeid' => $purposes->system->get('id'),
2438                 'roleid' => $role->id,
2439                 'retentionperiod' => 'PT1S',
2440             ]);
2441         $override->save();
2443         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2445         // This context is not _fully _ expired.
2446         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2447     }
2449     /**
2450      * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
2451      */
2452     public function test_is_context_expired_course_expired_override_inverse_parent() {
2453         global $DB;
2455         $this->resetAfterTest();
2457         $purposes = $this->setup_basics('P1Y', 'P1Y');
2459         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2460         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2461         $coursecontext = \context_course::instance($course->id);
2462         $systemcontext = \context_system::instance();
2464         $role = $DB->get_record('role', ['shortname' => 'manager']);
2465         $override = new purpose_override(0, (object) [
2466                 'purposeid' => $purposes->system->get('id'),
2467                 'roleid' => $role->id,
2468                 'retentionperiod' => 'PT1S',
2469             ]);
2470         $override->save();
2472         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2473         role_assign($role->id, $user->id, $systemcontext->id);
2475         $studentrole = $DB->get_record('role', ['shortname' => 'student']);
2476         role_unassign($studentrole->id, $user->id, $coursecontext->id);
2478         // This context is not _fully _ expired.
2479         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2480     }
2482     /**
2483      * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
2484      */
2485     public function test_is_context_expired_course_expired_override_inverse_parent_not_assigned() {
2486         global $DB;
2488         $this->resetAfterTest();
2490         $purposes = $this->setup_basics('P1Y', 'P1Y');
2492         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2493         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2494         $coursecontext = \context_course::instance($course->id);
2495         $systemcontext = \context_system::instance();
2497         $role = $DB->get_record('role', ['shortname' => 'manager']);
2498         $override = new purpose_override(0, (object) [
2499                 'purposeid' => $purposes->system->get('id'),
2500                 'roleid' => $role->id,
2501                 'retentionperiod' => 'PT1S',
2502             ]);
2503         $override->save();
2505         // Enrol the user in the course without any role.
2506         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2507         $studentrole = $DB->get_record('role', ['shortname' => 'student']);
2508         role_unassign($studentrole->id, $user->id, $coursecontext->id);
2510         // This context is not _fully _ expired.
2511         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2512     }
2514     /**
2515      * Ensure that context expired checks for a specific user taken into account roles.
2516      */
2517     public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected() {
2518         global $DB;
2520         $this->resetAfterTest();
2522         $purposes = $this->setup_basics('PT1S', 'PT1S', 'PT1S');
2524         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
2525         $coursecontext = \context_course::instance($course->id);
2526         $systemcontext = \context_system::instance();
2528         $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
2529         $override = new purpose_override(0, (object) [
2530                 'purposeid' => $purposes->course->get('id'),
2531                 'roleid' => $roles['manager'],
2532                 'retentionperiod' => 'P1W',
2533                 'protected' => 1,
2534             ]);
2535         $override->save();
2537         $s = $this->getDataGenerator()->create_user();
2538         $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');
2540         $t = $this->getDataGenerator()->create_user();
2541         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2543         $sm = $this->getDataGenerator()->create_user();
2544         $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
2545         role_assign($roles['manager'], $sm->id, $coursecontext->id);
2547         $m = $this->getDataGenerator()->create_user();
2548         role_assign($roles['manager'], $m->id, $coursecontext->id);
2550         $tm = $this->getDataGenerator()->create_user();
2551         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2552         role_assign($roles['manager'], $tm->id, $coursecontext->id);
2554         // The context should only be expired for users who are not a manager.
2555         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2556         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2557         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2558         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2559         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2561         $override->set('protected', 0)->save();
2562         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2563         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2564         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2565         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2566         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2567     }
2569     /**
2570      * Ensure that context expired checks for a specific user taken into account roles when retention is inversed.
2571      */
2572     public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected_inverse() {
2573         global $DB;
2575         $this->resetAfterTest();
2577         $purposes = $this->setup_basics('P5Y', 'P5Y', 'P5Y');
2579         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
2580         $coursecontext = \context_course::instance($course->id);
2581         $systemcontext = \context_system::instance();
2583         $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
2584         $override = new purpose_override(0, (object) [
2585                 'purposeid' => $purposes->course->get('id'),
2586                 'roleid' => $roles['student'],
2587                 'retentionperiod' => 'PT1S',
2588             ]);
2589         $override->save();
2591         $s = $this->getDataGenerator()->create_user();
2592         $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');
2594         $t = $this->getDataGenerator()->create_user();
2595         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2597         $sm = $this->getDataGenerator()->create_user();
2598         $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
2599         role_assign($roles['manager'], $sm->id, $coursecontext->id);
2601         $m = $this->getDataGenerator()->create_user();
2602         role_assign($roles['manager'], $m->id, $coursecontext->id);
2604         $tm = $this->getDataGenerator()->create_user();
2605         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2606         role_assign($roles['manager'], $tm->id, $coursecontext->id);
2608         // The context should only be expired for users who are only a student.
2609         $purposes->course->set('protected', 1)->save();
2610         $override->set('protected', 1)->save();
2611         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2612         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2613         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2614         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2615         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2617         $purposes->course->set('protected', 0)->save();
2618         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2619         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2620         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2621         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2622         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2623     }