Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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