2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Expired contexts tests.
20 * @package tool_dataprivacy
21 * @copyright 2018 David Monllao
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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\category;
30 use tool_dataprivacy\contextlevel;
32 defined('MOODLE_INTERNAL') || die();
36 * Expired contexts tests.
38 * @package tool_dataprivacy
39 * @copyright 2018 David Monllao
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
45 * Setup the basics with the specified retention period.
47 * @param string $system Retention policy for the system.
48 * @param string $user Retention policy for users.
49 * @param string $course Retention policy for courses.
50 * @param string $activity Retention policy for activities.
52 protected function setup_basics(string $system, string $user, string $course, string $activity = null) : array {
53 $this->resetAfterTest();
56 $purposes[] = $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM);
57 $purposes[] = $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER);
58 $purposes[] = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
59 if (null !== $activity) {
60 $purposes[] = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
67 * Create a retention period and set it for the specified context level.
69 * @param string $retention
70 * @param int $contextlevel
73 protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
74 $purpose = new purpose(0, (object) [
75 'name' => 'Test purpose ' . rand(1, 1000),
76 'retentionperiod' => $retention,
77 'lawfulbases' => 'gdpr_art_6_1_a',
81 $cat = new category(0, (object) ['name' => 'Test category']);
84 if ($contextlevel <= CONTEXT_USER) {
86 'purposeid' => $purpose->get('id'),
87 'categoryid' => $cat->get('id'),
88 'contextlevel' => $contextlevel,
90 api::set_contextlevel($record);
92 list($purposevar, ) = data_registry::var_names_from_context(
93 \context_helper::get_class_for_level(CONTEXT_COURSE)
95 set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
102 * Ensure that a user with no lastaccess is not flagged for deletion.
104 public function test_flag_not_setup() {
105 $this->resetAfterTest();
107 $user = $this->getDataGenerator()->create_user();
109 $this->setUser($user);
110 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
111 $context = \context_block::instance($block->instance->id);
114 // Flag all expired contexts.
115 $manager = new \tool_dataprivacy\expired_contexts_manager();
116 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
118 $this->assertEquals(0, $flaggedcourses);
119 $this->assertEquals(0, $flaggedusers);
123 * Ensure that a user with no lastaccess is not flagged for deletion.
125 public function test_flag_user_no_lastaccess() {
126 $this->resetAfterTest();
128 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
130 $user = $this->getDataGenerator()->create_user();
132 $this->setUser($user);
133 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
134 $context = \context_block::instance($block->instance->id);
137 // Flag all expired contexts.
138 $manager = new \tool_dataprivacy\expired_contexts_manager();
139 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
141 $this->assertEquals(0, $flaggedcourses);
142 $this->assertEquals(0, $flaggedusers);
146 * Ensure that a user with a recent lastaccess is not flagged for deletion.
148 public function test_flag_user_recent_lastaccess() {
149 $this->resetAfterTest();
151 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
153 $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
155 $this->setUser($user);
156 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
157 $context = \context_block::instance($block->instance->id);
160 // Flag all expired contexts.
161 $manager = new \tool_dataprivacy\expired_contexts_manager();
162 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
164 $this->assertEquals(0, $flaggedcourses);
165 $this->assertEquals(0, $flaggedusers);
169 * Ensure that a user with a lastaccess in the past is flagged for deletion.
171 public function test_flag_user_past_lastaccess() {
172 $this->resetAfterTest();
174 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
176 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
178 $this->setUser($user);
179 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
180 $context = \context_block::instance($block->instance->id);
183 // Flag all expired contexts.
184 $manager = new \tool_dataprivacy\expired_contexts_manager();
185 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
187 // Although there is a block in the user context, everything in the user context is regarded as one.
188 $this->assertEquals(0, $flaggedcourses);
189 $this->assertEquals(1, $flaggedusers);
193 * Ensure that a user with a lastaccess in the past but active enrolments is not flagged for deletion.
195 public function test_flag_user_past_lastaccess_still_enrolled() {
196 $this->resetAfterTest();
198 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
200 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
201 $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enddate' => time() + YEARSECS]);
202 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
204 $otheruser = $this->getDataGenerator()->create_user();
205 $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
207 $this->setUser($user);
208 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
209 $context = \context_block::instance($block->instance->id);
212 // Flag all expired contexts.
213 $manager = new \tool_dataprivacy\expired_contexts_manager();
214 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
216 $this->assertEquals(0, $flaggedcourses);
217 $this->assertEquals(0, $flaggedusers);
221 * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion.
223 public function test_flag_user_past_lastaccess_enrol_expired() {
224 $this->resetAfterTest();
226 $this->setup_basics('PT1H', 'PT1H', 'P5Y');
228 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
229 $course = $this->getDataGenerator()->create_course(['startdate' => time() - (YEARSECS * 2), 'enddate' => time() - DAYSECS]);
230 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
232 $this->setUser($user);
233 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
234 $context = \context_block::instance($block->instance->id);
237 // Flag all expired contexts.
238 $manager = new \tool_dataprivacy\expired_contexts_manager();
239 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
241 // Although there is a block in the user context, everything in the user context is regarded as one.
242 $this->assertEquals(0, $flaggedcourses);
243 $this->assertEquals(1, $flaggedusers);
247 * Ensure that a user with a recent lastaccess is not flagged for deletion.
249 public function test_flag_user_recent_lastaccess_existing_record() {
250 $this->resetAfterTest();
252 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
254 $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
255 $usercontext = \context_user::instance($user->id);
257 // Create an existing expired_context.
258 $expiredcontext = new expired_context(0, (object) [
259 'contextid' => $usercontext->id,
260 'status' => expired_context::STATUS_EXPIRED,
262 $expiredcontext->save();
264 $this->setUser($user);
265 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
266 $context = \context_block::instance($block->instance->id);
269 // Flag all expired contexts.
270 $manager = new \tool_dataprivacy\expired_contexts_manager();
271 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
273 $this->assertEquals(0, $flaggedcourses);
274 $this->assertEquals(0, $flaggedusers);
276 $this->expectException('dml_missing_record_exception');
277 new expired_context($expiredcontext->get('id'));
281 * Ensure that a user with a recent lastaccess is not flagged for deletion.
283 public function test_flag_user_retention_changed() {
284 $this->resetAfterTest();
286 list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
288 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
289 $usercontext = \context_user::instance($user->id);
291 $this->setUser($user);
292 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
293 $context = \context_block::instance($block->instance->id);
296 // Flag all expired contexts.
297 $manager = new \tool_dataprivacy\expired_contexts_manager();
298 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
300 $this->assertEquals(0, $flaggedcourses);
301 $this->assertEquals(1, $flaggedusers);
303 $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
304 $this->assertNotFalse($expiredcontext);
306 // Increase the retention period to 5 years.
307 $userpurpose->set('retentionperiod', 'P5Y');
308 $userpurpose->save();
310 // Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased.
311 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
312 $this->assertEquals(0, $flaggedcourses);
313 $this->assertEquals(0, $flaggedusers);
315 // The expiry record will now have been removed.
316 $this->expectException('dml_missing_record_exception');
317 new expired_context($expiredcontext->get('id'));
321 * Ensure that a user with a historically expired expired block record child is cleaned up.
323 public function test_flag_user_historic_block_unapproved() {
324 $this->resetAfterTest();
326 list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
328 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
329 $usercontext = \context_user::instance($user->id);
331 $this->setUser($user);
332 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
333 $blockcontext = \context_block::instance($block->instance->id);
336 // Create an existing expired_context which has not been approved for the block.
337 $expiredcontext = new expired_context(0, (object) [
338 'contextid' => $blockcontext->id,
339 'status' => expired_context::STATUS_EXPIRED,
341 $expiredcontext->save();
343 // Flag all expired contexts.
344 $manager = new \tool_dataprivacy\expired_contexts_manager();
345 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
347 $this->assertEquals(0, $flaggedcourses);
348 $this->assertEquals(1, $flaggedusers);
350 $expiredblockcontext = expired_context::get_record(['contextid' => $blockcontext->id]);
351 $this->assertFalse($expiredblockcontext);
353 $expiredusercontext = expired_context::get_record(['contextid' => $usercontext->id]);
354 $this->assertNotFalse($expiredusercontext);
358 * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
360 public function test_flag_user_historic_unexpired_child() {
361 $this->resetAfterTest();
363 list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
364 $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
366 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
367 $usercontext = \context_user::instance($user->id);
369 $this->setUser($user);
370 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
371 $blockcontext = \context_block::instance($block->instance->id);
374 // Flag all expired contexts.
375 $manager = new \tool_dataprivacy\expired_contexts_manager();
376 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
378 $this->assertEquals(0, $flaggedcourses);
379 $this->assertEquals(1, $flaggedusers);
381 $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
382 $this->assertNotFalse($expiredcontext);
386 * Ensure that a course with no end date is not flagged.
388 public function test_flag_course_no_enddate() {
389 $this->resetAfterTest();
391 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
393 $course = $this->getDataGenerator()->create_course();
394 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
396 // Flag all expired contexts.
397 $manager = new \tool_dataprivacy\expired_contexts_manager();
398 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
400 $this->assertEquals(0, $flaggedcourses);
401 $this->assertEquals(0, $flaggedusers);
405 * Ensure that a course with an end date in the distant past, but a child which is unexpired is not flagged.
407 public function test_flag_course_past_enddate_future_child() {
408 $this->resetAfterTest();
410 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'P5Y');
412 $course = $this->getDataGenerator()->create_course([
413 'startdate' => time() - (2 * YEARSECS),
414 'enddate' => time() - YEARSECS,
416 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
418 // Flag all expired contexts.
419 $manager = new \tool_dataprivacy\expired_contexts_manager();
420 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
422 $this->assertEquals(0, $flaggedcourses);
423 $this->assertEquals(0, $flaggedusers);
427 * Ensure that a course with an end date in the distant past is flagged.
429 public function test_flag_course_past_enddate() {
430 $this->resetAfterTest();
432 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
434 $course = $this->getDataGenerator()->create_course([
435 'startdate' => time() - (2 * YEARSECS),
436 'enddate' => time() - YEARSECS,
438 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
440 // Flag all expired contexts.
441 $manager = new \tool_dataprivacy\expired_contexts_manager();
442 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
444 $this->assertEquals(2, $flaggedcourses);
445 $this->assertEquals(0, $flaggedusers);
449 * Ensure that a course with an end date in the distant past is flagged.
451 public function test_flag_course_past_enddate_multiple() {
452 $this->resetAfterTest();
454 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
456 $course1 = $this->getDataGenerator()->create_course([
457 'startdate' => time() - (2 * YEARSECS),
458 'enddate' => time() - YEARSECS,
460 $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
462 $course2 = $this->getDataGenerator()->create_course([
463 'startdate' => time() - (2 * YEARSECS),
464 'enddate' => time() - YEARSECS,
466 $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
468 // Flag all expired contexts.
469 $manager = new \tool_dataprivacy\expired_contexts_manager();
470 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
472 $this->assertEquals(4, $flaggedcourses);
473 $this->assertEquals(0, $flaggedusers);
477 * Ensure that a course with an end date in the future is not flagged.
479 public function test_flag_course_future_enddate() {
480 $this->resetAfterTest();
482 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
484 $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
485 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
487 // Flag all expired contexts.
488 $manager = new \tool_dataprivacy\expired_contexts_manager();
489 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
491 $this->assertEquals(0, $flaggedcourses);
492 $this->assertEquals(0, $flaggedusers);
496 * Ensure that a course with an end date in the future is not flagged.
498 public function test_flag_course_recent_unexpired_enddate() {
499 $this->resetAfterTest();
501 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
503 $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);
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(0, $flaggedusers);
514 * Ensure that a site not setup will not process anything.
516 public function test_process_not_setup() {
517 $this->resetAfterTest();
519 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
520 $usercontext = \context_user::instance($user->id);
522 // Create an existing expired_context.
523 $expiredcontext = new expired_context(0, (object) [
524 'contextid' => $usercontext->id,
525 'status' => expired_context::STATUS_EXPIRED,
527 $expiredcontext->save();
529 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
531 'delete_data_for_user',
532 'delete_data_for_all_users_in_context',
535 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
536 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
538 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
539 ->setMethods(['get_privacy_manager'])
541 $manager->set_progress(new \null_progress_trace());
543 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
544 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
546 $this->assertEquals(0, $processedcourses);
547 $this->assertEquals(0, $processedusers);
551 * Ensure that a user with no lastaccess is not flagged for deletion.
553 public function test_process_none_approved() {
554 $this->resetAfterTest();
556 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
558 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
559 $usercontext = \context_user::instance($user->id);
561 // Create an existing expired_context.
562 $expiredcontext = new expired_context(0, (object) [
563 'contextid' => $usercontext->id,
564 'status' => expired_context::STATUS_EXPIRED,
566 $expiredcontext->save();
568 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
570 'delete_data_for_user',
571 'delete_data_for_all_users_in_context',
574 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
575 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
577 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
578 ->setMethods(['get_privacy_manager'])
580 $manager->set_progress(new \null_progress_trace());
582 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
583 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
585 $this->assertEquals(0, $processedcourses);
586 $this->assertEquals(0, $processedusers);
590 * Ensure that a user with no lastaccess is not flagged for deletion.
592 public function test_process_no_context() {
593 $this->resetAfterTest();
595 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
597 // Create an existing expired_context.
598 $expiredcontext = new expired_context(0, (object) [
600 'status' => expired_context::STATUS_APPROVED,
602 $expiredcontext->save();
604 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
606 'delete_data_for_user',
607 'delete_data_for_all_users_in_context',
610 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
611 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
613 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
614 ->setMethods(['get_privacy_manager'])
616 $manager->set_progress(new \null_progress_trace());
618 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
619 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
621 $this->assertEquals(0, $processedcourses);
622 $this->assertEquals(0, $processedusers);
624 $this->expectException('dml_missing_record_exception');
625 new expired_context($expiredcontext->get('id'));
629 * Ensure that a user context previously flagged as approved is removed.
631 public function test_process_user_context() {
632 $this->resetAfterTest();
634 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
636 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
637 $usercontext = \context_user::instance($user->id);
639 $this->setUser($user);
640 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
641 $blockcontext = \context_block::instance($block->instance->id);
644 // Create an existing expired_context.
645 $expiredcontext = new expired_context(0, (object) [
646 'contextid' => $usercontext->id,
647 'status' => expired_context::STATUS_APPROVED,
649 $expiredcontext->save();
651 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
653 'delete_data_for_user',
654 'delete_data_for_all_users_in_context',
657 $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
658 $mockprivacymanager->expects($this->exactly(2))
659 ->method('delete_data_for_all_users_in_context')
665 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
666 ->setMethods(['get_privacy_manager'])
668 $manager->set_progress(new \null_progress_trace());
670 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
671 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
673 $this->assertEquals(0, $processedcourses);
674 $this->assertEquals(1, $processedusers);
676 $updatedcontext = new expired_context($expiredcontext->get('id'));
677 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
679 // Flag all expired contexts again.
680 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
682 $this->assertEquals(0, $flaggedcourses);
683 $this->assertEquals(0, $flaggedusers);
685 // Ensure that the deleted context record is still present.
686 $updatedcontext = new expired_context($expiredcontext->get('id'));
687 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
691 * Ensure that a course context previously flagged as approved is removed.
693 public function test_process_course_context() {
694 $this->resetAfterTest();
696 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
698 $course = $this->getDataGenerator()->create_course([
699 'startdate' => time() - (2 * YEARSECS),
700 'enddate' => time() - YEARSECS,
702 $coursecontext = \context_course::instance($course->id);
704 // Create an existing expired_context.
705 $expiredcontext = new expired_context(0, (object) [
706 'contextid' => $coursecontext->id,
707 'status' => expired_context::STATUS_APPROVED,
709 $expiredcontext->save();
711 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
713 'delete_data_for_user',
714 'delete_data_for_all_users_in_context',
717 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
718 $mockprivacymanager->expects($this->once())->method('delete_data_for_all_users_in_context');
720 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
721 ->setMethods(['get_privacy_manager'])
723 $manager->set_progress(new \null_progress_trace());
725 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
726 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
728 $this->assertEquals(1, $processedcourses);
729 $this->assertEquals(0, $processedusers);
731 $updatedcontext = new expired_context($expiredcontext->get('id'));
732 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
736 * Ensure that a user context previously flagged as approved is not removed if the user then logs in.
738 public function test_process_user_context_logged_in_after_approval() {
739 $this->resetAfterTest();
741 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
743 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
744 $usercontext = \context_user::instance($user->id);
746 $this->setUser($user);
747 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
748 $context = \context_block::instance($block->instance->id);
751 // Create an existing expired_context.
752 $expiredcontext = new expired_context(0, (object) [
753 'contextid' => $usercontext->id,
754 'status' => expired_context::STATUS_APPROVED,
756 $expiredcontext->save();
758 // Now bump the user's last login time.
759 $this->setUser($user);
760 user_accesstime_log();
763 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
765 'delete_data_for_user',
766 'delete_data_for_all_users_in_context',
769 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
770 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
772 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
773 ->setMethods(['get_privacy_manager'])
775 $manager->set_progress(new \null_progress_trace());
777 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
778 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
780 $this->assertEquals(0, $processedcourses);
781 $this->assertEquals(0, $processedusers);
783 $this->expectException('dml_missing_record_exception');
784 new expired_context($expiredcontext->get('id'));
788 * Ensure that a user context previously flagged as approved is not removed if the purpose has changed.
790 public function test_process_user_context_changed_after_approved() {
791 $this->resetAfterTest();
793 $this->setup_basics('PT1H', 'PT1H', 'PT1H');
795 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
796 $usercontext = \context_user::instance($user->id);
798 $this->setUser($user);
799 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
800 $context = \context_block::instance($block->instance->id);
803 // Create an existing expired_context.
804 $expiredcontext = new expired_context(0, (object) [
805 'contextid' => $usercontext->id,
806 'status' => expired_context::STATUS_APPROVED,
808 $expiredcontext->save();
810 // Now make the user a site admin.
811 $admins = explode(',', get_config('moodle', 'siteadmins'));
812 $admins[] = $user->id;
813 set_config('siteadmins', implode(',', $admins));
815 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
817 'delete_data_for_user',
818 'delete_data_for_all_users_in_context',
821 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
822 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
824 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
825 ->setMethods(['get_privacy_manager'])
827 $manager->set_progress(new \null_progress_trace());
829 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
830 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
832 $this->assertEquals(0, $processedcourses);
833 $this->assertEquals(0, $processedusers);
835 $this->expectException('dml_missing_record_exception');
836 new expired_context($expiredcontext->get('id'));
840 * Ensure that a user with a historically expired expired block record child is cleaned up.
842 public function test_process_user_historic_block_unapproved() {
843 $this->resetAfterTest();
845 list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
847 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
848 $usercontext = \context_user::instance($user->id);
850 $this->setUser($user);
851 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
852 $blockcontext = \context_block::instance($block->instance->id);
855 // Create an expired_context for the user.
856 $expiredusercontext = new expired_context(0, (object) [
857 'contextid' => $usercontext->id,
858 'status' => expired_context::STATUS_APPROVED,
860 $expiredusercontext->save();
862 // Create an existing expired_context which has not been approved for the block.
863 $expiredblockcontext = new expired_context(0, (object) [
864 'contextid' => $blockcontext->id,
865 'status' => expired_context::STATUS_EXPIRED,
867 $expiredblockcontext->save();
869 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
871 'delete_data_for_user',
872 'delete_data_for_all_users_in_context',
875 $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
876 $mockprivacymanager->expects($this->exactly(2))
877 ->method('delete_data_for_all_users_in_context')
883 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
884 ->setMethods(['get_privacy_manager'])
886 $manager->set_progress(new \null_progress_trace());
888 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
889 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
891 $this->assertEquals(0, $processedcourses);
892 $this->assertEquals(1, $processedusers);
894 $updatedcontext = new expired_context($expiredusercontext->get('id'));
895 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
899 * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
901 public function test_process_user_historic_unexpired_child() {
902 $this->resetAfterTest();
904 list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
905 $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
907 $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
908 $usercontext = \context_user::instance($user->id);
910 $this->setUser($user);
911 $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
912 $blockcontext = \context_block::instance($block->instance->id);
915 // Create an expired_context for the user.
916 $expiredusercontext = new expired_context(0, (object) [
917 'contextid' => $usercontext->id,
918 'status' => expired_context::STATUS_APPROVED,
920 $expiredusercontext->save();
922 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
924 'delete_data_for_user',
925 'delete_data_for_all_users_in_context',
928 $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
929 $mockprivacymanager->expects($this->exactly(2))
930 ->method('delete_data_for_all_users_in_context')
936 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
937 ->setMethods(['get_privacy_manager'])
939 $manager->set_progress(new \null_progress_trace());
941 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
942 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
944 $this->assertEquals(0, $processedcourses);
945 $this->assertEquals(1, $processedusers);
947 $updatedcontext = new expired_context($expiredusercontext->get('id'));
948 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
952 * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
955 public function test_process_course_context_updated() {
956 $this->resetAfterTest();
958 $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
960 $course = $this->getDataGenerator()->create_course([
961 'startdate' => time() - (2 * YEARSECS),
962 'enddate' => time() - YEARSECS,
964 $coursecontext = \context_course::instance($course->id);
965 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
967 // Create an existing expired_context.
968 $expiredcontext = new expired_context(0, (object) [
969 'contextid' => $coursecontext->id,
970 'status' => expired_context::STATUS_APPROVED,
972 $expiredcontext->save();
974 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
976 'delete_data_for_user',
977 'delete_data_for_all_users_in_context',
980 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
981 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
983 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
984 ->setMethods(['get_privacy_manager'])
986 $manager->set_progress(new \null_progress_trace());
987 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
989 $coursepurpose = $purposes[2];
990 $coursepurpose->set('retentionperiod', 'P5Y');
991 $coursepurpose->save();
993 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
995 $this->assertEquals(0, $processedcourses);
996 $this->assertEquals(0, $processedusers);
998 $updatedcontext = new expired_context($expiredcontext->get('id'));
1000 // No change - we just can't process it until the children have finished.
1001 $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1005 * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1008 public function test_process_course_context_outstanding_children() {
1009 $this->resetAfterTest();
1011 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1013 $course = $this->getDataGenerator()->create_course([
1014 'startdate' => time() - (2 * YEARSECS),
1015 'enddate' => time() - YEARSECS,
1017 $coursecontext = \context_course::instance($course->id);
1018 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1020 // Create an existing expired_context.
1021 $expiredcontext = new expired_context(0, (object) [
1022 'contextid' => $coursecontext->id,
1023 'status' => expired_context::STATUS_APPROVED,
1025 $expiredcontext->save();
1027 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1029 'delete_data_for_user',
1030 'delete_data_for_all_users_in_context',
1033 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1034 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1036 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1037 ->setMethods(['get_privacy_manager'])
1039 $manager->set_progress(new \null_progress_trace());
1041 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1042 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1044 $this->assertEquals(0, $processedcourses);
1045 $this->assertEquals(0, $processedusers);
1047 $updatedcontext = new expired_context($expiredcontext->get('id'));
1049 // No change - we just can't process it until the children have finished.
1050 $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1054 * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1057 public function test_process_course_context_pending_children() {
1058 $this->resetAfterTest();
1060 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1062 $course = $this->getDataGenerator()->create_course([
1063 'startdate' => time() - (2 * YEARSECS),
1064 'enddate' => time() - YEARSECS,
1066 $coursecontext = \context_course::instance($course->id);
1067 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1068 $cm = get_coursemodule_from_instance('forum', $forum->id);
1069 $forumcontext = \context_module::instance($cm->id);
1071 // Create an existing expired_context for the course.
1072 $expiredcoursecontext = new expired_context(0, (object) [
1073 'contextid' => $coursecontext->id,
1074 'status' => expired_context::STATUS_APPROVED,
1076 $expiredcoursecontext->save();
1078 // And for the forum.
1079 $expiredforumcontext = new expired_context(0, (object) [
1080 'contextid' => $forumcontext->id,
1081 'status' => expired_context::STATUS_EXPIRED,
1083 $expiredforumcontext->save();
1085 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1087 'delete_data_for_user',
1088 'delete_data_for_all_users_in_context',
1091 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1092 $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
1094 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1095 ->setMethods(['get_privacy_manager'])
1097 $manager->set_progress(new \null_progress_trace());
1099 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1100 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1102 $this->assertEquals(0, $processedcourses);
1103 $this->assertEquals(0, $processedusers);
1105 $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1107 // No change - we just can't process it until the children have finished.
1108 $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1112 * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
1115 public function test_process_course_context_approved_children() {
1116 $this->resetAfterTest();
1118 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1120 $course = $this->getDataGenerator()->create_course([
1121 'startdate' => time() - (2 * YEARSECS),
1122 'enddate' => time() - YEARSECS,
1124 $coursecontext = \context_course::instance($course->id);
1125 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1126 $cm = get_coursemodule_from_instance('forum', $forum->id);
1127 $forumcontext = \context_module::instance($cm->id);
1129 // Create an existing expired_context for the course.
1130 $expiredcoursecontext = new expired_context(0, (object) [
1131 'contextid' => $coursecontext->id,
1132 'status' => expired_context::STATUS_APPROVED,
1134 $expiredcoursecontext->save();
1136 // And for the forum.
1137 $expiredforumcontext = new expired_context(0, (object) [
1138 'contextid' => $forumcontext->id,
1139 'status' => expired_context::STATUS_APPROVED,
1141 $expiredforumcontext->save();
1143 $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
1145 'delete_data_for_user',
1146 'delete_data_for_all_users_in_context',
1149 $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
1150 $mockprivacymanager->expects($this->exactly(2))
1151 ->method('delete_data_for_all_users_in_context')
1157 $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
1158 ->setMethods(['get_privacy_manager'])
1160 $manager->set_progress(new \null_progress_trace());
1162 $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
1164 // Initially only the forum will be processed.
1165 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1167 $this->assertEquals(1, $processedcourses);
1168 $this->assertEquals(0, $processedusers);
1170 $updatedcontext = new expired_context($expiredforumcontext->get('id'));
1171 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1173 // The course won't have been processed yet.
1174 $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1175 $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
1177 // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts.
1178 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1180 $this->assertEquals(1, $processedcourses);
1181 $this->assertEquals(0, $processedusers);
1182 $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
1183 $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
1187 * Test that the can_process_deletion function returns expected results.
1189 * @dataProvider can_process_deletion_provider
1190 * @param int $status
1191 * @param bool $expected
1193 public function test_can_process_deletion($status, $expected) {
1194 $purpose = new expired_context(0, (object) [
1195 'status' => $status,
1197 'contextid' => \context_system::instance()->id,
1200 $this->assertEquals($expected, $purpose->can_process_deletion());
1204 * Data provider for the can_process_deletion tests.
1208 public function can_process_deletion_provider() : array {
1211 expired_context::STATUS_EXPIRED,
1215 expired_context::STATUS_APPROVED,
1219 expired_context::STATUS_CLEANED,
1226 * Test that the is_complete function returns expected results.
1228 * @dataProvider is_complete_provider
1229 * @param int $status
1230 * @param bool $expected
1232 public function test_is_complete($status, $expected) {
1233 $purpose = new expired_context(0, (object) [
1234 'status' => $status,
1235 'contextid' => \context_system::instance()->id,
1238 $this->assertEquals($expected, $purpose->is_complete());
1242 * Data provider for the is_complete tests.
1246 public function is_complete_provider() : array {
1249 expired_context::STATUS_EXPIRED,
1253 expired_context::STATUS_APPROVED,
1257 expired_context::STATUS_CLEANED,
1264 * Ensure that any orphaned records are removed once the context has been removed.
1266 public function test_orphaned_records_are_cleared() {
1267 $this->resetAfterTest();
1269 $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
1271 $course = $this->getDataGenerator()->create_course([
1272 'startdate' => time() - (2 * YEARSECS),
1273 'enddate' => time() - YEARSECS,
1275 $context = \context_course::instance($course->id);
1277 // Flag all expired contexts.
1278 $manager = new \tool_dataprivacy\expired_contexts_manager();
1279 $manager->set_progress(new \null_progress_trace());
1280 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
1282 $this->assertEquals(1, $flaggedcourses);
1283 $this->assertEquals(0, $flaggedusers);
1285 // Ensure that the record currently exists.
1286 $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
1287 $this->assertNotFalse($expiredcontext);
1290 $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save();
1292 // Process deletions
1293 list($processedcourses, $processedusers) = $manager->process_approved_deletions();
1295 $this->assertEquals(1, $processedcourses);
1296 $this->assertEquals(0, $processedusers);
1298 // Ensure that the record still exists.
1299 $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
1300 $this->assertNotFalse($expiredcontext);
1302 // Remove the actual course.
1303 delete_course($course->id, false);
1305 // The record will still exist until we flag it again.
1306 $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
1307 $this->assertNotFalse($expiredcontext);
1309 list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
1310 $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
1311 $this->assertFalse($expiredcontext);
1315 * Ensure that the progres tracer works as expected out of the box.
1317 public function test_progress_tracer_default() {
1318 $manager = new \tool_dataprivacy\expired_contexts_manager();
1320 $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
1321 $rcm = $rc->getMethod('get_progress');
1323 $rcm->setAccessible(true);
1324 $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager));
1328 * Ensure that the progres tracer works as expected when given a specific traer.
1330 public function test_progress_tracer_set() {
1331 $manager = new \tool_dataprivacy\expired_contexts_manager();
1332 $mytrace = new \null_progress_trace();
1333 $manager->set_progress($mytrace);
1335 $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
1336 $rcm = $rc->getMethod('get_progress');
1338 $rcm->setAccessible(true);
1339 $this->assertSame($mytrace, $rcm->invoke($manager));
1343 * Creates an HTML block on a user.
1345 * @param string $title
1346 * @param string $body
1347 * @param string $format
1348 * @return \block_instance
1350 protected function create_user_block($title, $body, $format) {
1353 $configdata = (object) [
1358 'format' => $format,
1362 $this->create_block($this->construct_user_page($USER));
1363 $block = $this->get_last_block_on_page($this->construct_user_page($USER));
1364 $block = block_instance('html', $block->instance);
1365 $block->instance_config_save((object) $configdata);
1371 * Creates an HTML block on a page.
1373 * @param \page $page Page
1375 protected function create_block($page) {
1376 $page->blocks->add_block_at_end_of_default_region('html');
1380 * Constructs a Page object for the User Dashboard.
1382 * @param \stdClass $user User to create Dashboard for.
1383 * @return \moodle_page
1385 protected function construct_user_page(\stdClass $user) {
1386 $page = new \moodle_page();
1387 $page->set_context(\context_user::instance($user->id));
1388 $page->set_pagelayout('mydashboard');
1389 $page->set_pagetype('my-index');
1390 $page->blocks->load_blocks();
1395 * Get the last block on the page.
1397 * @param \page $page Page
1398 * @return \block_html Block instance object
1400 protected function get_last_block_on_page($page) {
1401 $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
1402 $block = end($blocks);