Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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