Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   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   * Redis session tests.
  19   *
  20   * NOTE: in order to execute this test you need to set up
  21   *       Redis server and add configuration a constant
  22   *       to config.php or phpunit.xml configuration file:
  23   *
  24   * define('TEST_SESSION_REDIS_HOST', '127.0.0.1');
  25   *
  26   * @package   core
  27   * @author    Russell Smith <mr-russ@smith2001.net>
  28   * @copyright 2016 Russell Smith
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  
  32  defined('MOODLE_INTERNAL') || die();
  33  
  34  /**
  35   * Unit tests for classes/session/redis.php.
  36   *
  37   * @package   core
  38   * @author    Russell Smith <mr-russ@smith2001.net>
  39   * @copyright 2016 Russell Smith
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   * @runClassInSeparateProcess
  42   */
  43  class core_session_redis_testcase extends advanced_testcase {
  44  
  45      /** @var $keyprefix This key prefix used when testing Redis */
  46      protected $keyprefix = null;
  47      /** @var $redis The current testing redis connection */
  48      protected $redis = null;
  49  
  50      public function setUp(): void {
  51          global $CFG;
  52  
  53          if (!extension_loaded('redis')) {
  54              $this->markTestSkipped('Redis extension not loaded.');
  55          }
  56          if (!defined('TEST_SESSION_REDIS_HOST')) {
  57              $this->markTestSkipped('Session test server not set. define: TEST_SESSION_REDIS_HOST');
  58          }
  59          $version = phpversion('Redis');
  60          if (!$version) {
  61              $this->markTestSkipped('Redis extension version missing');
  62          } else if (version_compare($version, '2.0') <= 0) {
  63              $this->markTestSkipped('Redis extension version must be at least 2.0: now running "' . $version . '"');
  64          }
  65  
  66          $this->resetAfterTest();
  67  
  68          $this->keyprefix = 'phpunit'.rand(1, 100000);
  69  
  70          $CFG->session_redis_host = TEST_SESSION_REDIS_HOST;
  71          $CFG->session_redis_prefix = $this->keyprefix;
  72  
  73          // Set a very short lock timeout to ensure tests run quickly.  We are running single threaded,
  74          // so unless we lock and expect it to be there, we will always see a lock.
  75          $CFG->session_redis_acquire_lock_timeout = 1;
  76          $CFG->session_redis_lock_expire = 70;
  77  
  78          $this->redis = new Redis();
  79          $this->redis->connect(TEST_SESSION_REDIS_HOST);
  80      }
  81  
  82      public function tearDown(): void {
  83          if (!extension_loaded('redis') || !defined('TEST_SESSION_REDIS_HOST')) {
  84              return;
  85          }
  86  
  87          $list = $this->redis->keys($this->keyprefix.'*');
  88          foreach ($list as $keyname) {
  89              $this->redis->del($keyname);
  90          }
  91          $this->redis->close();
  92      }
  93  
  94      public function test_normal_session_read_only() {
  95          $sess = new \core\session\redis();
  96          $sess->set_requires_write_lock(false);
  97          $sess->init();
  98          $this->assertSame('', $sess->handler_read('sess1'));
  99          $this->assertTrue($sess->handler_close());
 100      }
 101  
 102      public function test_normal_session_start_stop_works() {
 103          $sess = new \core\session\redis();
 104          $sess->init();
 105          $sess->set_requires_write_lock(true);
 106          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 107          $this->assertSame('', $sess->handler_read('sess1'));
 108          $this->assertTrue($sess->handler_write('sess1', 'DATA'));
 109          $this->assertTrue($sess->handler_close());
 110  
 111          // Read the session again to ensure locking did what it should.
 112          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 113          $this->assertSame('DATA', $sess->handler_read('sess1'));
 114          $this->assertTrue($sess->handler_write('sess1', 'DATA-new'));
 115          $this->assertTrue($sess->handler_close());
 116          $this->assertSessionNoLocks();
 117      }
 118  
 119      public function test_session_blocks_with_existing_session() {
 120          $sess = new \core\session\redis();
 121          $sess->init();
 122          $sess->set_requires_write_lock(true);
 123          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 124          $this->assertSame('', $sess->handler_read('sess1'));
 125          $this->assertTrue($sess->handler_write('sess1', 'DATA'));
 126          $this->assertTrue($sess->handler_close());
 127  
 128          // Sessions are not locked until they have been saved once.
 129          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 130          $this->assertSame('DATA', $sess->handler_read('sess1'));
 131  
 132          $sessblocked = new \core\session\redis();
 133          $sessblocked->init();
 134          $sessblocked->set_requires_write_lock(true);
 135          $this->assertTrue($sessblocked->handler_open('Not used', 'Not used'));
 136  
 137          // Trap the error log and send it to stdOut so we can expect output at the right times.
 138          $errorlog = tempnam(sys_get_temp_dir(), "rediserrorlog");
 139          $this->iniSet('error_log', $errorlog);
 140          try {
 141              $sessblocked->handler_read('sess1');
 142              $this->fail('Session lock must fail to be obtained.');
 143          } catch (\core\session\exception $e) {
 144              $this->assertStringContainsString("Unable to obtain session lock", $e->getMessage());
 145              $this->assertStringContainsString('Cannot obtain session lock for sid: sess1', file_get_contents($errorlog));
 146          }
 147  
 148          $this->assertTrue($sessblocked->handler_close());
 149          $this->assertTrue($sess->handler_write('sess1', 'DATA-new'));
 150          $this->assertTrue($sess->handler_close());
 151          $this->assertSessionNoLocks();
 152      }
 153  
 154      public function test_session_is_destroyed_when_it_does_not_exist() {
 155          $sess = new \core\session\redis();
 156          $sess->init();
 157          $sess->set_requires_write_lock(true);
 158          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 159          $this->assertTrue($sess->handler_destroy('sess-destroy'));
 160          $this->assertSessionNoLocks();
 161      }
 162  
 163      public function test_session_is_destroyed_when_we_have_it_open() {
 164          $sess = new \core\session\redis();
 165          $sess->init();
 166          $sess->set_requires_write_lock(true);
 167          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 168          $this->assertSame('', $sess->handler_read('sess-destroy'));
 169          $this->assertTrue($sess->handler_destroy('sess-destroy'));
 170          $this->assertTrue($sess->handler_close());
 171          $this->assertSessionNoLocks();
 172      }
 173  
 174      public function test_multiple_sessions_do_not_interfere_with_each_other() {
 175          $sess1 = new \core\session\redis();
 176          $sess1->set_requires_write_lock(true);
 177          $sess1->init();
 178          $sess2 = new \core\session\redis();
 179          $sess2->set_requires_write_lock(true);
 180          $sess2->init();
 181  
 182          // Initialize session 1.
 183          $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
 184          $this->assertSame('', $sess1->handler_read('sess1'));
 185          $this->assertTrue($sess1->handler_write('sess1', 'DATA'));
 186          $this->assertTrue($sess1->handler_close());
 187  
 188          // Initialize session 2.
 189          $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
 190          $this->assertSame('', $sess2->handler_read('sess2'));
 191          $this->assertTrue($sess2->handler_write('sess2', 'DATA2'));
 192          $this->assertTrue($sess2->handler_close());
 193  
 194          // Open and read session 1 and 2.
 195          $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
 196          $this->assertSame('DATA', $sess1->handler_read('sess1'));
 197          $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
 198          $this->assertSame('DATA2', $sess2->handler_read('sess2'));
 199  
 200          // Write both sessions.
 201          $this->assertTrue($sess1->handler_write('sess1', 'DATAX'));
 202          $this->assertTrue($sess2->handler_write('sess2', 'DATA2X'));
 203  
 204          // Read both sessions.
 205          $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
 206          $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
 207          $this->assertEquals('DATAX', $sess1->handler_read('sess1'));
 208          $this->assertEquals('DATA2X', $sess2->handler_read('sess2'));
 209  
 210          // Close both sessions
 211          $this->assertTrue($sess1->handler_close());
 212          $this->assertTrue($sess2->handler_close());
 213  
 214          // Read the session again to ensure locking did what it should.
 215          $this->assertSessionNoLocks();
 216      }
 217  
 218      public function test_multiple_sessions_work_with_a_single_instance() {
 219          $sess = new \core\session\redis();
 220          $sess->init();
 221          $sess->set_requires_write_lock(true);
 222  
 223          // Initialize session 1.
 224          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 225          $this->assertSame('', $sess->handler_read('sess1'));
 226          $this->assertTrue($sess->handler_write('sess1', 'DATA'));
 227          $this->assertSame('', $sess->handler_read('sess2'));
 228          $this->assertTrue($sess->handler_write('sess2', 'DATA2'));
 229          $this->assertSame('DATA', $sess->handler_read('sess1'));
 230          $this->assertSame('DATA2', $sess->handler_read('sess2'));
 231          $this->assertTrue($sess->handler_destroy('sess2'));
 232  
 233          $this->assertTrue($sess->handler_close());
 234          $this->assertSessionNoLocks();
 235  
 236          $this->assertTrue($sess->handler_close());
 237      }
 238  
 239      public function test_session_exists_returns_valid_values() {
 240          $sess = new \core\session\redis();
 241          $sess->init();
 242          $sess->set_requires_write_lock(true);
 243  
 244          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 245          $this->assertSame('', $sess->handler_read('sess1'));
 246  
 247          $this->assertFalse($sess->session_exists('sess1'), 'Session must not exist yet, it has not been saved');
 248          $this->assertTrue($sess->handler_write('sess1', 'DATA'));
 249          $this->assertTrue($sess->session_exists('sess1'), 'Session must exist now.');
 250          $this->assertTrue($sess->handler_destroy('sess1'));
 251          $this->assertFalse($sess->session_exists('sess1'), 'Session should be destroyed.');
 252      }
 253  
 254      public function test_kill_sessions_removes_the_session_from_redis() {
 255          global $DB;
 256  
 257          $sess = new \core\session\redis();
 258          $sess->init();
 259  
 260          $this->assertTrue($sess->handler_open('Not used', 'Not used'));
 261          $this->assertTrue($sess->handler_write('sess1', 'DATA'));
 262          $this->assertTrue($sess->handler_write('sess2', 'DATA'));
 263          $this->assertTrue($sess->handler_write('sess3', 'DATA'));
 264  
 265          $sessiondata = new \stdClass();
 266          $sessiondata->userid = 2;
 267          $sessiondata->timecreated = time();
 268          $sessiondata->timemodified = time();
 269  
 270          $sessiondata->sid = 'sess1';
 271          $DB->insert_record('sessions', $sessiondata);
 272          $sessiondata->sid = 'sess2';
 273          $DB->insert_record('sessions', $sessiondata);
 274          $sessiondata->sid = 'sess3';
 275          $DB->insert_record('sessions', $sessiondata);
 276  
 277          $this->assertNotEquals('', $sess->handler_read('sess1'));
 278          $sess->kill_session('sess1');
 279          $this->assertEquals('', $sess->handler_read('sess1'));
 280  
 281          $this->assertEmpty($this->redis->keys($this->keyprefix.'sess1.lock'));
 282  
 283          $sess->kill_all_sessions();
 284  
 285          $this->assertEquals(3, $DB->count_records('sessions'), 'Moodle handles session database, plugin must not change it.');
 286          $this->assertSessionNoLocks();
 287          $this->assertEmpty($this->redis->keys($this->keyprefix.'*'), 'There should be no session data left.');
 288      }
 289  
 290      public function test_exception_when_connection_attempts_exceeded() {
 291          global $CFG;
 292  
 293          $CFG->session_redis_port = 111111;
 294          $actual = '';
 295  
 296          $sess = new \core\session\redis();
 297          try {
 298              $sess->init();
 299          } catch (RedisException $e) {
 300              $actual = $e->getMessage();
 301          }
 302  
 303          $expected = 'Failed to connect (try 5 out of 5) to redis at ' . TEST_SESSION_REDIS_HOST . ':111111';
 304          $this->assertDebuggingCalledCount(5);
 305          $this->assertStringContainsString($expected, $actual);
 306      }
 307  
 308      /**
 309       * Assert that we don't have any session locks in Redis.
 310       */
 311      protected function assertSessionNoLocks() {
 312          $this->assertEmpty($this->redis->keys($this->keyprefix.'*.lock'));
 313      }
 314  }