Call resetServices() when setting globals in tests
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiOptionsTest.php
1 <?php
2
3 /**
4 * @group API
5 * @group Database
6 * @group medium
7 *
8 * @covers ApiOptions
9 */
10 class ApiOptionsTest extends MediaWikiLangTestCase {
11
12 /** @var PHPUnit_Framework_MockObject_MockObject */
13 private $mUserMock;
14 /** @var ApiOptions */
15 private $mTested;
16 private $mSession;
17 /** @var DerivativeContext */
18 private $mContext;
19
20 private static $Success = [ 'options' => 'success' ];
21
22 protected function setUp() {
23 parent::setUp();
24
25 $this->mUserMock = $this->getMockBuilder( User::class )
26 ->disableOriginalConstructor()
27 ->getMock();
28
29 // Set up groups and rights
30 $this->mUserMock->expects( $this->any() )
31 ->method( 'getEffectiveGroups' )->will( $this->returnValue( [ '*', 'user' ] ) );
32
33 // Set up callback for User::getOptionKinds
34 $this->mUserMock->expects( $this->any() )
35 ->method( 'getOptionKinds' )->will( $this->returnCallback( [ $this, 'getOptionKinds' ] ) );
36
37 // No actual DB data
38 $this->mUserMock->expects( $this->any() )
39 ->method( 'getInstanceForUpdate' )->will( $this->returnValue( $this->mUserMock ) );
40
41 // Needs to return something
42 $this->mUserMock->method( 'getOptions' )
43 ->willReturn( [] );
44
45 // DefaultPreferencesFactory calls a ton of user methods, but we still want to list all of
46 // them in case bugs are caused by unexpected things returning null that shouldn't.
47 $this->mUserMock->expects( $this->never() )->method( $this->anythingBut(
48 'getEffectiveGroups', 'getOptionKinds', 'getInstanceForUpdate', 'getOptions', 'getId',
49 'isAnon', 'getRequest', 'isLoggedIn', 'getName', 'getGroupMemberships', 'getEditCount',
50 'getRegistration', 'isAllowed', 'getRealName', 'getOption', 'getStubThreshold',
51 'getBoolOption', 'getEmail', 'getDatePreference', 'useRCPatrol', 'useNPPatrol',
52 'setOption', 'saveSettings', 'resetOptions', 'isRegistered'
53 ) );
54
55 // Create a new context
56 $this->mContext = new DerivativeContext( new RequestContext() );
57 $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) );
58 $this->mContext->setUser( $this->mUserMock );
59
60 $this->overrideUserPermissions( $this->mUserMock, [ 'editmyoptions' ] );
61 $main = new ApiMain( $this->mContext );
62
63 // Empty session
64 $this->mSession = [];
65
66 $this->mTested = new ApiOptions( $main, 'options' );
67
68 $this->mergeMwGlobalArrayValue( 'wgHooks', [
69 'GetPreferences' => [
70 [ $this, 'hookGetPreferences' ]
71 ]
72 ] );
73 $this->mergeMwGlobalArrayValue( 'wgDefaultUserOptions', [
74 'testradio' => 'option1',
75 ] );
76 // Workaround for static caching in User::getDefaultOptions()
77 $this->setContentLang( Language::factory( 'qqq' ) );
78 }
79
80 public function hookGetPreferences( $user, &$preferences ) {
81 $preferences = [];
82
83 foreach ( [ 'name', 'willBeNull', 'willBeEmpty', 'willBeHappy' ] as $k ) {
84 $preferences[$k] = [
85 'type' => 'text',
86 'section' => 'test',
87 'label' => "\u{00A0}",
88 ];
89 }
90
91 $preferences['testmultiselect'] = [
92 'type' => 'multiselect',
93 'options' => [
94 'Test' => [
95 '<span dir="auto">Some HTML here for option 1</span>' => 'opt1',
96 '<span dir="auto">Some HTML here for option 2</span>' => 'opt2',
97 '<span dir="auto">Some HTML here for option 3</span>' => 'opt3',
98 '<span dir="auto">Some HTML here for option 4</span>' => 'opt4',
99 ],
100 ],
101 'section' => 'test',
102 'label' => "\u{00A0}",
103 'prefix' => 'testmultiselect-',
104 'default' => [],
105 ];
106
107 $preferences['testradio'] = [
108 'type' => 'radio',
109 'options' => [ 'Option 1' => 'option1', 'Option 2' => 'option2' ],
110 'section' => 'test',
111 ];
112 }
113
114 /**
115 * @param IContextSource $context
116 * @param array|null $options
117 *
118 * @return array
119 */
120 public function getOptionKinds( IContextSource $context, $options = null ) {
121 // Match with above.
122 $kinds = [
123 'name' => 'registered',
124 'willBeNull' => 'registered',
125 'willBeEmpty' => 'registered',
126 'willBeHappy' => 'registered',
127 'testradio' => 'registered',
128 'testmultiselect-opt1' => 'registered-multiselect',
129 'testmultiselect-opt2' => 'registered-multiselect',
130 'testmultiselect-opt3' => 'registered-multiselect',
131 'testmultiselect-opt4' => 'registered-multiselect',
132 'special' => 'special',
133 ];
134
135 if ( $options === null ) {
136 return $kinds;
137 }
138
139 $mapping = [];
140 foreach ( $options as $key => $value ) {
141 if ( isset( $kinds[$key] ) ) {
142 $mapping[$key] = $kinds[$key];
143 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
144 $mapping[$key] = 'userjs';
145 } else {
146 $mapping[$key] = 'unused';
147 }
148 }
149
150 return $mapping;
151 }
152
153 private function getSampleRequest( $custom = [] ) {
154 $request = [
155 'token' => '123ABC',
156 'change' => null,
157 'optionname' => null,
158 'optionvalue' => null,
159 ];
160
161 return array_merge( $request, $custom );
162 }
163
164 private function executeQuery( $request ) {
165 $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) );
166 $this->mUserMock->method( 'getRequest' )->willReturn( $this->mContext->getRequest() );
167
168 $this->mTested->execute();
169
170 return $this->mTested->getResult()->getResultData( null, [ 'Strip' => 'all' ] );
171 }
172
173 /**
174 * @expectedException ApiUsageException
175 */
176 public function testNoToken() {
177 $request = $this->getSampleRequest( [ 'token' => null ] );
178
179 $this->executeQuery( $request );
180 }
181
182 public function testAnon() {
183 $this->mUserMock->expects( $this->once() )
184 ->method( 'isAnon' )
185 ->will( $this->returnValue( true ) );
186
187 try {
188 $request = $this->getSampleRequest();
189
190 $this->executeQuery( $request );
191 } catch ( ApiUsageException $e ) {
192 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'notloggedin' ) );
193 return;
194 }
195 $this->fail( "ApiUsageException was not thrown" );
196 }
197
198 public function testNoOptionname() {
199 try {
200 $request = $this->getSampleRequest( [ 'optionvalue' => '1' ] );
201
202 $this->executeQuery( $request );
203 } catch ( ApiUsageException $e ) {
204 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'nooptionname' ) );
205 return;
206 }
207 $this->fail( "ApiUsageException was not thrown" );
208 }
209
210 public function testNoChanges() {
211 $this->mUserMock->expects( $this->never() )
212 ->method( 'resetOptions' );
213
214 $this->mUserMock->expects( $this->never() )
215 ->method( 'setOption' );
216
217 $this->mUserMock->expects( $this->never() )
218 ->method( 'saveSettings' );
219
220 try {
221 $request = $this->getSampleRequest();
222
223 $this->executeQuery( $request );
224 } catch ( ApiUsageException $e ) {
225 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'nochanges' ) );
226 return;
227 }
228 $this->fail( "ApiUsageException was not thrown" );
229 }
230
231 public function testReset() {
232 $this->mUserMock->expects( $this->once() )
233 ->method( 'resetOptions' )
234 ->with( $this->equalTo( [ 'all' ] ) );
235
236 $this->mUserMock->expects( $this->never() )
237 ->method( 'setOption' );
238
239 $this->mUserMock->expects( $this->once() )
240 ->method( 'saveSettings' );
241
242 $request = $this->getSampleRequest( [ 'reset' => '' ] );
243
244 $response = $this->executeQuery( $request );
245
246 $this->assertEquals( self::$Success, $response );
247 }
248
249 public function testResetKinds() {
250 $this->mUserMock->expects( $this->once() )
251 ->method( 'resetOptions' )
252 ->with( $this->equalTo( [ 'registered' ] ) );
253
254 $this->mUserMock->expects( $this->never() )
255 ->method( 'setOption' );
256
257 $this->mUserMock->expects( $this->once() )
258 ->method( 'saveSettings' );
259
260 $request = $this->getSampleRequest( [ 'reset' => '', 'resetkinds' => 'registered' ] );
261
262 $response = $this->executeQuery( $request );
263
264 $this->assertEquals( self::$Success, $response );
265 }
266
267 public function testResetChangeOption() {
268 $this->mUserMock->expects( $this->once() )
269 ->method( 'resetOptions' );
270
271 $this->mUserMock->expects( $this->exactly( 2 ) )
272 ->method( 'setOption' )
273 ->withConsecutive(
274 [ $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ],
275 [ $this->equalTo( 'name' ), $this->equalTo( 'value' ) ]
276 );
277
278 $this->mUserMock->expects( $this->once() )
279 ->method( 'saveSettings' );
280
281 $args = [
282 'reset' => '',
283 'change' => 'willBeHappy=Happy',
284 'optionname' => 'name',
285 'optionvalue' => 'value'
286 ];
287
288 $response = $this->executeQuery( $this->getSampleRequest( $args ) );
289
290 $this->assertEquals( self::$Success, $response );
291 }
292
293 /**
294 * @dataProvider provideOptionManupulation
295 * @param array $params
296 * @param array $setOptions
297 * @param array|null $result
298 */
299 public function testOptionManupulation( array $params, array $setOptions, array $result = null,
300 $message = ''
301 ) {
302 $this->mUserMock->expects( $this->never() )
303 ->method( 'resetOptions' );
304
305 $this->mUserMock->expects( $this->exactly( count( $setOptions ) ) )
306 ->method( 'setOption' )
307 ->withConsecutive( ...$setOptions );
308
309 if ( $setOptions ) {
310 $this->mUserMock->expects( $this->once() )
311 ->method( 'saveSettings' );
312 } else {
313 $this->mUserMock->expects( $this->never() )
314 ->method( 'saveSettings' );
315 }
316
317 $request = $this->getSampleRequest( $params );
318 $response = $this->executeQuery( $request );
319
320 if ( !$result ) {
321 $result = self::$Success;
322 }
323 $this->assertEquals( $result, $response, $message );
324 }
325
326 public function provideOptionManupulation() {
327 return [
328 [
329 [ 'change' => 'userjs-option=1' ],
330 [ [ 'userjs-option', '1' ] ],
331 null,
332 'Setting userjs options',
333 ],
334 [
335 [ 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy' ],
336 [
337 [ 'willBeNull', null ],
338 [ 'willBeEmpty', '' ],
339 [ 'willBeHappy', 'Happy' ],
340 ],
341 null,
342 'Basic option setting',
343 ],
344 [
345 [ 'change' => 'testradio=option2' ],
346 [ [ 'testradio', 'option2' ] ],
347 null,
348 'Changing radio options',
349 ],
350 [
351 [ 'change' => 'testradio' ],
352 [ [ 'testradio', null ] ],
353 null,
354 'Resetting radio options',
355 ],
356 [
357 [ 'change' => 'unknownOption=1' ],
358 [],
359 [
360 'options' => 'success',
361 'warnings' => [
362 'options' => [
363 'warnings' => "Validation error for \"unknownOption\": not a valid preference."
364 ],
365 ],
366 ],
367 'Unrecognized options should be rejected',
368 ],
369 [
370 [ 'change' => 'special=1' ],
371 [],
372 [
373 'options' => 'success',
374 'warnings' => [
375 'options' => [
376 'warnings' => "Validation error for \"special\": cannot be set by this module."
377 ]
378 ]
379 ],
380 'Refuse setting special options',
381 ],
382 [
383 [
384 'change' => 'testmultiselect-opt1=1|testmultiselect-opt2|'
385 . 'testmultiselect-opt3=|testmultiselect-opt4=0'
386 ],
387 [
388 [ 'testmultiselect-opt1', true ],
389 [ 'testmultiselect-opt2', null ],
390 [ 'testmultiselect-opt3', false ],
391 [ 'testmultiselect-opt4', false ],
392 ],
393 null,
394 'Setting multiselect options',
395 ],
396 [
397 [ 'optionname' => 'name', 'optionvalue' => 'value' ],
398 [ [ 'name', 'value' ] ],
399 null,
400 'Setting options via optionname/optionvalue'
401 ],
402 [
403 [ 'optionname' => 'name' ],
404 [ [ 'name', null ] ],
405 null,
406 'Resetting options via optionname without optionvalue',
407 ],
408 ];
409 }
410 }