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