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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body