MDL-63496 tool_dataprivacy: Support for per-role retention
[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;
33 defined('MOODLE_INTERNAL') || die();
34 global $CFG;
36 /**
37  * Expired contexts tests.
38  *
39  * @package    tool_dataprivacy
40  * @copyright  2018 David Monllao
41  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42  */
43 class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
45     /**
46      * Setup the basics with the specified retention period.
47      *
48      * @param   string  $system Retention policy for the system.
49      * @param   string  $user Retention policy for users.
50      * @param   string  $course Retention policy for courses.
51      * @param   string  $activity Retention policy for activities.
52      */
53     protected function setup_basics(string $system, string $user, string $course, string $activity = null) : array {
54         $this->resetAfterTest();
56         $purposes = [];
57         $purposes[] = $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM);
58         $purposes[] = $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER);
59         $purposes[] = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
60         if (null !== $activity) {
61             $purposes[] = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
62         }
64         return $purposes;
65     }
67     /**
68      * Create a retention period and set it for the specified context level.
69      *
70      * @param   string  $retention
71      * @param   int     $contextlevel
72      * @return  purpose
73      */
74     protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
75         $purpose = new purpose(0, (object) [
76             'name' => 'Test purpose ' . rand(1, 1000),
77             'retentionperiod' => $retention,
78             'lawfulbases' => 'gdpr_art_6_1_a',
79         ]);
80         $purpose->create();
82         $cat = new category(0, (object) ['name' => 'Test category']);
83         $cat->create();
85         if ($contextlevel <= CONTEXT_USER) {
86             $record = (object) [
87                 'purposeid'     => $purpose->get('id'),
88                 'categoryid'    => $cat->get('id'),
89                 'contextlevel'  => $contextlevel,
90             ];
91             api::set_contextlevel($record);
92         } else {
93             list($purposevar, ) = data_registry::var_names_from_context(
94                     \context_helper::get_class_for_level(CONTEXT_COURSE)
95                 );
96             set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
97         }
99         return $purpose;
100     }
102     /**
103      * Ensure that a user with no lastaccess is not flagged for deletion.
104      */
105     public function test_flag_not_setup() {
106         $this->resetAfterTest();
108         $user = $this->getDataGenerator()->create_user();
110         $this->setUser($user);
111         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
112         $context = \context_block::instance($block->instance->id);
113         $this->setUser();
115         // Flag all expired contexts.
116         $manager = new \tool_dataprivacy\expired_contexts_manager();
117         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
119         $this->assertEquals(0, $flaggedcourses);
120         $this->assertEquals(0, $flaggedusers);
121     }
123     /**
124      * Ensure that a user with no lastaccess is not flagged for deletion.
125      */
126     public function test_flag_user_no_lastaccess() {
127         $this->resetAfterTest();
129         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
131         $user = $this->getDataGenerator()->create_user();
133         $this->setUser($user);
134         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
135         $context = \context_block::instance($block->instance->id);
136         $this->setUser();
138         // Flag all expired contexts.
139         $manager = new \tool_dataprivacy\expired_contexts_manager();
140         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
142         $this->assertEquals(0, $flaggedcourses);
143         $this->assertEquals(0, $flaggedusers);
144     }
146     /**
147      * Ensure that a user with a recent lastaccess is not flagged for deletion.
148      */
149     public function test_flag_user_recent_lastaccess() {
150         $this->resetAfterTest();
152         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
154         $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
156         $this->setUser($user);
157         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
158         $context = \context_block::instance($block->instance->id);
159         $this->setUser();
161         // Flag all expired contexts.
162         $manager = new \tool_dataprivacy\expired_contexts_manager();
163         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
165         $this->assertEquals(0, $flaggedcourses);
166         $this->assertEquals(0, $flaggedusers);
167     }
169     /**
170      * Ensure that a user with a lastaccess in the past is flagged for deletion.
171      */
172     public function test_flag_user_past_lastaccess() {
173         $this->resetAfterTest();
175         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
177         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
179         $this->setUser($user);
180         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
181         $context = \context_block::instance($block->instance->id);
182         $this->setUser();
184         // Flag all expired contexts.
185         $manager = new \tool_dataprivacy\expired_contexts_manager();
186         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
188         // Although there is a block in the user context, everything in the user context is regarded as one.
189         $this->assertEquals(0, $flaggedcourses);
190         $this->assertEquals(1, $flaggedusers);
191     }
193     /**
194      * Ensure that a user with a lastaccess in the past but active enrolments is not flagged for deletion.
195      */
196     public function test_flag_user_past_lastaccess_still_enrolled() {
197         $this->resetAfterTest();
199         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
201         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
202         $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enddate' => time() + YEARSECS]);
203         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
205         $otheruser = $this->getDataGenerator()->create_user();
206         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
208         $this->setUser($user);
209         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
210         $context = \context_block::instance($block->instance->id);
211         $this->setUser();
213         // Flag all expired contexts.
214         $manager = new \tool_dataprivacy\expired_contexts_manager();
215         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
217         $this->assertEquals(0, $flaggedcourses);
218         $this->assertEquals(0, $flaggedusers);
219     }
221     /**
222      * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion.
223      */
224     public function test_flag_user_update_existing() {
225         $this->resetAfterTest();
227         $this->setup_basics('PT1H', 'PT1H', 'P5Y');
229         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
230         $usercontext = \context_user::instance($user->id);
232         // Create an existing expired_context.
233         $expiredcontext = new expired_context(0, (object) [
234                 'contextid' => $usercontext->id,
235                 'defaultexpired' => 0,
236                 'status' => expired_context::STATUS_EXPIRED,
237             ]);
238         $expiredcontext->save();
239         $this->assertEquals(0, $expiredcontext->get('defaultexpired'));
241         // Flag all expired contexts.
242         $manager = new \tool_dataprivacy\expired_contexts_manager();
243         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
245         $this->assertEquals(0, $flaggedcourses);
246         $this->assertEquals(1, $flaggedusers);
248         // The user context will now have expired.
249         $updatedcontext = new expired_context($expiredcontext->get('id'));
250         $this->assertEquals(1, $updatedcontext->get('defaultexpired'));
251     }
253     /**
254      * Ensure that a user with a lastaccess in the past and expired enrolments.
255      */
256     public function test_flag_user_past_lastaccess_unexpired_past_enrolment() {
257         $this->resetAfterTest();
259         $this->setup_basics('PT1H', 'PT1H', 'P1Y');
261         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
262         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
263         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
265         $otheruser = $this->getDataGenerator()->create_user();
266         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
268         $this->setUser($user);
269         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
270         $context = \context_block::instance($block->instance->id);
271         $this->setUser();
273         // Flag all expired contexts.
274         $manager = new \tool_dataprivacy\expired_contexts_manager();
275         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
277         $this->assertEquals(0, $flaggedcourses);
278         $this->assertEquals(0, $flaggedusers);
279     }
281     /**
282      * Ensure that a user with a lastaccess in the past and expired enrolments.
283      */
284     public function test_flag_user_past_override_role() {
285         global $DB;
286         $this->resetAfterTest();
288         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
289         $userpurpose = $purposes[1];
291         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
292         $usercontext = \context_user::instance($user->id);
293         $systemcontext = \context_system::instance();
295         $role = $DB->get_record('role', ['shortname' => 'manager']);
297         $override = new purpose_override(0, (object) [
298                 'purposeid' => $userpurpose->get('id'),
299                 'roleid' => $role->id,
300                 'retentionperiod' => 'P5Y',
301             ]);
302         $override->save();
303         role_assign($role->id, $user->id, $systemcontext->id);
305         // Flag all expired contexts.
306         $manager = new \tool_dataprivacy\expired_contexts_manager();
307         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
309         $this->assertEquals(0, $flaggedcourses);
310         $this->assertEquals(0, $flaggedusers);
312         $expiredrecord = expired_context::get_record(['contextid' => $usercontext->id]);
313         $this->assertFalse($expiredrecord);
314     }
316     /**
317      * Ensure that a user with a lastaccess in the past and expired enrolments.
318      */
319     public function test_flag_user_past_lastaccess_expired_enrolled() {
320         $this->resetAfterTest();
322         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
324         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
325         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
326         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
328         $otheruser = $this->getDataGenerator()->create_user();
329         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
331         $this->setUser($user);
332         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
333         $context = \context_block::instance($block->instance->id);
334         $this->setUser();
336         // Flag all expired contexts.
337         $manager = new \tool_dataprivacy\expired_contexts_manager();
338         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
340         $this->assertEquals(1, $flaggedcourses);
341         $this->assertEquals(1, $flaggedusers);
342     }
344     /**
345      * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
346      * correctly.
347      */
348     public function test_flag_user_past_lastaccess_missing_enddate_required() {
349         $this->resetAfterTest();
351         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
353         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
354         $course = $this->getDataGenerator()->create_course();
355         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
357         $otheruser = $this->getDataGenerator()->create_user();
358         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
360         $this->setUser($user);
361         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
362         $context = \context_block::instance($block->instance->id);
363         $this->setUser();
365         // Ensure that course end dates are not required.
366         set_config('requireallenddatesforuserdeletion', 1, 'tool_dataprivacy');
368         // Flag all expired contexts.
369         $manager = new \tool_dataprivacy\expired_contexts_manager();
370         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
372         $this->assertEquals(0, $flaggedcourses);
373         $this->assertEquals(0, $flaggedusers);
374     }
376     /**
377      * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
378      * correctly when the end date is not required.
379      */
380     public function test_flag_user_past_lastaccess_missing_enddate_not_required() {
381         $this->resetAfterTest();
383         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
385         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
386         $course = $this->getDataGenerator()->create_course();
387         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
389         $otheruser = $this->getDataGenerator()->create_user();
390         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
392         $this->setUser($user);
393         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
394         $context = \context_block::instance($block->instance->id);
395         $this->setUser();
397         // Ensure that course end dates are required.
398         set_config('requireallenddatesforuserdeletion', 0, 'tool_dataprivacy');
400         // Flag all expired contexts.
401         $manager = new \tool_dataprivacy\expired_contexts_manager();
402         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
404         $this->assertEquals(0, $flaggedcourses);
405         $this->assertEquals(1, $flaggedusers);
406     }
408     /**
409      * Ensure that a user with a recent lastaccess is not flagged for deletion.
410      */
411     public function test_flag_user_recent_lastaccess_existing_record() {
412         $this->resetAfterTest();
414         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
416         $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
417         $usercontext = \context_user::instance($user->id);
419         // Create an existing expired_context.
420         $expiredcontext = new expired_context(0, (object) [
421                 'contextid' => $usercontext->id,
422                 'status' => expired_context::STATUS_EXPIRED,
423             ]);
424         $expiredcontext->save();
426         $this->setUser($user);
427         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
428         $context = \context_block::instance($block->instance->id);
429         $this->setUser();
431         // Flag all expired contexts.
432         $manager = new \tool_dataprivacy\expired_contexts_manager();
433         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
435         $this->assertEquals(0, $flaggedcourses);
436         $this->assertEquals(0, $flaggedusers);
438         $this->expectException('dml_missing_record_exception');
439         new expired_context($expiredcontext->get('id'));
440     }
442     /**
443      * Ensure that a user with a recent lastaccess is not flagged for deletion.
444      */
445     public function test_flag_user_retention_changed() {
446         $this->resetAfterTest();
448         list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
450         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
451         $usercontext = \context_user::instance($user->id);
453         $this->setUser($user);
454         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
455         $context = \context_block::instance($block->instance->id);
456         $this->setUser();
458         // Flag all expired contexts.
459         $manager = new \tool_dataprivacy\expired_contexts_manager();
460         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
462         $this->assertEquals(0, $flaggedcourses);
463         $this->assertEquals(1, $flaggedusers);
465         $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
466         $this->assertNotFalse($expiredcontext);
468         // Increase the retention period to 5 years.
469         $userpurpose->set('retentionperiod', 'P5Y');
470         $userpurpose->save();
472         // Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased.
473         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
474         $this->assertEquals(0, $flaggedcourses);
475         $this->assertEquals(0, $flaggedusers);
477         // The expiry record will now have been removed.
478         $this->expectException('dml_missing_record_exception');
479         new expired_context($expiredcontext->get('id'));
480     }
482     /**
483      * Ensure that a user with a historically expired expired block record child is cleaned up.
484      */
485     public function test_flag_user_historic_block_unapproved() {
486         $this->resetAfterTest();
488         list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
490         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
491         $usercontext = \context_user::instance($user->id);
493         $this->setUser($user);
494         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
495         $blockcontext = \context_block::instance($block->instance->id);
496         $this->setUser();
498         // Create an existing expired_context which has not been approved for the block.
499         $expiredcontext = new expired_context(0, (object) [
500                 'contextid' => $blockcontext->id,
501                 'status' => expired_context::STATUS_EXPIRED,
502             ]);
503         $expiredcontext->save();
505         // Flag all expired contexts.
506         $manager = new \tool_dataprivacy\expired_contexts_manager();
507         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
509         $this->assertEquals(0, $flaggedcourses);
510         $this->assertEquals(1, $flaggedusers);
512         $expiredblockcontext = expired_context::get_record(['contextid' => $blockcontext->id]);
513         $this->assertFalse($expiredblockcontext);
515         $expiredusercontext = expired_context::get_record(['contextid' => $usercontext->id]);
516         $this->assertNotFalse($expiredusercontext);
517     }
519     /**
520      * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
521      */
522     public function test_flag_user_historic_unexpired_child() {
523         $this->resetAfterTest();
525         list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
526         $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
528         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
529         $usercontext = \context_user::instance($user->id);
531         $this->setUser($user);
532         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
533         $blockcontext = \context_block::instance($block->instance->id);
534         $this->setUser();
536         // Flag all expired contexts.
537         $manager = new \tool_dataprivacy\expired_contexts_manager();
538         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
540         $this->assertEquals(0, $flaggedcourses);
541         $this->assertEquals(1, $flaggedusers);
543         $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
544         $this->assertNotFalse($expiredcontext);
545     }
547     /**
548      * Ensure that a course with no end date is not flagged.
549      */
550     public function test_flag_course_no_enddate() {
551         $this->resetAfterTest();
553         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
555         $course = $this->getDataGenerator()->create_course();
556         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
558         // Flag all expired contexts.
559         $manager = new \tool_dataprivacy\expired_contexts_manager();
560         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
562         $this->assertEquals(0, $flaggedcourses);
563         $this->assertEquals(0, $flaggedusers);
564     }
566     /**
567      * Ensure that a course with an end date in the distant past, but a child which is unexpired is not flagged.
568      */
569     public function test_flag_course_past_enddate_future_child() {
570         $this->resetAfterTest();
572         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'P5Y');
574         $course = $this->getDataGenerator()->create_course([
575                 'startdate' => time() - (2 * YEARSECS),
576                 'enddate' => time() - YEARSECS,
577             ]);
578         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
580         // Flag all expired contexts.
581         $manager = new \tool_dataprivacy\expired_contexts_manager();
582         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
584         $this->assertEquals(0, $flaggedcourses);
585         $this->assertEquals(0, $flaggedusers);
586     }
588     /**
589      * Ensure that a course with an end date in the distant past is flagged.
590      */
591     public function test_flag_course_past_enddate() {
592         $this->resetAfterTest();
594         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
596         $course = $this->getDataGenerator()->create_course([
597                 'startdate' => time() - (2 * YEARSECS),
598                 'enddate' => time() - YEARSECS,
599             ]);
600         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
602         // Flag all expired contexts.
603         $manager = new \tool_dataprivacy\expired_contexts_manager();
604         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
606         $this->assertEquals(2, $flaggedcourses);
607         $this->assertEquals(0, $flaggedusers);
608     }
610     /**
611      * Ensure that a course with an end date in the distant past is flagged.
612      */
613     public function test_flag_course_past_enddate_multiple() {
614         $this->resetAfterTest();
616         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
618         $course1 = $this->getDataGenerator()->create_course([
619                 'startdate' => time() - (2 * YEARSECS),
620                 'enddate' => time() - YEARSECS,
621             ]);
622         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
624         $course2 = $this->getDataGenerator()->create_course([
625                 'startdate' => time() - (2 * YEARSECS),
626                 'enddate' => time() - YEARSECS,
627             ]);
628         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
630         // Flag all expired contexts.
631         $manager = new \tool_dataprivacy\expired_contexts_manager();
632         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
634         $this->assertEquals(4, $flaggedcourses);
635         $this->assertEquals(0, $flaggedusers);
636     }
638     /**
639      * Ensure that a course with an end date in the future is not flagged.
640      */
641     public function test_flag_course_future_enddate() {
642         $this->resetAfterTest();
644         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
646         $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
647         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
649         // Flag all expired contexts.
650         $manager = new \tool_dataprivacy\expired_contexts_manager();
651         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
653         $this->assertEquals(0, $flaggedcourses);
654         $this->assertEquals(0, $flaggedusers);
655     }
657     /**
658      * Ensure that a course with an end date in the future is not flagged.
659      */
660     public function test_flag_course_recent_unexpired_enddate() {
661         $this->resetAfterTest();
663         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
665         $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);
667         // Flag all expired contexts.
668         $manager = new \tool_dataprivacy\expired_contexts_manager();
669         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
671         $this->assertEquals(0, $flaggedcourses);
672         $this->assertEquals(0, $flaggedusers);
673     }
675     /**
676      * Ensure that a course with an end date in the distant past is flagged, taking into account any purpose override
677      */
678     public function test_flag_course_past_enddate_with_override_unexpired_role() {
679         global $DB;
680         $this->resetAfterTest();
682         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
683         $coursepurpose = $purposes[2];
685         $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
687         $override = new purpose_override(0, (object) [
688                 'purposeid' => $coursepurpose->get('id'),
689                 'roleid' => $role->id,
690                 'retentionperiod' => 'P5Y',
691             ]);
692         $override->save();
694         $course = $this->getDataGenerator()->create_course([
695                 'startdate' => time() - (2 * DAYSECS),
696                 'enddate' => time() - DAYSECS,
697             ]);
698         $coursecontext = \context_course::instance($course->id);
700         // Flag all expired contexts.
701         $manager = new \tool_dataprivacy\expired_contexts_manager();
702         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
704         $this->assertEquals(1, $flaggedcourses);
705         $this->assertEquals(0, $flaggedusers);
707         $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
708         $this->assertEmpty($expiredrecord->get('expiredroles'));
710         $unexpiredroles = $expiredrecord->get('unexpiredroles');
711         $this->assertCount(1, $unexpiredroles);
712         $this->assertContains($role->id, $unexpiredroles);
713     }
715     /**
716      * Ensure that a course with an end date in the distant past is flagged, and any expired role is ignored.
717      */
718     public function test_flag_course_past_enddate_with_override_expired_role() {
719         global $DB;
720         $this->resetAfterTest();
722         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
723         $coursepurpose = $purposes[2];
725         $role = $DB->get_record('role', ['shortname' => 'student']);
727         // The role has a much shorter retention, but both should match.
728         $override = new purpose_override(0, (object) [
729                 'purposeid' => $coursepurpose->get('id'),
730                 'roleid' => $role->id,
731                 'retentionperiod' => 'PT1M',
732             ]);
733         $override->save();
735         $course = $this->getDataGenerator()->create_course([
736                 'startdate' => time() - (2 * DAYSECS),
737                 'enddate' => time() - DAYSECS,
738             ]);
739         $coursecontext = \context_course::instance($course->id);
741         // Flag all expired contexts.
742         $manager = new \tool_dataprivacy\expired_contexts_manager();
743         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
745         $this->assertEquals(1, $flaggedcourses);
746         $this->assertEquals(0, $flaggedusers);
748         $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
749         $this->assertEmpty($expiredrecord->get('expiredroles'));
750         $this->assertEmpty($expiredrecord->get('unexpiredroles'));
751         $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
752     }
754     /**
755      * Ensure that where a course has explicitly expired one role, but that role is explicitly not expired in a child
756      * context, does not have the parent context role expired.
757      */
758     public function test_flag_course_override_expiredwith_override_unexpired_on_child() {
759         global $DB;
760         $this->resetAfterTest();
762         $purposes = $this->setup_basics('P1Y', 'P1Y', 'P1Y');
763         $coursepurpose = $purposes[2];
765         $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
767         (new purpose_override(0, (object) [
768                 'purposeid' => $coursepurpose->get('id'),
769                 'roleid' => $role->id,
770                 'retentionperiod' => 'PT1S',
771             ]))->save();
773         $modpurpose = new purpose(0, (object) [
774             'name' => 'Module purpose',
775             'retentionperiod' => 'PT1S',
776             'lawfulbases' => 'gdpr_art_6_1_a',
777         ]);
778         $modpurpose->create();
780         (new purpose_override(0, (object) [
781                 'purposeid' => $modpurpose->get('id'),
782                 'roleid' => $role->id,
783                 'retentionperiod' => 'P5Y',
784             ]))->save();
786         $course = $this->getDataGenerator()->create_course([
787                 'startdate' => time() - (2 * DAYSECS),
788                 'enddate' => time() - DAYSECS,
789             ]);
790         $coursecontext = \context_course::instance($course->id);
792         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
793         $cm = get_coursemodule_from_instance('forum', $forum->id);
794         $forumcontext = \context_module::instance($cm->id);
796         api::set_context_instance((object) [
797                 'contextid' => $forumcontext->id,
798                 'purposeid' => $modpurpose->get('id'),
799                 'categoryid' => 0,
800             ]);
802         // Flag all expired contexts.
803         $manager = new \tool_dataprivacy\expired_contexts_manager();
804         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
806         $this->assertEquals(1, $flaggedcourses);
807         $this->assertEquals(0, $flaggedusers);
809         // The course will not be expired as the default expiry has not passed, and the explicit role override has been
810         // removed due to the child non-expiry.
811         $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
812         $this->assertFalse($expiredrecord);
814         // The forum will have an expiry for all _but_ the overridden role.
815         $expiredrecord = expired_context::get_record(['contextid' => $forumcontext->id]);
816         $this->assertEmpty($expiredrecord->get('expiredroles'));
818         // The teacher is not expired.
819         $unexpiredroles = $expiredrecord->get('unexpiredroles');
820         $this->assertCount(1, $unexpiredroles);
821         $this->assertContains($role->id, $unexpiredroles);
822         $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
823     }
825     /**
826      * Ensure that a user context previously flagged as approved is not removed if the user has any unexpired roles.
827      */
828     public function test_process_user_context_with_override_unexpired_role() {
829         global $DB;
830         $this->resetAfterTest();
832         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
833         $userpurpose = $purposes[1];
835         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
836         $usercontext = \context_user::instance($user->id);
837         $systemcontext = \context_system::instance();
839         $role = $DB->get_record('role', ['shortname' => 'manager']);
841         $override = new purpose_override(0, (object) [
842                 'purposeid' => $userpurpose->get('id'),
843                 'roleid' => $role->id,
844                 'retentionperiod' => 'P5Y',
845             ]);
846         $override->save();
847         role_assign($role->id, $user->id, $systemcontext->id);
849         // Create an existing expired_context.
850         $expiredcontext = new expired_context(0, (object) [
851                 'contextid' => $usercontext->id,
852                 'defaultexpired' => 1,
853                 'status' => expired_context::STATUS_APPROVED,
854             ]);
855         $expiredcontext->add_unexpiredroles([$role->id]);
856         $expiredcontext->save();
858         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
859             ->setMethods([
860                 'delete_data_for_user',
861                 'delete_data_for_users_in_context',
862                 'delete_data_for_all_users_in_context',
863             ])
864             ->getMock();
865         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
866         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
867         $mockprivacymanager->expects($this->never())->method('delete_data_for_users_in_context');
869         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
870             ->setMethods(['get_privacy_manager'])
871             ->getMock();
873         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
874         $manager->set_progress(new \null_progress_trace());
875         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
877         $this->assertEquals(0, $processedcourses);
878         $this->assertEquals(0, $processedusers);
880         $this->expectException('dml_missing_record_exception');
881         $updatedcontext = new expired_context($expiredcontext->get('id'));
882     }
884     /**
885      * Ensure that a module context previously flagged as approved is removed with appropriate unexpiredroles kept.
886      */
887     public function test_process_course_context_with_override_unexpired_role() {
888         global $DB;
889         $this->resetAfterTest();
891         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
892         $coursepurpose = $purposes[2];
894         $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
896         $override = new purpose_override(0, (object) [
897                 'purposeid' => $coursepurpose->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');
981         $coursepurpose = $purposes[2];
983         $role = $DB->get_record('role', ['shortname' => 'student']);
985         $override = new purpose_override(0, (object) [
986                 'purposeid' => $coursepurpose->get('id'),
987                 'roleid' => $role->id,
988                 'retentionperiod' => 'PT1M',
989             ]);
990         $override->save();
992         $course = $this->getDataGenerator()->create_course([
993                 'startdate' => time() - (2 * YEARSECS),
994                 'enddate' => time() - YEARSECS,
995             ]);
996         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
997         $cm = get_coursemodule_from_instance('forum', $forum->id);
998         $forumcontext = \context_module::instance($cm->id);
999         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
1001         $student = $this->getDataGenerator()->create_user();
1002         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1003         $generator->create_discussion((object) [
1004             'course' => $forum->course,
1005             'forum' => $forum->id,
1006             'userid' => $student->id,
1007         ]);
1009         $teacher = $this->getDataGenerator()->create_user();
1010         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1011         $generator->create_discussion((object) [
1012             'course' => $forum->course,
1013             'forum' => $forum->id,
1014             'userid' => $teacher->id,
1015         ]);
1017         // Create an existing expired_context.
1018         $expiredcontext = new expired_context(0, (object) [
1019                 'contextid' => $forumcontext->id,
1020                 'defaultexpired' => 0,
1021                 'status' => expired_context::STATUS_APPROVED,
1022             ]);
1023         $expiredcontext->add_expiredroles([$role->id]);
1024         $expiredcontext->save();
1026         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1027             ->setMethods([
1028                 'delete_data_for_user',
1029                 'delete_data_for_users_in_context',
1030                 'delete_data_for_all_users_in_context',
1031             ])
1032             ->getMock();
1033         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1034         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1035         $mockprivacymanager
1036             ->expects($this->once())
1037             ->method('delete_data_for_users_in_context')
1038             ->with($this->callback(function($userlist) use ($student, $teacher) {
1039                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
1040                 $userids = $forumlist->get_userids();
1041                 $this->assertCount(1, $userids);
1042                 $this->assertContains($student->id, $userids);
1043                 $this->assertNotContains($teacher->id, $userids);
1044                 return true;
1045             }));
1047         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1048             ->setMethods(['get_privacy_manager'])
1049             ->getMock();
1051         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1052         $manager->set_progress(new \null_progress_trace());
1053         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1055         $this->assertEquals(1, $processedcourses);
1056         $this->assertEquals(0, $processedusers);
1058         $updatedcontext = new expired_context($expiredcontext->get('id'));
1059         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1060     }
1062     /**
1063      * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
1064      */
1065     public function test_process_course_context_with_user_in_both_lists() {
1066         global $DB;
1067         $this->resetAfterTest();
1069         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
1070         $coursepurpose = $purposes[2];
1072         $role = $DB->get_record('role', ['shortname' => 'student']);
1074         $override = new purpose_override(0, (object) [
1075                 'purposeid' => $coursepurpose->get('id'),
1076                 'roleid' => $role->id,
1077                 'retentionperiod' => 'PT1M',
1078             ]);
1079         $override->save();
1081         $course = $this->getDataGenerator()->create_course([
1082                 'startdate' => time() - (2 * YEARSECS),
1083                 'enddate' => time() - YEARSECS,
1084             ]);
1085         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1086         $cm = get_coursemodule_from_instance('forum', $forum->id);
1087         $forumcontext = \context_module::instance($cm->id);
1088         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
1090         $teacher = $this->getDataGenerator()->create_user();
1091         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1092         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
1093         $generator->create_discussion((object) [
1094             'course' => $forum->course,
1095             'forum' => $forum->id,
1096             'userid' => $teacher->id,
1097         ]);
1099         $student = $this->getDataGenerator()->create_user();
1100         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1101         $generator->create_discussion((object) [
1102             'course' => $forum->course,
1103             'forum' => $forum->id,
1104             'userid' => $student->id,
1105         ]);
1107         // Create an existing expired_context.
1108         $expiredcontext = new expired_context(0, (object) [
1109                 'contextid' => $forumcontext->id,
1110                 'defaultexpired' => 0,
1111                 'status' => expired_context::STATUS_APPROVED,
1112             ]);
1113         $expiredcontext->add_expiredroles([$role->id]);
1114         $expiredcontext->save();
1116         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1117             ->setMethods([
1118                 'delete_data_for_user',
1119                 'delete_data_for_users_in_context',
1120                 'delete_data_for_all_users_in_context',
1121             ])
1122             ->getMock();
1123         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1124         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1125         $mockprivacymanager
1126             ->expects($this->once())
1127             ->method('delete_data_for_users_in_context')
1128             ->with($this->callback(function($userlist) use ($student, $teacher) {
1129                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
1130                 $userids = $forumlist->get_userids();
1131                 $this->assertCount(1, $userids);
1132                 $this->assertContains($student->id, $userids);
1133                 $this->assertNotContains($teacher->id, $userids);
1134                 return true;
1135             }));
1137         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1138             ->setMethods(['get_privacy_manager'])
1139             ->getMock();
1141         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1142         $manager->set_progress(new \null_progress_trace());
1143         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1145         $this->assertEquals(1, $processedcourses);
1146         $this->assertEquals(0, $processedusers);
1148         $updatedcontext = new expired_context($expiredcontext->get('id'));
1149         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1150     }
1152     /**
1153      * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
1154      */
1155     public function test_process_course_context_with_user_in_both_lists_expired() {
1156         global $DB;
1157         $this->resetAfterTest();
1159         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
1160         $coursepurpose = $purposes[2];
1162         $studentrole = $DB->get_record('role', ['shortname' => 'student']);
1163         $override = new purpose_override(0, (object) [
1164                 'purposeid' => $coursepurpose->get('id'),
1165                 'roleid' => $studentrole->id,
1166                 'retentionperiod' => 'PT1M',
1167             ]);
1168         $override->save();
1170         $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
1171         $override = new purpose_override(0, (object) [
1172                 'purposeid' => $coursepurpose->get('id'),
1173                 'roleid' => $teacherrole->id,
1174                 'retentionperiod' => 'PT1M',
1175             ]);
1176         $override->save();
1178         $course = $this->getDataGenerator()->create_course([
1179                 'startdate' => time() - (2 * YEARSECS),
1180                 'enddate' => time() - YEARSECS,
1181             ]);
1182         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1183         $cm = get_coursemodule_from_instance('forum', $forum->id);
1184         $forumcontext = \context_module::instance($cm->id);
1185         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
1187         $teacher = $this->getDataGenerator()->create_user();
1188         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1189         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
1190         $generator->create_discussion((object) [
1191             'course' => $forum->course,
1192             'forum' => $forum->id,
1193             'userid' => $teacher->id,
1194         ]);
1196         $student = $this->getDataGenerator()->create_user();
1197         $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1198         $generator->create_discussion((object) [
1199             'course' => $forum->course,
1200             'forum' => $forum->id,
1201             'userid' => $student->id,
1202         ]);
1204         // Create an existing expired_context.
1205         $expiredcontext = new expired_context(0, (object) [
1206                 'contextid' => $forumcontext->id,
1207                 'defaultexpired' => 0,
1208                 'status' => expired_context::STATUS_APPROVED,
1209             ]);
1210         $expiredcontext->add_expiredroles([$studentrole->id, $teacherrole->id]);
1211         $expiredcontext->save();
1213         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1214             ->setMethods([
1215                 'delete_data_for_user',
1216                 'delete_data_for_users_in_context',
1217                 'delete_data_for_all_users_in_context',
1218             ])
1219             ->getMock();
1220         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1221         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1222         $mockprivacymanager
1223             ->expects($this->once())
1224             ->method('delete_data_for_users_in_context')
1225             ->with($this->callback(function($userlist) use ($student, $teacher) {
1226                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
1227                 $userids = $forumlist->get_userids();
1228                 $this->assertCount(2, $userids);
1229                 $this->assertContains($student->id, $userids);
1230                 $this->assertContains($teacher->id, $userids);
1231                 return true;
1232             }));
1234         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1235             ->setMethods(['get_privacy_manager'])
1236             ->getMock();
1238         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1239         $manager->set_progress(new \null_progress_trace());
1240         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1242         $this->assertEquals(1, $processedcourses);
1243         $this->assertEquals(0, $processedusers);
1245         $updatedcontext = new expired_context($expiredcontext->get('id'));
1246         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1247     }
1249     /**
1250      * Ensure that a site not setup will not process anything.
1251      */
1252     public function test_process_not_setup() {
1253         $this->resetAfterTest();
1255         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1256         $usercontext = \context_user::instance($user->id);
1258         // Create an existing expired_context.
1259         $expiredcontext = new expired_context(0, (object) [
1260                 'contextid' => $usercontext->id,
1261                 'status' => expired_context::STATUS_EXPIRED,
1262             ]);
1263         $expiredcontext->save();
1265         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1266             ->setMethods([
1267                 'delete_data_for_user',
1268                 'delete_data_for_all_users_in_context',
1269             ])
1270             ->getMock();
1271         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1272         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1274         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1275             ->setMethods(['get_privacy_manager'])
1276             ->getMock();
1277         $manager->set_progress(new \null_progress_trace());
1279         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1280         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1282         $this->assertEquals(0, $processedcourses);
1283         $this->assertEquals(0, $processedusers);
1284     }
1286     /**
1287      * Ensure that a user with no lastaccess is not flagged for deletion.
1288      */
1289     public function test_process_none_approved() {
1290         $this->resetAfterTest();
1292         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1294         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1295         $usercontext = \context_user::instance($user->id);
1297         // Create an existing expired_context.
1298         $expiredcontext = new expired_context(0, (object) [
1299                 'contextid' => $usercontext->id,
1300                 'status' => expired_context::STATUS_EXPIRED,
1301             ]);
1302         $expiredcontext->save();
1304         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1305             ->setMethods([
1306                 'delete_data_for_user',
1307                 'delete_data_for_all_users_in_context',
1308             ])
1309             ->getMock();
1310         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1311         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1313         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1314             ->setMethods(['get_privacy_manager'])
1315             ->getMock();
1316         $manager->set_progress(new \null_progress_trace());
1318         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1319         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1321         $this->assertEquals(0, $processedcourses);
1322         $this->assertEquals(0, $processedusers);
1323     }
1325     /**
1326      * Ensure that a user with no lastaccess is not flagged for deletion.
1327      */
1328     public function test_process_no_context() {
1329         $this->resetAfterTest();
1331         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1333         // Create an existing expired_context.
1334         $expiredcontext = new expired_context(0, (object) [
1335                 'contextid' => -1,
1336                 'status' => expired_context::STATUS_APPROVED,
1337             ]);
1338         $expiredcontext->save();
1340         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1341             ->setMethods([
1342                 'delete_data_for_user',
1343                 'delete_data_for_all_users_in_context',
1344             ])
1345             ->getMock();
1346         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1347         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1349         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1350             ->setMethods(['get_privacy_manager'])
1351             ->getMock();
1352         $manager->set_progress(new \null_progress_trace());
1354         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1355         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1357         $this->assertEquals(0, $processedcourses);
1358         $this->assertEquals(0, $processedusers);
1360         $this->expectException('dml_missing_record_exception');
1361         new expired_context($expiredcontext->get('id'));
1362     }
1364     /**
1365      * Ensure that a user context previously flagged as approved is removed.
1366      */
1367     public function test_process_user_context() {
1368         $this->resetAfterTest();
1370         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1372         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1373         $usercontext = \context_user::instance($user->id);
1375         $this->setUser($user);
1376         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1377         $blockcontext = \context_block::instance($block->instance->id);
1378         $this->setUser();
1380         // Create an existing expired_context.
1381         $expiredcontext = new expired_context(0, (object) [
1382                 'contextid' => $usercontext->id,
1383                 'status' => expired_context::STATUS_APPROVED,
1384             ]);
1385         $expiredcontext->save();
1387         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1388             ->setMethods([
1389                 'delete_data_for_user',
1390                 'delete_data_for_all_users_in_context',
1391             ])
1392             ->getMock();
1393         $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
1394         $mockprivacymanager->expects($this->exactly(2))
1395             ->method('delete_data_for_all_users_in_context')
1396             ->withConsecutive(
1397                 [$blockcontext],
1398                 [$usercontext]
1399             );
1401         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1402             ->setMethods(['get_privacy_manager'])
1403             ->getMock();
1404         $manager->set_progress(new \null_progress_trace());
1406         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1407         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1409         $this->assertEquals(0, $processedcourses);
1410         $this->assertEquals(1, $processedusers);
1412         $updatedcontext = new expired_context($expiredcontext->get('id'));
1413         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1415         // Flag all expired contexts again.
1416         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
1418         $this->assertEquals(0, $flaggedcourses);
1419         $this->assertEquals(0, $flaggedusers);
1421         // Ensure that the deleted context record is still present.
1422         $updatedcontext = new expired_context($expiredcontext->get('id'));
1423         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1424     }
1426     /**
1427      * Ensure that a course context previously flagged as approved is removed.
1428      */
1429     public function test_process_course_context() {
1430         $this->resetAfterTest();
1432         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1434         $course = $this->getDataGenerator()->create_course([
1435                 'startdate' => time() - (2 * YEARSECS),
1436                 'enddate' => time() - YEARSECS,
1437             ]);
1438         $coursecontext = \context_course::instance($course->id);
1440         // Create an existing expired_context.
1441         $expiredcontext = new expired_context(0, (object) [
1442                 'contextid' => $coursecontext->id,
1443                 'status' => expired_context::STATUS_APPROVED,
1444             ]);
1445         $expiredcontext->save();
1447         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1448             ->setMethods([
1449                 'delete_data_for_user',
1450                 'delete_data_for_all_users_in_context',
1451             ])
1452             ->getMock();
1453         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1454         $mockprivacymanager->expects($this->once())->method('delete_data_for_all_users_in_context');
1456         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1457             ->setMethods(['get_privacy_manager'])
1458             ->getMock();
1459         $manager->set_progress(new \null_progress_trace());
1461         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1462         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1464         $this->assertEquals(1, $processedcourses);
1465         $this->assertEquals(0, $processedusers);
1467         $updatedcontext = new expired_context($expiredcontext->get('id'));
1468         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1469     }
1471     /**
1472      * Ensure that a user context previously flagged as approved is not removed if the user then logs in.
1473      */
1474     public function test_process_user_context_logged_in_after_approval() {
1475         $this->resetAfterTest();
1477         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1479         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1480         $usercontext = \context_user::instance($user->id);
1482         $this->setUser($user);
1483         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1484         $context = \context_block::instance($block->instance->id);
1485         $this->setUser();
1487         // Create an existing expired_context.
1488         $expiredcontext = new expired_context(0, (object) [
1489                 'contextid' => $usercontext->id,
1490                 'status' => expired_context::STATUS_APPROVED,
1491             ]);
1492         $expiredcontext->save();
1494         // Now bump the user's last login time.
1495         $this->setUser($user);
1496         user_accesstime_log();
1497         $this->setUser();
1499         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1500             ->setMethods([
1501                 'delete_data_for_user',
1502                 'delete_data_for_all_users_in_context',
1503             ])
1504             ->getMock();
1505         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1506         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1508         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1509             ->setMethods(['get_privacy_manager'])
1510             ->getMock();
1511         $manager->set_progress(new \null_progress_trace());
1513         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1514         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1516         $this->assertEquals(0, $processedcourses);
1517         $this->assertEquals(0, $processedusers);
1519         $this->expectException('dml_missing_record_exception');
1520         new expired_context($expiredcontext->get('id'));
1521     }
1523     /**
1524      * Ensure that a user context previously flagged as approved is not removed if the purpose has changed.
1525      */
1526     public function test_process_user_context_changed_after_approved() {
1527         $this->resetAfterTest();
1529         $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1531         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
1532         $usercontext = \context_user::instance($user->id);
1534         $this->setUser($user);
1535         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1536         $context = \context_block::instance($block->instance->id);
1537         $this->setUser();
1539         // Create an existing expired_context.
1540         $expiredcontext = new expired_context(0, (object) [
1541                 'contextid' => $usercontext->id,
1542                 'status' => expired_context::STATUS_APPROVED,
1543             ]);
1544         $expiredcontext->save();
1546         // Now make the user a site admin.
1547         $admins = explode(',', get_config('moodle', 'siteadmins'));
1548         $admins[] = $user->id;
1549         set_config('siteadmins', implode(',', $admins));
1551         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1552             ->setMethods([
1553                 'delete_data_for_user',
1554                 'delete_data_for_all_users_in_context',
1555             ])
1556             ->getMock();
1557         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1558         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1560         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1561             ->setMethods(['get_privacy_manager'])
1562             ->getMock();
1563         $manager->set_progress(new \null_progress_trace());
1565         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1566         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1568         $this->assertEquals(0, $processedcourses);
1569         $this->assertEquals(0, $processedusers);
1571         $this->expectException('dml_missing_record_exception');
1572         new expired_context($expiredcontext->get('id'));
1573     }
1575     /**
1576      * Ensure that a user with a historically expired expired block record child is cleaned up.
1577      */
1578     public function test_process_user_historic_block_unapproved() {
1579         $this->resetAfterTest();
1581         list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1583         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
1584         $usercontext = \context_user::instance($user->id);
1586         $this->setUser($user);
1587         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1588         $blockcontext = \context_block::instance($block->instance->id);
1589         $this->setUser();
1591         // Create an expired_context for the user.
1592         $expiredusercontext = new expired_context(0, (object) [
1593                 'contextid' => $usercontext->id,
1594                 'status' => expired_context::STATUS_APPROVED,
1595             ]);
1596         $expiredusercontext->save();
1598         // Create an existing expired_context which has not been approved for the block.
1599         $expiredblockcontext = new expired_context(0, (object) [
1600                 'contextid' => $blockcontext->id,
1601                 'status' => expired_context::STATUS_EXPIRED,
1602             ]);
1603         $expiredblockcontext->save();
1605         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1606             ->setMethods([
1607                 'delete_data_for_user',
1608                 'delete_data_for_all_users_in_context',
1609             ])
1610             ->getMock();
1611         $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
1612         $mockprivacymanager->expects($this->exactly(2))
1613             ->method('delete_data_for_all_users_in_context')
1614             ->withConsecutive(
1615                 [$blockcontext],
1616                 [$usercontext]
1617             );
1619         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1620             ->setMethods(['get_privacy_manager'])
1621             ->getMock();
1622         $manager->set_progress(new \null_progress_trace());
1624         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1625         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1627         $this->assertEquals(0, $processedcourses);
1628         $this->assertEquals(1, $processedusers);
1630         $updatedcontext = new expired_context($expiredusercontext->get('id'));
1631         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1632     }
1634     /**
1635      * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
1636      */
1637     public function test_process_user_historic_unexpired_child() {
1638         $this->resetAfterTest();
1640         list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1641         $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
1643         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
1644         $usercontext = \context_user::instance($user->id);
1646         $this->setUser($user);
1647         $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
1648         $blockcontext = \context_block::instance($block->instance->id);
1649         $this->setUser();
1651         // Create an expired_context for the user.
1652         $expiredusercontext = new expired_context(0, (object) [
1653                 'contextid' => $usercontext->id,
1654                 'status' => expired_context::STATUS_APPROVED,
1655             ]);
1656         $expiredusercontext->save();
1658         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1659             ->setMethods([
1660                 'delete_data_for_user',
1661                 'delete_data_for_all_users_in_context',
1662             ])
1663             ->getMock();
1664         $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
1665         $mockprivacymanager->expects($this->exactly(2))
1666             ->method('delete_data_for_all_users_in_context')
1667             ->withConsecutive(
1668                 [$blockcontext],
1669                 [$usercontext]
1670             );
1672         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1673             ->setMethods(['get_privacy_manager'])
1674             ->getMock();
1675         $manager->set_progress(new \null_progress_trace());
1677         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1678         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1680         $this->assertEquals(0, $processedcourses);
1681         $this->assertEquals(1, $processedusers);
1683         $updatedcontext = new expired_context($expiredusercontext->get('id'));
1684         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1685     }
1687     /**
1688      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1689      * updated.
1690      */
1691     public function test_process_course_context_updated() {
1692         $this->resetAfterTest();
1694         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1696         $course = $this->getDataGenerator()->create_course([
1697                 'startdate' => time() - (2 * YEARSECS),
1698                 'enddate' => time() - YEARSECS,
1699             ]);
1700         $coursecontext = \context_course::instance($course->id);
1701         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1703         // Create an existing expired_context.
1704         $expiredcontext = new expired_context(0, (object) [
1705                 'contextid' => $coursecontext->id,
1706                 'status' => expired_context::STATUS_APPROVED,
1707             ]);
1708         $expiredcontext->save();
1710         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1711             ->setMethods([
1712                 'delete_data_for_user',
1713                 'delete_data_for_all_users_in_context',
1714             ])
1715             ->getMock();
1716         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1717         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1719         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1720             ->setMethods(['get_privacy_manager'])
1721             ->getMock();
1722         $manager->set_progress(new \null_progress_trace());
1723         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1725         $coursepurpose = $purposes[2];
1726         $coursepurpose->set('retentionperiod', 'P5Y');
1727         $coursepurpose->save();
1729         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1731         $this->assertEquals(0, $processedcourses);
1732         $this->assertEquals(0, $processedusers);
1734         $updatedcontext = new expired_context($expiredcontext->get('id'));
1736         // No change - we just can't process it until the children have finished.
1737         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1738     }
1740     /**
1741      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1742      * updated.
1743      */
1744     public function test_process_course_context_outstanding_children() {
1745         $this->resetAfterTest();
1747         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1749         $course = $this->getDataGenerator()->create_course([
1750                 'startdate' => time() - (2 * YEARSECS),
1751                 'enddate' => time() - YEARSECS,
1752             ]);
1753         $coursecontext = \context_course::instance($course->id);
1754         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1756         // Create an existing expired_context.
1757         $expiredcontext = new expired_context(0, (object) [
1758                 'contextid' => $coursecontext->id,
1759                 'status' => expired_context::STATUS_APPROVED,
1760             ]);
1761         $expiredcontext->save();
1763         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1764             ->setMethods([
1765                 'delete_data_for_user',
1766                 'delete_data_for_all_users_in_context',
1767             ])
1768             ->getMock();
1769         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1770         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1772         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1773             ->setMethods(['get_privacy_manager'])
1774             ->getMock();
1775         $manager->set_progress(new \null_progress_trace());
1777         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1778         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1780         $this->assertEquals(0, $processedcourses);
1781         $this->assertEquals(0, $processedusers);
1783         $updatedcontext = new expired_context($expiredcontext->get('id'));
1785         // No change - we just can't process it until the children have finished.
1786         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1787     }
1789     /**
1790      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1791      * updated.
1792      */
1793     public function test_process_course_context_pending_children() {
1794         $this->resetAfterTest();
1796         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1798         $course = $this->getDataGenerator()->create_course([
1799                 'startdate' => time() - (2 * YEARSECS),
1800                 'enddate' => time() - YEARSECS,
1801             ]);
1802         $coursecontext = \context_course::instance($course->id);
1803         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1804         $cm = get_coursemodule_from_instance('forum', $forum->id);
1805         $forumcontext = \context_module::instance($cm->id);
1807         // Create an existing expired_context for the course.
1808         $expiredcoursecontext = new expired_context(0, (object) [
1809                 'contextid' => $coursecontext->id,
1810                 'status' => expired_context::STATUS_APPROVED,
1811             ]);
1812         $expiredcoursecontext->save();
1814         // And for the forum.
1815         $expiredforumcontext = new expired_context(0, (object) [
1816                 'contextid' => $forumcontext->id,
1817                 'status' => expired_context::STATUS_EXPIRED,
1818             ]);
1819         $expiredforumcontext->save();
1821         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1822             ->setMethods([
1823                 'delete_data_for_user',
1824                 'delete_data_for_all_users_in_context',
1825             ])
1826             ->getMock();
1827         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1828         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1830         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1831             ->setMethods(['get_privacy_manager'])
1832             ->getMock();
1833         $manager->set_progress(new \null_progress_trace());
1835         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1836         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1838         $this->assertEquals(0, $processedcourses);
1839         $this->assertEquals(0, $processedusers);
1841         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1843         // No change - we just can't process it until the children have finished.
1844         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1845     }
1847     /**
1848      * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1849      * updated.
1850      */
1851     public function test_process_course_context_approved_children() {
1852         $this->resetAfterTest();
1854         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1856         $course = $this->getDataGenerator()->create_course([
1857                 'startdate' => time() - (2 * YEARSECS),
1858                 'enddate' => time() - YEARSECS,
1859             ]);
1860         $coursecontext = \context_course::instance($course->id);
1861         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1862         $cm = get_coursemodule_from_instance('forum', $forum->id);
1863         $forumcontext = \context_module::instance($cm->id);
1865         // Create an existing expired_context for the course.
1866         $expiredcoursecontext = new expired_context(0, (object) [
1867                 'contextid' => $coursecontext->id,
1868                 'status' => expired_context::STATUS_APPROVED,
1869             ]);
1870         $expiredcoursecontext->save();
1872         // And for the forum.
1873         $expiredforumcontext = new expired_context(0, (object) [
1874                 'contextid' => $forumcontext->id,
1875                 'status' => expired_context::STATUS_APPROVED,
1876             ]);
1877         $expiredforumcontext->save();
1879         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1880             ->setMethods([
1881                 'delete_data_for_user',
1882                 'delete_data_for_all_users_in_context',
1883             ])
1884             ->getMock();
1885         $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1886         $mockprivacymanager->expects($this->exactly(2))
1887             ->method('delete_data_for_all_users_in_context')
1888             ->withConsecutive(
1889                 [$forumcontext],
1890                 [$coursecontext]
1891             );
1893         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1894             ->setMethods(['get_privacy_manager'])
1895             ->getMock();
1896         $manager->set_progress(new \null_progress_trace());
1898         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1900         // Initially only the forum will be processed.
1901         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1903         $this->assertEquals(1, $processedcourses);
1904         $this->assertEquals(0, $processedusers);
1906         $updatedcontext = new expired_context($expiredforumcontext->get('id'));
1907         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1909         // The course won't have been processed yet.
1910         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1911         $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1913         // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts.
1914         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1916         $this->assertEquals(1, $processedcourses);
1917         $this->assertEquals(0, $processedusers);
1918         $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1919         $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1920     }
1922     /**
1923      * Test that the can_process_deletion function returns expected results.
1924      *
1925      * @dataProvider    can_process_deletion_provider
1926      * @param       int     $status
1927      * @param       bool    $expected
1928      */
1929     public function test_can_process_deletion($status, $expected) {
1930         $purpose = new expired_context(0, (object) [
1931             'status' => $status,
1933             'contextid' => \context_system::instance()->id,
1934         ]);
1936         $this->assertEquals($expected, $purpose->can_process_deletion());
1937     }
1939     /**
1940      * Data provider for the can_process_deletion tests.
1941      *
1942      * @return  array
1943      */
1944     public function can_process_deletion_provider() : array {
1945         return [
1946             'Pending' => [
1947                 expired_context::STATUS_EXPIRED,
1948                 false,
1949             ],
1950             'Approved' => [
1951                 expired_context::STATUS_APPROVED,
1952                 true,
1953             ],
1954             'Complete' => [
1955                 expired_context::STATUS_CLEANED,
1956                 false,
1957             ],
1958         ];
1959     }
1961     /**
1962      * Test that the is_complete function returns expected results.
1963      *
1964      * @dataProvider        is_complete_provider
1965      * @param       int     $status
1966      * @param       bool    $expected
1967      */
1968     public function test_is_complete($status, $expected) {
1969         $purpose = new expired_context(0, (object) [
1970             'status' => $status,
1971             'contextid' => \context_system::instance()->id,
1972         ]);
1974         $this->assertEquals($expected, $purpose->is_complete());
1975     }
1977     /**
1978      * Data provider for the is_complete tests.
1979      *
1980      * @return  array
1981      */
1982     public function is_complete_provider() : array {
1983         return [
1984             'Pending' => [
1985                 expired_context::STATUS_EXPIRED,
1986                 false,
1987             ],
1988             'Approved' => [
1989                 expired_context::STATUS_APPROVED,
1990                 false,
1991             ],
1992             'Complete' => [
1993                 expired_context::STATUS_CLEANED,
1994                 true,
1995             ],
1996         ];
1997     }
1999     /**
2000      * Test that the is_fully_expired function returns expected results.
2001      *
2002      * @dataProvider        is_fully_expired_provider
2003      * @param       array   $record
2004      * @param       bool    $expected
2005      */
2006     public function test_is_fully_expired($record, $expected) {
2007         $purpose = new expired_context(0, (object) $record);
2009         $this->assertEquals($expected, $purpose->is_fully_expired());
2010     }
2012     /**
2013      * Data provider for the is_fully_expired tests.
2014      *
2015      * @return  array
2016      */
2017     public function is_fully_expired_provider() : array {
2018         return [
2019             'Fully expired' => [
2020                 [
2021                     'status' => expired_context::STATUS_APPROVED,
2022                     'defaultexpired' => 1,
2023                 ],
2024                 true,
2025             ],
2026             'Unexpired roles present' => [
2027                 [
2028                     'status' => expired_context::STATUS_APPROVED,
2029                     'defaultexpired' => 1,
2030                     'unexpiredroles' => json_encode([1]),
2031                 ],
2032                 false,
2033             ],
2034             'Only some expired roles present' => [
2035                 [
2036                     'status' => expired_context::STATUS_APPROVED,
2037                     'defaultexpired' => 0,
2038                     'expiredroles' => json_encode([1]),
2039                 ],
2040                 false,
2041             ],
2042         ];
2043     }
2045     /**
2046      * Ensure that any orphaned records are removed once the context has been removed.
2047      */
2048     public function test_orphaned_records_are_cleared() {
2049         $this->resetAfterTest();
2051         $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
2053         $course = $this->getDataGenerator()->create_course([
2054                 'startdate' => time() - (2 * YEARSECS),
2055                 'enddate' => time() - YEARSECS,
2056             ]);
2057         $context = \context_course::instance($course->id);
2059         // Flag all expired contexts.
2060         $manager = new \tool_dataprivacy\expired_contexts_manager();
2061         $manager->set_progress(new \null_progress_trace());
2062         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
2064         $this->assertEquals(1, $flaggedcourses);
2065         $this->assertEquals(0, $flaggedusers);
2067         // Ensure that the record currently exists.
2068         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2069         $this->assertNotFalse($expiredcontext);
2071         // Approve it.
2072         $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save();
2074         // Process deletions.
2075         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
2077         $this->assertEquals(1, $processedcourses);
2078         $this->assertEquals(0, $processedusers);
2080         // Ensure that the record still exists.
2081         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2082         $this->assertNotFalse($expiredcontext);
2084         // Remove the actual course.
2085         delete_course($course->id, false);
2087         // The record will still exist until we flag it again.
2088         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2089         $this->assertNotFalse($expiredcontext);
2091         list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
2092         $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
2093         $this->assertFalse($expiredcontext);
2094     }
2096     /**
2097      * Ensure that the progres tracer works as expected out of the box.
2098      */
2099     public function test_progress_tracer_default() {
2100         $manager = new \tool_dataprivacy\expired_contexts_manager();
2102         $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
2103         $rcm = $rc->getMethod('get_progress');
2105         $rcm->setAccessible(true);
2106         $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager));
2107     }
2109     /**
2110      * Ensure that the progres tracer works as expected when given a specific traer.
2111      */
2112     public function test_progress_tracer_set() {
2113         $manager = new \tool_dataprivacy\expired_contexts_manager();
2114         $mytrace = new \null_progress_trace();
2115         $manager->set_progress($mytrace);
2117         $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
2118         $rcm = $rc->getMethod('get_progress');
2120         $rcm->setAccessible(true);
2121         $this->assertSame($mytrace, $rcm->invoke($manager));
2122     }
2124     /**
2125      * Creates an HTML block on a user.
2126      *
2127      * @param   string  $title
2128      * @param   string  $body
2129      * @param   string  $format
2130      * @return  \block_instance
2131      */
2132     protected function create_user_block($title, $body, $format) {
2133         global $USER;
2135         $configdata = (object) [
2136             'title' => $title,
2137             'text' => [
2138                 'itemid' => 19,
2139                 'text' => $body,
2140                 'format' => $format,
2141             ],
2142         ];
2144         $this->create_block($this->construct_user_page($USER));
2145         $block = $this->get_last_block_on_page($this->construct_user_page($USER));
2146         $block = block_instance('html', $block->instance);
2147         $block->instance_config_save((object) $configdata);
2149         return $block;
2150     }
2152     /**
2153      * Creates an HTML block on a page.
2154      *
2155      * @param \page $page Page
2156      */
2157     protected function create_block($page) {
2158         $page->blocks->add_block_at_end_of_default_region('html');
2159     }
2161     /**
2162      * Constructs a Page object for the User Dashboard.
2163      *
2164      * @param   \stdClass       $user User to create Dashboard for.
2165      * @return  \moodle_page
2166      */
2167     protected function construct_user_page(\stdClass $user) {
2168         $page = new \moodle_page();
2169         $page->set_context(\context_user::instance($user->id));
2170         $page->set_pagelayout('mydashboard');
2171         $page->set_pagetype('my-index');
2172         $page->blocks->load_blocks();
2173         return $page;
2174     }
2176     /**
2177      * Get the last block on the page.
2178      *
2179      * @param \page $page Page
2180      * @return \block_html Block instance object
2181      */
2182     protected function get_last_block_on_page($page) {
2183         $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
2184         $block = end($blocks);
2186         return $block;
2187     }