MDL-74216 navigation: Do not show 'My courses' in primary nav to guests
[moodle.git] / lib / tests / session_redis_test.php
blobdab879f696cc32702224b64955372f9b5dac51ff
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.
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/>.
17 /**
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');
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
32 defined('MOODLE_INTERNAL') || die();
34 /**
35 * Unit tests for classes/session/redis.php.
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
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 {
51 global $CFG;
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');
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 . '"');
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')) {
84 return;
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);
97 $sess->init();
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();
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());
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() {
120 global $CFG;
122 $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_GZIP;
124 $sess = new \core\session\redis();
125 $sess->init();
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();
134 $sess->init();
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();
145 $sess->init();
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);
164 try {
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();
180 $sess->init();
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();
189 $sess->init();
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);
201 $sess1->init();
202 $sess2 = new \core\session\redis();
203 $sess2->set_requires_write_lock(true);
204 $sess2->init();
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();
244 $sess->init();
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();
265 $sess->init();
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() {
279 global $DB;
281 $sess = new \core\session\redis();
282 $sess->init();
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() {
315 global $CFG;
317 $CFG->session_redis_port = 111111;
318 $actual = '';
320 $sess = new \core\session\redis();
321 try {
322 $sess->init();
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'));