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