Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 * @covers \core\session\redis 33 * @author Russell Smith <mr-russ@smith2001.net> 34 * @copyright 2016 Russell Smith 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 * @runClassInSeparateProcess 37 */ 38 class session_redis_test extends \advanced_testcase { 39 40 /** @var $keyprefix This key prefix used when testing Redis */ 41 protected $keyprefix = null; 42 /** @var $redis The current testing redis connection */ 43 protected $redis = null; 44 /** @var bool $encrypted Is the current testing redis connection encrypted*/ 45 protected $encrypted = false; 46 /** @var int $acquiretimeout how long we wait for session lock in seconds when testing Redis */ 47 protected $acquiretimeout = 1; 48 /** @var int $lockexpire how long to wait in seconds before expiring the lock when testing Redis */ 49 protected $lockexpire = 70; 50 51 52 public function setUp(): void { 53 global $CFG; 54 55 if (!extension_loaded('redis')) { 56 $this->markTestSkipped('Redis extension not loaded.'); 57 } 58 if (!defined('TEST_SESSION_REDIS_HOST')) { 59 $this->markTestSkipped('Session test server not set. define: TEST_SESSION_REDIS_HOST'); 60 } 61 $version = phpversion('Redis'); 62 if (!$version) { 63 $this->markTestSkipped('Redis extension version missing'); 64 } else if (version_compare($version, '2.0') <= 0) { 65 $this->markTestSkipped('Redis extension version must be at least 2.0: now running "' . $version . '"'); 66 } 67 68 $this->resetAfterTest(); 69 70 $this->keyprefix = 'phpunit'.rand(1, 100000); 71 72 $CFG->session_redis_host = TEST_SESSION_REDIS_HOST; 73 if (strpos(TEST_SESSION_REDIS_HOST, ':')) { 74 list($server, $port) = explode(':', TEST_SESSION_REDIS_HOST); 75 } else { 76 $server = TEST_SESSION_REDIS_HOST; 77 $port = 6379; 78 } 79 80 $opts = []; 81 if (defined('TEST_SESSION_REDIS_ENCRYPT') && TEST_SESSION_REDIS_ENCRYPT) { 82 $this->encrypted = true; 83 $sslopts = $CFG->session_redis_encrypt = ['verify_peer' => false, 'verify_peer_name' => false]; 84 $opts['stream'] = $sslopts; 85 } 86 $CFG->session_redis_prefix = $this->keyprefix; 87 88 // Set a very short lock timeout to ensure tests run quickly. We are running single threaded, 89 // so unless we lock and expect it to be there, we will always see a lock. 90 $CFG->session_redis_acquire_lock_timeout = $this->acquiretimeout; 91 $CFG->session_redis_lock_expire = $this->lockexpire; 92 93 $this->redis = new Redis(); 94 $this->redis->connect($server, $port, 1, null, 1, 0, $opts); 95 if (!$this->redis->ping()) { 96 $this->markTestSkipped("Redis ping failed"); 97 } 98 } 99 100 public function tearDown(): void { 101 if (!extension_loaded('redis') || !defined('TEST_SESSION_REDIS_HOST')) { 102 return; 103 } 104 105 $list = $this->redis->keys($this->keyprefix.'*'); 106 foreach ($list as $keyname) { 107 $this->redis->del($keyname); 108 } 109 $this->redis->close(); 110 } 111 112 public function test_normal_session_read_only() { 113 $sess = new \core\session\redis(); 114 $sess->set_requires_write_lock(false); 115 $sess->init(); 116 $this->assertSame('', $sess->handler_read('sess1')); 117 $this->assertTrue($sess->handler_close()); 118 } 119 120 public function test_normal_session_start_stop_works() { 121 $sess = new \core\session\redis(); 122 $sess->init(); 123 $sess->set_requires_write_lock(true); 124 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 125 $this->assertSame('', $sess->handler_read('sess1')); 126 $this->assertTrue($sess->handler_write('sess1', 'DATA')); 127 $this->assertTrue($sess->handler_close()); 128 129 // Read the session again to ensure locking did what it should. 130 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 131 $this->assertSame('DATA', $sess->handler_read('sess1')); 132 $this->assertTrue($sess->handler_write('sess1', 'DATA-new')); 133 $this->assertTrue($sess->handler_close()); 134 $this->assertSessionNoLocks(); 135 } 136 137 public function test_compression_read_and_write_works() { 138 global $CFG; 139 140 $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_GZIP; 141 142 $sess = new \core\session\redis(); 143 $sess->init(); 144 $this->assertTrue($sess->handler_write('sess1', 'DATA')); 145 $this->assertSame('DATA', $sess->handler_read('sess1')); 146 $this->assertTrue($sess->handler_close()); 147 148 if (extension_loaded('zstd')) { 149 $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_ZSTD; 150 151 $sess = new \core\session\redis(); 152 $sess->init(); 153 $this->assertTrue($sess->handler_write('sess2', 'DATA')); 154 $this->assertSame('DATA', $sess->handler_read('sess2')); 155 $this->assertTrue($sess->handler_close()); 156 } 157 158 $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_NONE; 159 } 160 161 public function test_session_blocks_with_existing_session() { 162 $sess = new \core\session\redis(); 163 $sess->init(); 164 $sess->set_requires_write_lock(true); 165 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 166 $this->assertSame('', $sess->handler_read('sess1')); 167 $this->assertTrue($sess->handler_write('sess1', 'DATA')); 168 $this->assertTrue($sess->handler_close()); 169 170 // Sessions are not locked until they have been saved once. 171 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 172 $this->assertSame('DATA', $sess->handler_read('sess1')); 173 174 $sessblocked = new \core\session\redis(); 175 $sessblocked->init(); 176 $sessblocked->set_requires_write_lock(true); 177 $this->assertTrue($sessblocked->handler_open('Not used', 'Not used')); 178 179 // Trap the error log and send it to stdOut so we can expect output at the right times. 180 $errorlog = tempnam(sys_get_temp_dir(), "rediserrorlog"); 181 $this->iniSet('error_log', $errorlog); 182 try { 183 $sessblocked->handler_read('sess1'); 184 $this->fail('Session lock must fail to be obtained.'); 185 } catch (\core\session\exception $e) { 186 $this->assertStringContainsString("Unable to obtain lock for session id sess1", $e->getMessage()); 187 $this->assertStringContainsString('within 1 sec.', $e->getMessage()); 188 $this->assertStringContainsString('session lock timeout (1 min 10 secs) ', $e->getMessage()); 189 $this->assertStringContainsString('Cannot obtain session lock for sid: sess1', file_get_contents($errorlog)); 190 } 191 192 $this->assertTrue($sessblocked->handler_close()); 193 $this->assertTrue($sess->handler_write('sess1', 'DATA-new')); 194 $this->assertTrue($sess->handler_close()); 195 $this->assertSessionNoLocks(); 196 } 197 198 public function test_session_is_destroyed_when_it_does_not_exist() { 199 $sess = new \core\session\redis(); 200 $sess->init(); 201 $sess->set_requires_write_lock(true); 202 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 203 $this->assertTrue($sess->handler_destroy('sess-destroy')); 204 $this->assertSessionNoLocks(); 205 } 206 207 public function test_session_is_destroyed_when_we_have_it_open() { 208 $sess = new \core\session\redis(); 209 $sess->init(); 210 $sess->set_requires_write_lock(true); 211 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 212 $this->assertSame('', $sess->handler_read('sess-destroy')); 213 $this->assertTrue($sess->handler_destroy('sess-destroy')); 214 $this->assertTrue($sess->handler_close()); 215 $this->assertSessionNoLocks(); 216 } 217 218 public function test_multiple_sessions_do_not_interfere_with_each_other() { 219 $sess1 = new \core\session\redis(); 220 $sess1->set_requires_write_lock(true); 221 $sess1->init(); 222 $sess2 = new \core\session\redis(); 223 $sess2->set_requires_write_lock(true); 224 $sess2->init(); 225 226 // Initialize session 1. 227 $this->assertTrue($sess1->handler_open('Not used', 'Not used')); 228 $this->assertSame('', $sess1->handler_read('sess1')); 229 $this->assertTrue($sess1->handler_write('sess1', 'DATA')); 230 $this->assertTrue($sess1->handler_close()); 231 232 // Initialize session 2. 233 $this->assertTrue($sess2->handler_open('Not used', 'Not used')); 234 $this->assertSame('', $sess2->handler_read('sess2')); 235 $this->assertTrue($sess2->handler_write('sess2', 'DATA2')); 236 $this->assertTrue($sess2->handler_close()); 237 238 // Open and read session 1 and 2. 239 $this->assertTrue($sess1->handler_open('Not used', 'Not used')); 240 $this->assertSame('DATA', $sess1->handler_read('sess1')); 241 $this->assertTrue($sess2->handler_open('Not used', 'Not used')); 242 $this->assertSame('DATA2', $sess2->handler_read('sess2')); 243 244 // Write both sessions. 245 $this->assertTrue($sess1->handler_write('sess1', 'DATAX')); 246 $this->assertTrue($sess2->handler_write('sess2', 'DATA2X')); 247 248 // Read both sessions. 249 $this->assertTrue($sess1->handler_open('Not used', 'Not used')); 250 $this->assertTrue($sess2->handler_open('Not used', 'Not used')); 251 $this->assertEquals('DATAX', $sess1->handler_read('sess1')); 252 $this->assertEquals('DATA2X', $sess2->handler_read('sess2')); 253 254 // Close both sessions 255 $this->assertTrue($sess1->handler_close()); 256 $this->assertTrue($sess2->handler_close()); 257 258 // Read the session again to ensure locking did what it should. 259 $this->assertSessionNoLocks(); 260 } 261 262 public function test_multiple_sessions_work_with_a_single_instance() { 263 $sess = new \core\session\redis(); 264 $sess->init(); 265 $sess->set_requires_write_lock(true); 266 267 // Initialize session 1. 268 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 269 $this->assertSame('', $sess->handler_read('sess1')); 270 $this->assertTrue($sess->handler_write('sess1', 'DATA')); 271 $this->assertSame('', $sess->handler_read('sess2')); 272 $this->assertTrue($sess->handler_write('sess2', 'DATA2')); 273 $this->assertSame('DATA', $sess->handler_read('sess1')); 274 $this->assertSame('DATA2', $sess->handler_read('sess2')); 275 $this->assertTrue($sess->handler_destroy('sess2')); 276 277 $this->assertTrue($sess->handler_close()); 278 $this->assertSessionNoLocks(); 279 280 $this->assertTrue($sess->handler_close()); 281 } 282 283 public function test_session_exists_returns_valid_values() { 284 $sess = new \core\session\redis(); 285 $sess->init(); 286 $sess->set_requires_write_lock(true); 287 288 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 289 $this->assertSame('', $sess->handler_read('sess1')); 290 291 $this->assertFalse($sess->session_exists('sess1'), 'Session must not exist yet, it has not been saved'); 292 $this->assertTrue($sess->handler_write('sess1', 'DATA')); 293 $this->assertTrue($sess->session_exists('sess1'), 'Session must exist now.'); 294 $this->assertTrue($sess->handler_destroy('sess1')); 295 $this->assertFalse($sess->session_exists('sess1'), 'Session should be destroyed.'); 296 } 297 298 public function test_kill_sessions_removes_the_session_from_redis() { 299 global $DB; 300 301 $sess = new \core\session\redis(); 302 $sess->init(); 303 304 $this->assertTrue($sess->handler_open('Not used', 'Not used')); 305 $this->assertTrue($sess->handler_write('sess1', 'DATA')); 306 $this->assertTrue($sess->handler_write('sess2', 'DATA')); 307 $this->assertTrue($sess->handler_write('sess3', 'DATA')); 308 309 $sessiondata = new \stdClass(); 310 $sessiondata->userid = 2; 311 $sessiondata->timecreated = time(); 312 $sessiondata->timemodified = time(); 313 314 $sessiondata->sid = 'sess1'; 315 $DB->insert_record('sessions', $sessiondata); 316 $sessiondata->sid = 'sess2'; 317 $DB->insert_record('sessions', $sessiondata); 318 $sessiondata->sid = 'sess3'; 319 $DB->insert_record('sessions', $sessiondata); 320 321 $this->assertNotEquals('', $sess->handler_read('sess1')); 322 $sess->kill_session('sess1'); 323 $this->assertEquals('', $sess->handler_read('sess1')); 324 325 $this->assertEmpty($this->redis->keys($this->keyprefix.'sess1.lock')); 326 327 $sess->kill_all_sessions(); 328 329 $this->assertEquals(3, $DB->count_records('sessions'), 'Moodle handles session database, plugin must not change it.'); 330 $this->assertSessionNoLocks(); 331 $this->assertEmpty($this->redis->keys($this->keyprefix.'*'), 'There should be no session data left.'); 332 } 333 334 public function test_exception_when_connection_attempts_exceeded() { 335 global $CFG; 336 337 $CFG->session_redis_port = 111111; 338 $actual = ''; 339 340 $sess = new \core\session\redis(); 341 try { 342 $sess->init(); 343 } catch (RedisException $e) { 344 $actual = $e->getMessage(); 345 } 346 347 $host = TEST_SESSION_REDIS_HOST; 348 if ($this->encrypted) { 349 $host = "tls://$host"; 350 } 351 $expected = "Failed to connect (try 5 out of 5) to redis at $host:111111"; 352 $this->assertDebuggingCalledCount(5); 353 $this->assertStringContainsString($expected, $actual); 354 } 355 356 /** 357 * Assert that we don't have any session locks in Redis. 358 */ 359 protected function assertSessionNoLocks() { 360 $this->assertEmpty($this->redis->keys($this->keyprefix.'*.lock')); 361 } 362 363 public function test_session_redis_encrypt() { 364 global $CFG; 365 366 $CFG->session_redis_encrypt = ['verify_peer' => false, 'verify_peer_name' => false]; 367 368 $sess = new \core\session\redis(); 369 370 $prop = new \ReflectionProperty(\core\session\redis::class, 'host'); 371 $prop->setAccessible(true); 372 $this->assertEquals('tls://' . TEST_SESSION_REDIS_HOST, $prop->getValue($sess)); 373 } 374 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body