2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Redis session tests.
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:
24 * define('TEST_SESSION_REDIS_HOST', '127.0.0.1');
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
32 defined('MOODLE_INTERNAL') ||
die();
35 * Unit tests for classes/session/redis.php.
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
43 class core_session_redis_testcase
extends advanced_testcase
{
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;
50 public function setUp(): void
{
53 if (!extension_loaded('redis')) {
54 $this->markTestSkipped('Redis extension not loaded.');
56 if (!defined('TEST_SESSION_REDIS_HOST')) {
57 $this->markTestSkipped('Session test server not set. define: TEST_SESSION_REDIS_HOST');
59 $version = phpversion('Redis');
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 . '"');
66 $this->resetAfterTest();
68 $this->keyprefix
= 'phpunit'.rand(1, 100000);
70 $CFG->session_redis_host
= TEST_SESSION_REDIS_HOST
;
71 $CFG->session_redis_prefix
= $this->keyprefix
;
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;
78 $this->redis
= new Redis();
79 $this->redis
->connect(TEST_SESSION_REDIS_HOST
);
82 public function tearDown(): void
{
83 if (!extension_loaded('redis') ||
!defined('TEST_SESSION_REDIS_HOST')) {
87 $list = $this->redis
->keys($this->keyprefix
.'*');
88 foreach ($list as $keyname) {
89 $this->redis
->del($keyname);
91 $this->redis
->close();
94 public function test_normal_session_read_only() {
95 $sess = new \core\session\redis
();
96 $sess->set_requires_write_lock(false);
98 $this->assertSame('', $sess->handler_read('sess1'));
99 $this->assertTrue($sess->handler_close());
102 public function test_normal_session_start_stop_works() {
103 $sess = new \core\session\redis
();
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());
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();
119 public function test_compression_read_and_write_works() {
122 $CFG->session_redis_compressor
= \core\session\redis
::COMPRESSION_GZIP
;
124 $sess = new \core\session\redis
();
126 $this->assertTrue($sess->handler_write('sess1', 'DATA'));
127 $this->assertSame('DATA', $sess->handler_read('sess1'));
128 $this->assertTrue($sess->handler_close());
130 if (extension_loaded('zstd')) {
131 $CFG->session_redis_compressor
= \core\session\redis
::COMPRESSION_ZSTD
;
133 $sess = new \core\session\redis
();
135 $this->assertTrue($sess->handler_write('sess2', 'DATA'));
136 $this->assertSame('DATA', $sess->handler_read('sess2'));
137 $this->assertTrue($sess->handler_close());
140 $CFG->session_redis_compressor
= \core\session\redis
::COMPRESSION_NONE
;
143 public function test_session_blocks_with_existing_session() {
144 $sess = new \core\session\redis
();
146 $sess->set_requires_write_lock(true);
147 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
148 $this->assertSame('', $sess->handler_read('sess1'));
149 $this->assertTrue($sess->handler_write('sess1', 'DATA'));
150 $this->assertTrue($sess->handler_close());
152 // Sessions are not locked until they have been saved once.
153 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
154 $this->assertSame('DATA', $sess->handler_read('sess1'));
156 $sessblocked = new \core\session\redis
();
157 $sessblocked->init();
158 $sessblocked->set_requires_write_lock(true);
159 $this->assertTrue($sessblocked->handler_open('Not used', 'Not used'));
161 // Trap the error log and send it to stdOut so we can expect output at the right times.
162 $errorlog = tempnam(sys_get_temp_dir(), "rediserrorlog");
163 $this->iniSet('error_log', $errorlog);
165 $sessblocked->handler_read('sess1');
166 $this->fail('Session lock must fail to be obtained.');
167 } catch (\core\session\exception
$e) {
168 $this->assertStringContainsString("Unable to obtain session lock", $e->getMessage());
169 $this->assertStringContainsString('Cannot obtain session lock for sid: sess1', file_get_contents($errorlog));
172 $this->assertTrue($sessblocked->handler_close());
173 $this->assertTrue($sess->handler_write('sess1', 'DATA-new'));
174 $this->assertTrue($sess->handler_close());
175 $this->assertSessionNoLocks();
178 public function test_session_is_destroyed_when_it_does_not_exist() {
179 $sess = new \core\session\redis
();
181 $sess->set_requires_write_lock(true);
182 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
183 $this->assertTrue($sess->handler_destroy('sess-destroy'));
184 $this->assertSessionNoLocks();
187 public function test_session_is_destroyed_when_we_have_it_open() {
188 $sess = new \core\session\redis
();
190 $sess->set_requires_write_lock(true);
191 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
192 $this->assertSame('', $sess->handler_read('sess-destroy'));
193 $this->assertTrue($sess->handler_destroy('sess-destroy'));
194 $this->assertTrue($sess->handler_close());
195 $this->assertSessionNoLocks();
198 public function test_multiple_sessions_do_not_interfere_with_each_other() {
199 $sess1 = new \core\session\redis
();
200 $sess1->set_requires_write_lock(true);
202 $sess2 = new \core\session\redis
();
203 $sess2->set_requires_write_lock(true);
206 // Initialize session 1.
207 $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
208 $this->assertSame('', $sess1->handler_read('sess1'));
209 $this->assertTrue($sess1->handler_write('sess1', 'DATA'));
210 $this->assertTrue($sess1->handler_close());
212 // Initialize session 2.
213 $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
214 $this->assertSame('', $sess2->handler_read('sess2'));
215 $this->assertTrue($sess2->handler_write('sess2', 'DATA2'));
216 $this->assertTrue($sess2->handler_close());
218 // Open and read session 1 and 2.
219 $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
220 $this->assertSame('DATA', $sess1->handler_read('sess1'));
221 $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
222 $this->assertSame('DATA2', $sess2->handler_read('sess2'));
224 // Write both sessions.
225 $this->assertTrue($sess1->handler_write('sess1', 'DATAX'));
226 $this->assertTrue($sess2->handler_write('sess2', 'DATA2X'));
228 // Read both sessions.
229 $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
230 $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
231 $this->assertEquals('DATAX', $sess1->handler_read('sess1'));
232 $this->assertEquals('DATA2X', $sess2->handler_read('sess2'));
234 // Close both sessions
235 $this->assertTrue($sess1->handler_close());
236 $this->assertTrue($sess2->handler_close());
238 // Read the session again to ensure locking did what it should.
239 $this->assertSessionNoLocks();
242 public function test_multiple_sessions_work_with_a_single_instance() {
243 $sess = new \core\session\redis
();
245 $sess->set_requires_write_lock(true);
247 // Initialize session 1.
248 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
249 $this->assertSame('', $sess->handler_read('sess1'));
250 $this->assertTrue($sess->handler_write('sess1', 'DATA'));
251 $this->assertSame('', $sess->handler_read('sess2'));
252 $this->assertTrue($sess->handler_write('sess2', 'DATA2'));
253 $this->assertSame('DATA', $sess->handler_read('sess1'));
254 $this->assertSame('DATA2', $sess->handler_read('sess2'));
255 $this->assertTrue($sess->handler_destroy('sess2'));
257 $this->assertTrue($sess->handler_close());
258 $this->assertSessionNoLocks();
260 $this->assertTrue($sess->handler_close());
263 public function test_session_exists_returns_valid_values() {
264 $sess = new \core\session\redis
();
266 $sess->set_requires_write_lock(true);
268 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
269 $this->assertSame('', $sess->handler_read('sess1'));
271 $this->assertFalse($sess->session_exists('sess1'), 'Session must not exist yet, it has not been saved');
272 $this->assertTrue($sess->handler_write('sess1', 'DATA'));
273 $this->assertTrue($sess->session_exists('sess1'), 'Session must exist now.');
274 $this->assertTrue($sess->handler_destroy('sess1'));
275 $this->assertFalse($sess->session_exists('sess1'), 'Session should be destroyed.');
278 public function test_kill_sessions_removes_the_session_from_redis() {
281 $sess = new \core\session\redis
();
284 $this->assertTrue($sess->handler_open('Not used', 'Not used'));
285 $this->assertTrue($sess->handler_write('sess1', 'DATA'));
286 $this->assertTrue($sess->handler_write('sess2', 'DATA'));
287 $this->assertTrue($sess->handler_write('sess3', 'DATA'));
289 $sessiondata = new \
stdClass();
290 $sessiondata->userid
= 2;
291 $sessiondata->timecreated
= time();
292 $sessiondata->timemodified
= time();
294 $sessiondata->sid
= 'sess1';
295 $DB->insert_record('sessions', $sessiondata);
296 $sessiondata->sid
= 'sess2';
297 $DB->insert_record('sessions', $sessiondata);
298 $sessiondata->sid
= 'sess3';
299 $DB->insert_record('sessions', $sessiondata);
301 $this->assertNotEquals('', $sess->handler_read('sess1'));
302 $sess->kill_session('sess1');
303 $this->assertEquals('', $sess->handler_read('sess1'));
305 $this->assertEmpty($this->redis
->keys($this->keyprefix
.'sess1.lock'));
307 $sess->kill_all_sessions();
309 $this->assertEquals(3, $DB->count_records('sessions'), 'Moodle handles session database, plugin must not change it.');
310 $this->assertSessionNoLocks();
311 $this->assertEmpty($this->redis
->keys($this->keyprefix
.'*'), 'There should be no session data left.');
314 public function test_exception_when_connection_attempts_exceeded() {
317 $CFG->session_redis_port
= 111111;
320 $sess = new \core\session\redis
();
323 } catch (RedisException
$e) {
324 $actual = $e->getMessage();
327 $expected = 'Failed to connect (try 5 out of 5) to redis at ' . TEST_SESSION_REDIS_HOST
. ':111111';
328 $this->assertDebuggingCalledCount(5);
329 $this->assertStringContainsString($expected, $actual);
333 * Assert that we don't have any session locks in Redis.
335 protected function assertSessionNoLocks() {
336 $this->assertEmpty($this->redis
->keys($this->keyprefix
.'*.lock'));