Separate MediaWiki unit and integration tests
[lhc/web/wiklou.git] / tests / phpunit / unit / includes / session / SessionTest.php
1 <?php
2
3 namespace MediaWiki\Session;
4
5 use Psr\Log\LogLevel;
6 use User;
7 use Wikimedia\TestingAccessWrapper;
8
9 /**
10 * @group Session
11 * @covers MediaWiki\Session\Session
12 */
13 class SessionTest extends \MediaWikiUnitTestCase {
14
15 public function testConstructor() {
16 $backend = TestUtils::getDummySessionBackend();
17 TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
18 TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
19
20 $session = new Session( $backend, 42, new \TestLogger );
21 $priv = TestingAccessWrapper::newFromObject( $session );
22 $this->assertSame( $backend, $priv->backend );
23 $this->assertSame( 42, $priv->index );
24
25 $request = new \FauxRequest();
26 $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
27 $this->assertSame( $backend, $priv2->backend );
28 $this->assertNotSame( $priv->index, $priv2->index );
29 $this->assertSame( $request, $priv2->getRequest() );
30 }
31
32 /**
33 * @dataProvider provideMethods
34 * @param string $m Method to test
35 * @param array $args Arguments to pass to the method
36 * @param bool $index Whether the backend method gets passed the index
37 * @param bool $ret Whether the method returns a value
38 */
39 public function testMethods( $m, $args, $index, $ret ) {
40 $mock = $this->getMockBuilder( DummySessionBackend::class )
41 ->setMethods( [ $m, 'deregisterSession' ] )
42 ->getMock();
43 $mock->expects( $this->once() )->method( 'deregisterSession' )
44 ->with( $this->identicalTo( 42 ) );
45
46 $tmp = $mock->expects( $this->once() )->method( $m );
47 $expectArgs = [];
48 if ( $index ) {
49 $expectArgs[] = $this->identicalTo( 42 );
50 }
51 foreach ( $args as $arg ) {
52 $expectArgs[] = $this->identicalTo( $arg );
53 }
54 $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
55
56 $retval = new \stdClass;
57 $tmp->will( $this->returnValue( $retval ) );
58
59 $session = TestUtils::getDummySession( $mock, 42 );
60
61 if ( $ret ) {
62 $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
63 } else {
64 $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
65 }
66
67 // Trigger Session destructor
68 $session = null;
69 }
70
71 public static function provideMethods() {
72 return [
73 [ 'getId', [], false, true ],
74 [ 'getSessionId', [], false, true ],
75 [ 'resetId', [], false, true ],
76 [ 'getProvider', [], false, true ],
77 [ 'isPersistent', [], false, true ],
78 [ 'persist', [], false, false ],
79 [ 'unpersist', [], false, false ],
80 [ 'shouldRememberUser', [], false, true ],
81 [ 'setRememberUser', [ true ], false, false ],
82 [ 'getRequest', [], true, true ],
83 [ 'getUser', [], false, true ],
84 [ 'getAllowedUserRights', [], false, true ],
85 [ 'canSetUser', [], false, true ],
86 [ 'setUser', [ new \stdClass ], false, false ],
87 [ 'suggestLoginUsername', [], true, true ],
88 [ 'shouldForceHTTPS', [], false, true ],
89 [ 'setForceHTTPS', [ true ], false, false ],
90 [ 'getLoggedOutTimestamp', [], false, true ],
91 [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
92 [ 'getProviderMetadata', [], false, true ],
93 [ 'save', [], false, false ],
94 [ 'delaySave', [], false, true ],
95 [ 'renew', [], false, false ],
96 ];
97 }
98
99 public function testDataAccess() {
100 $session = TestUtils::getDummySession();
101 $backend = TestingAccessWrapper::newFromObject( $session )->backend;
102
103 $this->assertEquals( 1, $session->get( 'foo' ) );
104 $this->assertEquals( 'zero', $session->get( 0 ) );
105 $this->assertFalse( $backend->dirty );
106
107 $this->assertEquals( null, $session->get( 'null' ) );
108 $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
109 $this->assertFalse( $backend->dirty );
110
111 $session->set( 'foo', 55 );
112 $this->assertEquals( 55, $backend->data['foo'] );
113 $this->assertTrue( $backend->dirty );
114 $backend->dirty = false;
115
116 $session->set( 1, 'one' );
117 $this->assertEquals( 'one', $backend->data[1] );
118 $this->assertTrue( $backend->dirty );
119 $backend->dirty = false;
120
121 $session->set( 1, 'one' );
122 $this->assertFalse( $backend->dirty );
123
124 $this->assertTrue( $session->exists( 'foo' ) );
125 $this->assertTrue( $session->exists( 1 ) );
126 $this->assertFalse( $session->exists( 'null' ) );
127 $this->assertFalse( $session->exists( 100 ) );
128 $this->assertFalse( $backend->dirty );
129
130 $session->remove( 'foo' );
131 $this->assertArrayNotHasKey( 'foo', $backend->data );
132 $this->assertTrue( $backend->dirty );
133 $backend->dirty = false;
134 $session->remove( 1 );
135 $this->assertArrayNotHasKey( 1, $backend->data );
136 $this->assertTrue( $backend->dirty );
137 $backend->dirty = false;
138
139 $session->remove( 101 );
140 $this->assertFalse( $backend->dirty );
141
142 $backend->data = [ 'a', 'b', '?' => 'c' ];
143 $this->assertSame( 3, $session->count() );
144 $this->assertSame( 3, count( $session ) );
145 $this->assertFalse( $backend->dirty );
146
147 $data = [];
148 foreach ( $session as $key => $value ) {
149 $data[$key] = $value;
150 }
151 $this->assertEquals( $backend->data, $data );
152 $this->assertFalse( $backend->dirty );
153
154 $this->assertEquals( $backend->data, iterator_to_array( $session ) );
155 $this->assertFalse( $backend->dirty );
156 }
157
158 public function testArrayAccess() {
159 $logger = new \TestLogger;
160 $session = TestUtils::getDummySession( null, -1, $logger );
161 $backend = TestingAccessWrapper::newFromObject( $session )->backend;
162
163 $this->assertEquals( 1, $session['foo'] );
164 $this->assertEquals( 'zero', $session[0] );
165 $this->assertFalse( $backend->dirty );
166
167 $logger->setCollect( true );
168 $this->assertEquals( null, $session['null'] );
169 $logger->setCollect( false );
170 $this->assertFalse( $backend->dirty );
171 $this->assertSame( [
172 [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
173 ], $logger->getBuffer() );
174 $logger->clearBuffer();
175
176 $session['foo'] = 55;
177 $this->assertEquals( 55, $backend->data['foo'] );
178 $this->assertTrue( $backend->dirty );
179 $backend->dirty = false;
180
181 $session[1] = 'one';
182 $this->assertEquals( 'one', $backend->data[1] );
183 $this->assertTrue( $backend->dirty );
184 $backend->dirty = false;
185
186 $session[1] = 'one';
187 $this->assertFalse( $backend->dirty );
188
189 $session['bar'] = [ 'baz' => [] ];
190 $session['bar']['baz']['quux'] = 2;
191 $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
192
193 $logger->setCollect( true );
194 $session['bar2']['baz']['quux'] = 3;
195 $logger->setCollect( false );
196 $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
197 $this->assertSame( [
198 [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
199 ], $logger->getBuffer() );
200 $logger->clearBuffer();
201
202 $backend->dirty = false;
203 $this->assertTrue( isset( $session['foo'] ) );
204 $this->assertTrue( isset( $session[1] ) );
205 $this->assertFalse( isset( $session['null'] ) );
206 $this->assertFalse( isset( $session['missing'] ) );
207 $this->assertFalse( isset( $session[100] ) );
208 $this->assertFalse( $backend->dirty );
209
210 unset( $session['foo'] );
211 $this->assertArrayNotHasKey( 'foo', $backend->data );
212 $this->assertTrue( $backend->dirty );
213 $backend->dirty = false;
214 unset( $session[1] );
215 $this->assertArrayNotHasKey( 1, $backend->data );
216 $this->assertTrue( $backend->dirty );
217 $backend->dirty = false;
218
219 unset( $session[101] );
220 $this->assertFalse( $backend->dirty );
221 }
222
223 public function testClear() {
224 $session = TestUtils::getDummySession();
225 $priv = TestingAccessWrapper::newFromObject( $session );
226
227 $backend = $this->getMockBuilder( DummySessionBackend::class )
228 ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
229 ->getMock();
230 $backend->expects( $this->once() )->method( 'canSetUser' )
231 ->will( $this->returnValue( true ) );
232 $backend->expects( $this->once() )->method( 'setUser' )
233 ->with( $this->callback( function ( $user ) {
234 return $user instanceof User && $user->isAnon();
235 } ) );
236 $backend->expects( $this->once() )->method( 'save' );
237 $priv->backend = $backend;
238 $session->clear();
239 $this->assertSame( [], $backend->data );
240 $this->assertTrue( $backend->dirty );
241
242 $backend = $this->getMockBuilder( DummySessionBackend::class )
243 ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
244 ->getMock();
245 $backend->data = [];
246 $backend->expects( $this->once() )->method( 'canSetUser' )
247 ->will( $this->returnValue( true ) );
248 $backend->expects( $this->once() )->method( 'setUser' )
249 ->with( $this->callback( function ( $user ) {
250 return $user instanceof User && $user->isAnon();
251 } ) );
252 $backend->expects( $this->once() )->method( 'save' );
253 $priv->backend = $backend;
254 $session->clear();
255 $this->assertFalse( $backend->dirty );
256
257 $backend = $this->getMockBuilder( DummySessionBackend::class )
258 ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
259 ->getMock();
260 $backend->expects( $this->once() )->method( 'canSetUser' )
261 ->will( $this->returnValue( false ) );
262 $backend->expects( $this->never() )->method( 'setUser' );
263 $backend->expects( $this->once() )->method( 'save' );
264 $priv->backend = $backend;
265 $session->clear();
266 $this->assertSame( [], $backend->data );
267 $this->assertTrue( $backend->dirty );
268 }
269
270 public function testTokens() {
271 $session = TestUtils::getDummySession();
272 $priv = TestingAccessWrapper::newFromObject( $session );
273 $backend = $priv->backend;
274
275 $token = TestingAccessWrapper::newFromObject( $session->getToken() );
276 $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
277 $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
278 $secret = $backend->data['wsTokenSecrets']['default'];
279 $this->assertSame( $secret, $token->secret );
280 $this->assertSame( '', $token->salt );
281 $this->assertTrue( $token->wasNew() );
282
283 $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
284 $this->assertSame( $secret, $token->secret );
285 $this->assertSame( 'foo', $token->salt );
286 $this->assertFalse( $token->wasNew() );
287
288 $backend->data['wsTokenSecrets']['secret'] = 'sekret';
289 $token = TestingAccessWrapper::newFromObject(
290 $session->getToken( [ 'bar', 'baz' ], 'secret' )
291 );
292 $this->assertSame( 'sekret', $token->secret );
293 $this->assertSame( 'bar|baz', $token->salt );
294 $this->assertFalse( $token->wasNew() );
295
296 $session->resetToken( 'secret' );
297 $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
298 $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
299 $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
300
301 $session->resetAllTokens();
302 $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
303 }
304
305 /**
306 * @dataProvider provideSecretsRoundTripping
307 * @param mixed $data
308 */
309 public function testSecretsRoundTripping( $data ) {
310 $session = TestUtils::getDummySession();
311
312 // Simple round-trip
313 $session->setSecret( 'secret', $data );
314 $this->assertNotEquals( $data, $session->get( 'secret' ) );
315 $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
316 }
317
318 public static function provideSecretsRoundTripping() {
319 return [
320 [ 'Foobar' ],
321 [ 42 ],
322 [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
323 [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
324 [ true ],
325 [ false ],
326 [ null ],
327 ];
328 }
329
330 public function testSecrets() {
331 $logger = new \TestLogger;
332 $session = TestUtils::getDummySession( null, -1, $logger );
333
334 // Simple defaulting
335 $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
336
337 // Bad encrypted data
338 $session->set( 'test', 'foobar' );
339 $logger->setCollect( true );
340 $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
341 $logger->setCollect( false );
342 $this->assertSame( [
343 [ LogLevel::WARNING, 'Invalid sealed-secret format' ]
344 ], $logger->getBuffer() );
345 $logger->clearBuffer();
346
347 // Tampered data
348 $session->setSecret( 'test', 'foobar' );
349 $encrypted = $session->get( 'test' );
350 $session->set( 'test', $encrypted . 'x' );
351 $logger->setCollect( true );
352 $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
353 $logger->setCollect( false );
354 $this->assertSame( [
355 [ LogLevel::WARNING, 'Sealed secret has been tampered with, aborting.' ]
356 ], $logger->getBuffer() );
357 $logger->clearBuffer();
358
359 // Unserializable data
360 $iv = random_bytes( 16 );
361 list( $encKey, $hmacKey ) = TestingAccessWrapper::newFromObject( $session )->getSecretKeys();
362 $ciphertext = openssl_encrypt( 'foobar', 'aes-256-ctr', $encKey, OPENSSL_RAW_DATA, $iv );
363 $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
364 $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
365 $encrypted = base64_encode( $hmac ) . '.' . $sealed;
366 $session->set( 'test', $encrypted );
367 \Wikimedia\suppressWarnings();
368 $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
369 \Wikimedia\restoreWarnings();
370 }
371
372 }