Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace factor_token;
  18  
  19  /**
  20   * Tests for MFA manager class.
  21   *
  22   * @package     factor_token
  23   * @author      Peter Burnett <peterburnett@catalyst-au.net>
  24   * @author      Kevin Pham <kevinpham@catalyst-au.net>
  25   * @copyright   Catalyst IT
  26   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  class factor_test extends \advanced_testcase {
  29  
  30      /**
  31       * Holds specific requested factor, which is token factor.
  32       *
  33       * @var \factor_token\factor $factor
  34       */
  35      public \factor_token\factor $factor;
  36  
  37      public function setUp(): void {
  38          $this->resetAfterTest();
  39          $this->factor = new \factor_token\factor('token');
  40      }
  41  
  42      /**
  43       * Test calculating expiry time in general
  44       *
  45       * @covers ::calculate_expiry_time
  46       * @return void
  47       */
  48      public function test_calculate_expiry_time_in_general() {
  49          $timestamp = 1642213800; // 1230 UTC.
  50  
  51          set_config('expireovernight', 0, 'factor_token');
  52          $method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
  53          $method->setAccessible(true);
  54  
  55          // Test that non-overnight timestamps are just exactly as configured.
  56          // We don't need to care about 0 or negative ints, they will just make the cookie expire immediately.
  57          $expiry = $method->invoke($this->factor, $timestamp);
  58          $this->assertEquals(DAYSECS, $expiry[1]);
  59  
  60          set_config('expiry', HOURSECS, 'factor_token');
  61          $expiry = $method->invoke($this->factor, $timestamp);
  62          $this->assertGreaterThan(HOURSECS - 30, $expiry[1]);
  63          $this->assertLessThan(HOURSECS + 30, $expiry[1]);
  64  
  65          set_config('expireovernight', 1, 'factor_token');
  66          // Manually calculate the next reset time.
  67          $reset = strtotime('tomorrow 0200', $timestamp);
  68          $resetdelta = $reset - $timestamp;
  69          // Confirm that a timestamp that doesnt reach reset time.
  70          if ($timestamp + HOURSECS < $reset) {
  71              $expiry = $method->invoke($this->factor, $timestamp);
  72              $this->assertGreaterThan(HOURSECS - 30, $expiry[1]);
  73              $this->assertLessThan(HOURSECS + 30, $expiry[1]);
  74          }
  75  
  76          set_config('expiry', 2 * DAYSECS, 'factor_token');
  77          // Now confirm that the returned expiry is less than the absolute amount.
  78          $expiry = $method->invoke($this->factor, $timestamp);
  79          $this->assertGreaterThan(DAYSECS, $expiry[1]);
  80          $this->assertLessThan(2 * DAYSECS, $expiry[1]);
  81          $this->assertGreaterThan($resetdelta + DAYSECS - 30, $expiry[1]);
  82          $this->assertLessThan($resetdelta + DAYSECS + 30, $expiry[1]);
  83      }
  84  
  85      /**
  86       * Everything should end at 2am unless adding the hours lands it between
  87       * 0 <= x < 2am, which in that case it should just expire using the raw
  88       * value, provided it never goes past raw value expiry time, and when it
  89       * needs to be 2am, it's 2am on the following morning.
  90       *
  91       * @covers ::calculate_expiry_time
  92       * @param int $timestamp
  93       * @dataProvider timestamp_provider
  94       */
  95      public function test_calculate_expiry_time_for_overnight_expiry_with_one_day_expiry($timestamp) {
  96          // Setup configuration.
  97          $method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
  98          $method->setAccessible(true);
  99          set_config('expireovernight', 1, 'factor_token');
 100          set_config('expiry', DAYSECS, 'factor_token');
 101  
 102          // All the results here, should be for 2am the following morning from the timestamp provided.
 103          $expiry = $method->invoke($this->factor, $timestamp);
 104          list($expiresat, $secondstillexpiry) = $expiry;
 105  
 106          // Calculate the expected raw expiry if not considering 'overnight'.
 107          $timezone = \core_date::get_user_timezone_object();
 108          $datetime = new \DateTime();
 109          $datetime->setTimezone($timezone);
 110  
 111          $rawexpiry = $timestamp + DAYSECS;
 112          $datetime->setTimestamp($rawexpiry);
 113          $rawhour = $datetime->format('H');
 114          $rawminute = $datetime->format('m');
 115  
 116          // Sanity check, that the $secondstillexpiry is in the appropriate ranges.
 117          $this->assertGreaterThan(0, $secondstillexpiry);
 118          $this->assertLessThan(DAYSECS + 1, $secondstillexpiry);
 119  
 120          if ($rawhour >= 0 && $rawhour < 2 || $rawhour == 2 && $rawminute == 0) {
 121              // Should just use expiry time, if the hours will land between 0 and 2am.
 122              $this->assertEquals($datetime->getTimestamp(), $expiresat);
 123              // Ensure the $secondstillexpiry is calculated correctly.
 124              $this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
 125          } else {
 126              // Otherwise it should fall on 2am the following day.
 127              $followingdayattwoam = strtotime('tomorrow 0200', $timestamp);
 128              $this->assertEquals($followingdayattwoam, $expiresat);
 129              // Ensure the $secondstillexpiry is calculated correctly.
 130              $this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
 131          }
 132      }
 133  
 134      /**
 135       * Everything should end at 2am unless adding the hours lands it between
 136       * 0 <= x < 2am, which in that case it should just expire using the raw
 137       * value, provided it never goes past raw value expiry time, and when it
 138       * needs to be 2am, it's 2am on the morning after tomorrow.
 139       *
 140       * @covers ::calculate_expiry_time
 141       * @param int $timestamp
 142       * @dataProvider timestamp_provider
 143       */
 144      public function test_calculate_expiry_time_for_overnight_expiry_with_two_day_expiry($timestamp) {
 145          // Setup configuration.
 146          $method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
 147          $method->setAccessible(true);
 148          set_config('expireovernight', 1, 'factor_token');
 149          set_config('expiry', 2 * DAYSECS, 'factor_token');
 150  
 151          // All the results here, should be for 2am the following morning from the timestamp provided.
 152          $expiry = $method->invoke($this->factor, $timestamp);
 153          list($expiresat, $secondstillexpiry) = $expiry;
 154  
 155          // Calculate the expected raw expiry if not considering 'overnight'.
 156          $timezone = \core_date::get_user_timezone_object();
 157          $datetime = new \DateTime();
 158          $datetime->setTimezone($timezone);
 159  
 160          $rawexpiry = $timestamp + (2 * DAYSECS);
 161          $datetime->setTimestamp($rawexpiry);
 162          $rawhour = $datetime->format('H');
 163          $rawminute = $datetime->format('m');
 164  
 165          // Sanity check, that the $secondstillexpiry is in the appropriate ranges.
 166          $this->assertGreaterThan(0, $secondstillexpiry);
 167          $this->assertLessThan((2 * DAYSECS) + 1, $secondstillexpiry);
 168  
 169          if ($rawhour >= 0 && $rawhour < 2 || $rawhour == 2 && $rawminute == 0) {
 170              // Should just use expiry time, if the hours will land between 0 and 2am.
 171              $this->assertEquals($datetime->getTimestamp(), $expiresat);
 172              // Ensure the $secondstillexpiry is calculated correctly.
 173              $this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
 174          } else {
 175              // Otherwise it should fall on 2am the following day after tomorrow.
 176              $followingdayattwoam = strtotime('tomorrow 0200', $timestamp) + DAYSECS;
 177              $this->assertEquals($followingdayattwoam, $expiresat);
 178              // Ensure the $secondstillexpiry is calculated correctly.
 179              $this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
 180          }
 181  
 182          // Expiry should always be more than one day for an expiry duration of
 183          // more than 1 day, but the overnight check should apply for the
 184          // duration of the final night.
 185          $this->assertGreaterThan(DAYSECS, $secondstillexpiry);
 186      }
 187  
 188      /**
 189       * This should check if the 3am expiry is pushed back to 2am as expected, but everything else appears as expected
 190       *
 191       * @covers ::calculate_expiry_time
 192       * @param int $timestamp
 193       * @dataProvider timestamp_provider
 194       */
 195      public function test_calculate_expiry_time_for_overnight_expiry_with_three_hour_expiry($timestamp) {
 196          // Setup configuration.
 197          $method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
 198          $method->setAccessible(true);
 199          set_config('expireovernight', 1, 'factor_token');
 200          set_config('expiry', 3 * HOURSECS, 'factor_token');
 201  
 202          // All the results here, should be for 2am the following morning from the timestamp provided.
 203          $expiry = $method->invoke($this->factor, $timestamp);
 204          list($expiresat, $secondstillexpiry) = $expiry;
 205  
 206          // Calculate the expected raw expiry if not considering 'overnight'.
 207          $timezone = \core_date::get_user_timezone_object();
 208          $datetime = new \DateTime();
 209          $datetime->setTimezone($timezone);
 210  
 211          $rawexpiry = $timestamp + (3 * HOURSECS);
 212          $datetime->setTimestamp($rawexpiry);
 213  
 214          // Sanity check, that the $secondstillexpiry is in the appropriate ranges.
 215          $this->assertGreaterThan(0, $secondstillexpiry);
 216          $this->assertLessThan((3 * HOURSECS) + 1, $secondstillexpiry);
 217  
 218          // If the raw timestamp of the expiry, is less than tomorrow at 2am,
 219          // then use the raw expiry time.
 220          $followingdayattwoam = strtotime('tomorrow 0200', $timestamp);
 221          if ($datetime->getTimestamp() < $followingdayattwoam) {
 222              $this->assertEquals($datetime->getTimestamp(), $expiresat);
 223              // Ensure the $secondstillexpiry is calculated correctly.
 224              $this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
 225          } else {
 226              // Otherwsie it should be pushed back to 2am.
 227              $this->assertEquals($followingdayattwoam, $expiresat);
 228              // Ensure the $secondstillexpiry is calculated correctly.
 229              $this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
 230          }
 231      }
 232  
 233      /**
 234       * Only relevant based on the hour padding used, which is currently set to 2 hours (2am).
 235       *
 236       * @covers ::calculate_expiry_time
 237       * @param int $timestamp
 238       * @dataProvider timestamp_provider
 239       */
 240      public function test_calculate_expiry_time_for_overnight_expiry_with_an_hour_expiry($timestamp) {
 241          // Setup configuration.
 242          $method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
 243          $method->setAccessible(true);
 244          set_config('expireovernight', 1, 'factor_token');
 245          set_config('expiry', HOURSECS, 'factor_token');
 246  
 247          // All the results here, should be for 2am the following morning from the timestamp provided.
 248          $expiry = $method->invoke($this->factor, $timestamp);
 249          list($expiresat, $secondstillexpiry) = $expiry;
 250  
 251          // Calculate the expected raw expiry if not considering 'overnight'.
 252          $timezone = \core_date::get_user_timezone_object();
 253          $datetime = new \DateTime();
 254          $datetime->setTimezone($timezone);
 255  
 256          $rawexpiry = $timestamp + HOURSECS;
 257          $datetime->setTimestamp($rawexpiry);
 258  
 259          // Sanity check, that the $secondstillexpiry is in the appropriate ranges.
 260          $this->assertGreaterThan(0, $secondstillexpiry);
 261          $this->assertLessThan(HOURSECS + 1, $secondstillexpiry);
 262  
 263          // If the raw timestamp of the expiry, is less than tomorrow at 2am,
 264          // then use the raw expiry time.
 265          $followingdayattwoam = strtotime('tomorrow 0200', $timestamp);
 266          if ($datetime->getTimestamp() < $followingdayattwoam) {
 267              $this->assertEquals($datetime->getTimestamp(), $expiresat);
 268              // Ensure the $secondstillexpiry is calculated correctly.
 269              $this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
 270          } else {
 271              // Otherwsie it should be pushed back to 2am.
 272              $this->assertEquals($followingdayattwoam, $expiresat);
 273              // Ensure the $secondstillexpiry is calculated correctly.
 274              $this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
 275          }
 276      }
 277  
 278      /**
 279       * Timestamps for a 24 hour period starting from a fixed time.
 280       * Increments by 30 minutes to cover half hour and hour cases.
 281       * Starting timestamp: 2022-01-15 07:30:00 Australia/Melbourne time.
 282       */
 283      public function timestamp_provider() {
 284          $starttimestamp = 1642192200;
 285          foreach (range(0, 23) as $i) {
 286              $timestamps[] = [$starttimestamp + ($i * HOURSECS)];
 287              $timestamps[] = [$starttimestamp + ($i * HOURSECS) + (30 * MINSECS)];
 288          }
 289          return $timestamps;
 290      }
 291  }