Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

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