Search moodle.org's
Developer Documentation

Long Term Support Release

  • Bug fixes for general core bugs in 3.5.x ended May 2019 (12 months).
  • Bug fixes for security issues in 3.5.x will end 10 May 2021 (36 months) - Support has ended.
  • minimum PHP 7.0.0 Note: minimum PHP version has increased since Moodle 3.3. PHP 7.1.x and 7.2.x are supported too. PHP 7.x could have some engine limitations.
  • Differences Between: [Versions 35 and 311]

       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  }