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