6058ac5502b5b9b8ed250892bb86a0bc8d1ebb4d
[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(CONTEXT_COURSE)
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         $purposes->course->set('retentionperiod', 'P5Y');
1723         $purposes->course->save();
1725         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1727         $this->assertEquals(0, $processedcourses);
1728         $this->assertEquals(0, $processedusers);
1730         $updatedcontext = new expired_context($expiredcontext->get('id'));
1732         // No change - we just can't process it until the children have finished.
1733         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1734     }
1736     /**
1737      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1738      * updated.
1739      */
1740     public function test_process_course_context_outstanding_children() {
1741         $this->resetAfterTest();
1743         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1745         $course = $this->getDataGenerator()->create_course([
1746                 'startdate' => time() - (2 * YEARSECS),
1747                 'enddate' => time() - YEARSECS,
1748             ]);
1749         $coursecontext = \context_course::instance($course->id);
1750         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1752         // Create an existing expired_context.
1753         $expiredcontext = new expired_context(0, (object) [
1754                 'contextid' => $coursecontext->id,
1755                 'status' => expired_context::STATUS_APPROVED,
1756             ]);
1757         $expiredcontext->save();
1759         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1760             ->setMethods([
1761                 'delete_data_for_user',
1762                 'delete_data_for_all_users_in_context',
1763             ])
1764             ->getMock();
1765         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1766         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1768         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1769             ->setMethods(['get_privacy_manager'])
1770             ->getMock();
1771         $manager->set_progress(new \null_progress_trace());
1773         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1774         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1776         $this->assertEquals(0, $processedcourses);
1777         $this->assertEquals(0, $processedusers);
1779         $updatedcontext = new expired_context($expiredcontext->get('id'));
1781         // No change - we just can't process it until the children have finished.
1782         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1783     }
1785     /**
1786      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1787      * updated.
1788      */
1789     public function test_process_course_context_pending_children() {
1790         $this->resetAfterTest();
1792         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1794         $course = $this->getDataGenerator()->create_course([
1795                 'startdate' => time() - (2 * YEARSECS),
1796                 'enddate' => time() - YEARSECS,
1797             ]);
1798         $coursecontext = \context_course::instance($course->id);
1799         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1800         $cm = get_coursemodule_from_instance('forum', $forum->id);
1801         $forumcontext = \context_module::instance($cm->id);
1803         // Create an existing expired_context for the course.
1804         $expiredcoursecontext = new expired_context(0, (object) [
1805                 'contextid' => $coursecontext->id,
1806                 'status' => expired_context::STATUS_APPROVED,
1807             ]);
1808         $expiredcoursecontext->save();
1810         // And for the forum.
1811         $expiredforumcontext = new expired_context(0, (object) [
1812                 'contextid' => $forumcontext->id,
1813                 'status' => expired_context::STATUS_EXPIRED,
1814             ]);
1815         $expiredforumcontext->save();
1817         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1818             ->setMethods([
1819                 'delete_data_for_user',
1820                 'delete_data_for_all_users_in_context',
1821             ])
1822             ->getMock();
1823         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1824         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1826         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1827             ->setMethods(['get_privacy_manager'])
1828             ->getMock();
1829         $manager->set_progress(new \null_progress_trace());
1831         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1832         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1834         $this->assertEquals(0, $processedcourses);
1835         $this->assertEquals(0, $processedusers);
1837         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1839         // No change - we just can't process it until the children have finished.
1840         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1841     }
1843     /**
1844      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1845      * updated.
1846      */
1847     public function test_process_course_context_approved_children() {
1848         $this->resetAfterTest();
1850         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1852         $course = $this->getDataGenerator()->create_course([
1853                 'startdate' => time() - (2 * YEARSECS),
1854                 'enddate' => time() - YEARSECS,
1855             ]);
1856         $coursecontext = \context_course::instance($course->id);
1857         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1858         $cm = get_coursemodule_from_instance('forum', $forum->id);
1859         $forumcontext = \context_module::instance($cm->id);
1861         // Create an existing expired_context for the course.
1862         $expiredcoursecontext = new expired_context(0, (object) [
1863                 'contextid' => $coursecontext->id,
1864                 'status' => expired_context::STATUS_APPROVED,
1865             ]);
1866         $expiredcoursecontext->save();
1868         // And for the forum.
1869         $expiredforumcontext = new expired_context(0, (object) [
1870                 'contextid' => $forumcontext->id,
1871                 'status' => expired_context::STATUS_APPROVED,
1872             ]);
1873         $expiredforumcontext->save();
1875         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1876             ->setMethods([
1877                 'delete_data_for_user',
1878                 'delete_data_for_all_users_in_context',
1879             ])
1880             ->getMock();
1881         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1882         $mockprivacymanager->expects($this->exactly(2))
1883             ->method('delete_data_for_all_users_in_context')
1884             ->withConsecutive(
1885                 [$forumcontext],
1886                 [$coursecontext]
1887             );
1889         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1890             ->setMethods(['get_privacy_manager'])
1891             ->getMock();
1892         $manager->set_progress(new \null_progress_trace());
1894         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1896         // Initially only the forum will be processed.
1897         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1899         $this->assertEquals(1, $processedcourses);
1900         $this->assertEquals(0, $processedusers);
1902         $updatedcontext = new expired_context($expiredforumcontext->get('id'));
1903         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1905         // The course won't have been processed yet.
1906         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1907         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1909         // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts.
1910         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1912         $this->assertEquals(1, $processedcourses);
1913         $this->assertEquals(0, $processedusers);
1914         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1915         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1916     }
1918     /**
1919      * Test that the can_process_deletion function returns expected results.
1920      *
1921      * @dataProvider    can_process_deletion_provider
1922      * @param       int     $status
1923      * @param       bool    $expected
1924      */
1925     public function test_can_process_deletion($status, $expected) {
1926         $purpose = new expired_context(0, (object) [
1927             'status' => $status,
1929             'contextid' => \context_system::instance()->id,
1930         ]);
1932         $this->assertEquals($expected, $purpose->can_process_deletion());
1933     }
1935     /**
1936      * Data provider for the can_process_deletion tests.
1937      *
1938      * @return  array
1939      */
1940     public function can_process_deletion_provider() : array {
1941         return [
1942             'Pending' => [
1943                 expired_context::STATUS_EXPIRED,
1944                 false,
1945             ],
1946             'Approved' => [
1947                 expired_context::STATUS_APPROVED,
1948                 true,
1949             ],
1950             'Complete' => [
1951                 expired_context::STATUS_CLEANED,
1952                 false,
1953             ],
1954         ];
1955     }
1957     /**
1958      * Test that the is_complete function returns expected results.
1959      *
1960      * @dataProvider        is_complete_provider
1961      * @param       int     $status
1962      * @param       bool    $expected
1963      */
1964     public function test_is_complete($status, $expected) {
1965         $purpose = new expired_context(0, (object) [
1966             'status' => $status,
1967             'contextid' => \context_system::instance()->id,
1968         ]);
1970         $this->assertEquals($expected, $purpose->is_complete());
1971     }
1973     /**
1974      * Data provider for the is_complete tests.
1975      *
1976      * @return  array
1977      */
1978     public function is_complete_provider() : array {
1979         return [
1980             'Pending' => [
1981                 expired_context::STATUS_EXPIRED,
1982                 false,
1983             ],
1984             'Approved' => [
1985                 expired_context::STATUS_APPROVED,
1986                 false,
1987             ],
1988             'Complete' => [
1989                 expired_context::STATUS_CLEANED,
1990                 true,
1991             ],
1992         ];
1993     }
1995     /**
1996      * Test that the is_fully_expired function returns expected results.
1997      *
1998      * @dataProvider        is_fully_expired_provider
1999      * @param       array   $record
2000      * @param       bool    $expected
2001      */
2002     public function test_is_fully_expired($record, $expected) {
2003         $purpose = new expired_context(0, (object) $record);
2005         $this->assertEquals($expected, $purpose->is_fully_expired());
2006     }
2008     /**
2009      * Data provider for the is_fully_expired tests.
2010      *
2011      * @return  array
2012      */
2013     public function is_fully_expired_provider() : array {
2014         return [
2015             'Fully expired' => [
2016                 [
2017                     'status' => expired_context::STATUS_APPROVED,
2018                     'defaultexpired' => 1,
2019                 ],
2020                 true,
2021             ],
2022             'Unexpired roles present' => [
2023                 [
2024                     'status' => expired_context::STATUS_APPROVED,
2025                     'defaultexpired' => 1,
2026                     'unexpiredroles' => json_encode([1]),
2027                 ],
2028                 false,
2029             ],
2030             'Only some expired roles present' => [
2031                 [
2032                     'status' => expired_context::STATUS_APPROVED,
2033                     'defaultexpired' => 0,
2034                     'expiredroles' => json_encode([1]),
2035                 ],
2036                 false,
2037             ],
2038         ];
2039     }
2041     /**
2042      * Ensure that any orphaned records are removed once the context has been removed.
2043      */
2044     public function test_orphaned_records_are_cleared() {
2045         $this->resetAfterTest();
2047         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
2049         $course = $this->getDataGenerator()->create_course([
2050                 'startdate' => time() - (2 * YEARSECS),
2051                 'enddate' => time() - YEARSECS,
2052             ]);
2053         $context = \context_course::instance($course->id);
2055         // Flag all expired contexts.
2056         $manager = new \tool_dataprivacy\expired_contexts_manager();
2057         $manager->set_progress(new \null_progress_trace());
2058         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
2060         $this->assertEquals(1, $flaggedcourses);
2061         $this->assertEquals(0, $flaggedusers);
2063         // Ensure that the record currently exists.
2064         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2065         $this->assertNotFalse($expiredcontext);
2067         // Approve it.
2068         $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save();
2070         // Process deletions.
2071         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
2073         $this->assertEquals(1, $processedcourses);
2074         $this->assertEquals(0, $processedusers);
2076         // Ensure that the record still exists.
2077         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2078         $this->assertNotFalse($expiredcontext);
2080         // Remove the actual course.
2081         delete_course($course->id, false);
2083         // The record will still exist until we flag it again.
2084         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2085         $this->assertNotFalse($expiredcontext);
2087         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
2088         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2089         $this->assertFalse($expiredcontext);
2090     }
2092     /**
2093      * Ensure that the progres tracer works as expected out of the box.
2094      */
2095     public function test_progress_tracer_default() {
2096         $manager = new \tool_dataprivacy\expired_contexts_manager();
2098         $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
2099         $rcm = $rc->getMethod('get_progress');
2101         $rcm->setAccessible(true);
2102         $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager));
2103     }
2105     /**
2106      * Ensure that the progres tracer works as expected when given a specific traer.
2107      */
2108     public function test_progress_tracer_set() {
2109         $manager = new \tool_dataprivacy\expired_contexts_manager();
2110         $mytrace = new \null_progress_trace();
2111         $manager->set_progress($mytrace);
2113         $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
2114         $rcm = $rc->getMethod('get_progress');
2116         $rcm->setAccessible(true);
2117         $this->assertSame($mytrace, $rcm->invoke($manager));
2118     }
2120     /**
2121      * Creates an HTML block on a user.
2122      *
2123      * @param   string  $title
2124      * @param   string  $body
2125      * @param   string  $format
2126      * @return  \block_instance
2127      */
2128     protected function create_user_block($title, $body, $format) {
2129         global $USER;
2131         $configdata = (object) [
2132             'title' => $title,
2133             'text' => [
2134                 'itemid' => 19,
2135                 'text' => $body,
2136                 'format' => $format,
2137             ],
2138         ];
2140         $this->create_block($this->construct_user_page($USER));
2141         $block = $this->get_last_block_on_page($this->construct_user_page($USER));
2142         $block = block_instance('html', $block->instance);
2143         $block->instance_config_save((object) $configdata);
2145         return $block;
2146     }
2148     /**
2149      * Creates an HTML block on a page.
2150      *
2151      * @param \page $page Page
2152      */
2153     protected function create_block($page) {
2154         $page->blocks->add_block_at_end_of_default_region('html');
2155     }
2157     /**
2158      * Constructs a Page object for the User Dashboard.
2159      *
2160      * @param   \stdClass       $user User to create Dashboard for.
2161      * @return  \moodle_page
2162      */
2163     protected function construct_user_page(\stdClass $user) {
2164         $page = new \moodle_page();
2165         $page->set_context(\context_user::instance($user->id));
2166         $page->set_pagelayout('mydashboard');
2167         $page->set_pagetype('my-index');
2168         $page->blocks->load_blocks();
2169         return $page;
2170     }
2172     /**
2173      * Get the last block on the page.
2174      *
2175      * @param \page $page Page
2176      * @return \block_html Block instance object
2177      */
2178     protected function get_last_block_on_page($page) {
2179         $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
2180         $block = end($blocks);
2182         return $block;
2183     }
2185     /**
2186      * Test the is_context_expired functions when supplied with the system context.
2187      */
2188     public function test_is_context_expired_system() {
2189         global $DB;
2191         $this->resetAfterTest();
2192         $this->setup_basics('PT1H', 'PT1H', 'P1D');
2193         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2195         $this->assertFalse(expired_contexts_manager::is_context_expired(\context_system::instance()));
2196         $this->assertFalse(
2197                 expired_contexts_manager::is_context_expired_or_unprotected_for_user(\context_system::instance(), $user));
2198     }
2200     /**
2201      * Test the is_context_expired functions when supplied with an expired course.
2202      */
2203     public function test_is_context_expired_course_expired() {
2204         global $DB;
2206         $this->resetAfterTest();
2208         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
2210         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2211         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
2212         $coursecontext = \context_course::instance($course->id);
2214         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2216         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2218         $purposes->course->set('protected', 1)->save();
2219         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2221         $purposes->course->set('protected', 0)->save();
2222         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2223     }
2225     /**
2226      * Test the is_context_expired functions when supplied with an unexpired course.
2227      */
2228     public function test_is_context_expired_course_unexpired() {
2229         global $DB;
2231         $this->resetAfterTest();
2233         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
2235         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2236         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2237         $coursecontext = \context_course::instance($course->id);
2239         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2241         $this->assertTrue(expired_contexts_manager::is_context_expired($coursecontext));
2243         $purposes->course->set('protected', 1)->save();
2244         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2246         $purposes->course->set('protected', 0)->save();
2247         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2248     }
2250     /**
2251      * Test the is_context_expired functions when supplied with an expired course which has role overrides.
2252      */
2253     public function test_is_context_expired_course_expired_override() {
2254         global $DB;
2256         $this->resetAfterTest();
2258         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
2260         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2261         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2262         $coursecontext = \context_course::instance($course->id);
2263         $systemcontext = \context_system::instance();
2265         $role = $DB->get_record('role', ['shortname' => 'manager']);
2266         $override = new purpose_override(0, (object) [
2267                 'purposeid' => $purposes->course->get('id'),
2268                 'roleid' => $role->id,
2269                 'retentionperiod' => 'P5Y',
2270             ]);
2271         $override->save();
2272         role_assign($role->id, $user->id, $systemcontext->id);
2274         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2276         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2278         $purposes->course->set('protected', 1)->save();
2279         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2281         $purposes->course->set('protected', 0)->save();
2282         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2283     }
2285     /**
2286      * Test the is_context_expired functions when supplied with an expired course which has role overrides.
2287      */
2288     public function test_is_context_expired_course_expired_override_parent() {
2289         global $DB;
2291         $this->resetAfterTest();
2293         $purposes = $this->setup_basics('PT1H', 'PT1H');
2295         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2296         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2297         $coursecontext = \context_course::instance($course->id);
2298         $systemcontext = \context_system::instance();
2300         $role = $DB->get_record('role', ['shortname' => 'manager']);
2301         $override = new purpose_override(0, (object) [
2302                 'purposeid' => $purposes->system->get('id'),
2303                 'roleid' => $role->id,
2304                 'retentionperiod' => 'P5Y',
2305             ]);
2306         $override->save();
2307         role_assign($role->id, $user->id, $systemcontext->id);
2309         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2311         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2313         // The user override applies to this user. THIs means that the default expiry has no effect.
2314         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2316         $purposes->system->set('protected', 1)->save();
2317         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2319         $purposes->system->set('protected', 0)->save();
2320         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2322         $override->set('protected', 1)->save();
2323         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2325         $purposes->system->set('protected', 1)->save();
2326         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2328         $purposes->system->set('protected', 0)->save();
2329         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
2331     }
2333     /**
2334      * Test the is_context_expired functions when supplied with an expired course which has role overrides but the user
2335      * does not hold the role.
2336      */
2337     public function test_is_context_expired_course_expired_override_parent_no_role() {
2338         global $DB;
2340         $this->resetAfterTest();
2342         $purposes = $this->setup_basics('PT1H', 'PT1H');
2344         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2345         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2346         $coursecontext = \context_course::instance($course->id);
2347         $systemcontext = \context_system::instance();
2349         $role = $DB->get_record('role', ['shortname' => 'manager']);
2350         $override = new purpose_override(0, (object) [
2351                 'purposeid' => $purposes->system->get('id'),
2352                 'roleid' => $role->id,
2353                 'retentionperiod' => 'P5Y',
2354             ]);
2355         $override->save();
2357         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2359         // This context is not _fully _ expired.
2360         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2361     }
2363     /**
2364      * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
2365      */
2366     public function test_is_context_expired_course_expired_override_inverse() {
2367         global $DB;
2369         $this->resetAfterTest();
2371         $purposes = $this->setup_basics('P1Y', 'P1Y');
2373         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2374         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2375         $coursecontext = \context_course::instance($course->id);
2376         $systemcontext = \context_system::instance();
2378         $role = $DB->get_record('role', ['shortname' => 'student']);
2379         $override = new purpose_override(0, (object) [
2380                 'purposeid' => $purposes->system->get('id'),
2381                 'roleid' => $role->id,
2382                 'retentionperiod' => 'PT1S',
2383             ]);
2384         $override->save();
2386         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2388         // This context is not _fully _ expired.
2389         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2390     }
2392     /**
2393      * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
2394      */
2395     public function test_is_context_expired_course_expired_override_inverse_parent() {
2396         global $DB;
2398         $this->resetAfterTest();
2400         $purposes = $this->setup_basics('P1Y', 'P1Y');
2402         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2403         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2404         $coursecontext = \context_course::instance($course->id);
2405         $systemcontext = \context_system::instance();
2407         $role = $DB->get_record('role', ['shortname' => 'manager']);
2408         $override = new purpose_override(0, (object) [
2409                 'purposeid' => $purposes->system->get('id'),
2410                 'roleid' => $role->id,
2411                 'retentionperiod' => 'PT1S',
2412             ]);
2413         $override->save();
2415         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2416         role_assign($role->id, $user->id, $systemcontext->id);
2418         $studentrole = $DB->get_record('role', ['shortname' => 'student']);
2419         role_unassign($studentrole->id, $user->id, $coursecontext->id);
2421         // This context is not _fully _ expired.
2422         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2423     }
2425     /**
2426      * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
2427      */
2428     public function test_is_context_expired_course_expired_override_inverse_parent_not_assigned() {
2429         global $DB;
2431         $this->resetAfterTest();
2433         $purposes = $this->setup_basics('P1Y', 'P1Y');
2435         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
2436         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
2437         $coursecontext = \context_course::instance($course->id);
2438         $systemcontext = \context_system::instance();
2440         $role = $DB->get_record('role', ['shortname' => 'manager']);
2441         $override = new purpose_override(0, (object) [
2442                 'purposeid' => $purposes->system->get('id'),
2443                 'roleid' => $role->id,
2444                 'retentionperiod' => 'PT1S',
2445             ]);
2446         $override->save();
2448         // Enrol the user in the course without any role.
2449         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2450         $studentrole = $DB->get_record('role', ['shortname' => 'student']);
2451         role_unassign($studentrole->id, $user->id, $coursecontext->id);
2453         // This context is not _fully _ expired.
2454         $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
2455     }
2457     /**
2458      * Ensure that context expired checks for a specific user taken into account roles.
2459      */
2460     public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected() {
2461         global $DB;
2463         $this->resetAfterTest();
2465         $purposes = $this->setup_basics('PT1S', 'PT1S', 'PT1S');
2467         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
2468         $coursecontext = \context_course::instance($course->id);
2469         $systemcontext = \context_system::instance();
2471         $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
2472         $override = new purpose_override(0, (object) [
2473                 'purposeid' => $purposes->course->get('id'),
2474                 'roleid' => $roles['manager'],
2475                 'retentionperiod' => 'P1W',
2476                 'protected' => 1,
2477             ]);
2478         $override->save();
2480         $s = $this->getDataGenerator()->create_user();
2481         $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');
2483         $t = $this->getDataGenerator()->create_user();
2484         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2486         $sm = $this->getDataGenerator()->create_user();
2487         $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
2488         role_assign($roles['manager'], $sm->id, $coursecontext->id);
2490         $m = $this->getDataGenerator()->create_user();
2491         role_assign($roles['manager'], $m->id, $coursecontext->id);
2493         $tm = $this->getDataGenerator()->create_user();
2494         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2495         role_assign($roles['manager'], $tm->id, $coursecontext->id);
2497         // The context should only be expired for users who are not a manager.
2498         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2499         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2500         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2501         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2502         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2504         $override->set('protected', 0)->save();
2505         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2506         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2507         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2508         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2509         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2510     }
2512     /**
2513      * Ensure that context expired checks for a specific user taken into account roles when retention is inversed.
2514      */
2515     public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected_inverse() {
2516         global $DB;
2518         $this->resetAfterTest();
2520         $purposes = $this->setup_basics('P5Y', 'P5Y', 'P5Y');
2522         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
2523         $coursecontext = \context_course::instance($course->id);
2524         $systemcontext = \context_system::instance();
2526         $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
2527         $override = new purpose_override(0, (object) [
2528                 'purposeid' => $purposes->course->get('id'),
2529                 'roleid' => $roles['student'],
2530                 'retentionperiod' => 'PT1S',
2531             ]);
2532         $override->save();
2534         $s = $this->getDataGenerator()->create_user();
2535         $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');
2537         $t = $this->getDataGenerator()->create_user();
2538         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2540         $sm = $this->getDataGenerator()->create_user();
2541         $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
2542         role_assign($roles['manager'], $sm->id, $coursecontext->id);
2544         $m = $this->getDataGenerator()->create_user();
2545         role_assign($roles['manager'], $m->id, $coursecontext->id);
2547         $tm = $this->getDataGenerator()->create_user();
2548         $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
2549         role_assign($roles['manager'], $tm->id, $coursecontext->id);
2551         // The context should only be expired for users who are only a student.
2552         $purposes->course->set('protected', 1)->save();
2553         $override->set('protected', 1)->save();
2554         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2555         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2556         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2557         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2558         $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2560         $purposes->course->set('protected', 0)->save();
2561         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
2562         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
2563         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
2564         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
2565         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
2566     }