Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / ParamValidator / ParamValidatorTest.php
1 <?php
2
3 namespace Wikimedia\ParamValidator;
4
5 use Psr\Container\ContainerInterface;
6 use Wikimedia\ObjectFactory;
7
8 /**
9 * @covers Wikimedia\ParamValidator\ParamValidator
10 */
11 class ParamValidatorTest extends \PHPUnit\Framework\TestCase {
12
13 public function testTypeRegistration() {
14 $validator = new ParamValidator(
15 new SimpleCallbacks( [] ),
16 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) )
17 );
18 $this->assertSame( array_keys( ParamValidator::$STANDARD_TYPES ), $validator->knownTypes() );
19
20 $validator = new ParamValidator(
21 new SimpleCallbacks( [] ),
22 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
23 [ 'typeDefs' => [ 'foo' => [], 'bar' => [] ] ]
24 );
25 $validator->addTypeDef( 'baz', [] );
26 try {
27 $validator->addTypeDef( 'baz', [] );
28 $this->fail( 'Expected exception not thrown' );
29 } catch ( \InvalidArgumentException $ex ) {
30 }
31 $validator->overrideTypeDef( 'bar', null );
32 $validator->overrideTypeDef( 'baz', [] );
33 $this->assertSame( [ 'foo', 'baz' ], $validator->knownTypes() );
34
35 $this->assertTrue( $validator->hasTypeDef( 'foo' ) );
36 $this->assertFalse( $validator->hasTypeDef( 'bar' ) );
37 $this->assertTrue( $validator->hasTypeDef( 'baz' ) );
38 $this->assertFalse( $validator->hasTypeDef( 'bazz' ) );
39 }
40
41 public function testGetTypeDef() {
42 $callbacks = new SimpleCallbacks( [] );
43 $factory = $this->getMockBuilder( ObjectFactory::class )
44 ->setConstructorArgs( [ $this->getMockForAbstractClass( ContainerInterface::class ) ] )
45 ->setMethods( [ 'createObject' ] )
46 ->getMock();
47 $factory->method( 'createObject' )
48 ->willReturnCallback( function ( $spec, $options ) use ( $callbacks ) {
49 $this->assertInternalType( 'array', $spec );
50 $this->assertSame(
51 [ 'extraArgs' => [ $callbacks ], 'assertClass' => TypeDef::class ], $options
52 );
53 $ret = $this->getMockBuilder( TypeDef::class )
54 ->setConstructorArgs( [ $callbacks ] )
55 ->getMockForAbstractClass();
56 $ret->spec = $spec;
57 return $ret;
58 } );
59 $validator = new ParamValidator( $callbacks, $factory );
60
61 $def = $validator->getTypeDef( 'boolean' );
62 $this->assertInstanceOf( TypeDef::class, $def );
63 $this->assertSame( ParamValidator::$STANDARD_TYPES['boolean'], $def->spec );
64
65 $def = $validator->getTypeDef( [] );
66 $this->assertInstanceOf( TypeDef::class, $def );
67 $this->assertSame( ParamValidator::$STANDARD_TYPES['enum'], $def->spec );
68
69 $def = $validator->getTypeDef( 'missing' );
70 $this->assertNull( $def );
71 }
72
73 public function testGetTypeDef_caching() {
74 $callbacks = new SimpleCallbacks( [] );
75
76 $mb = $this->getMockBuilder( TypeDef::class )
77 ->setConstructorArgs( [ $callbacks ] );
78 $def1 = $mb->getMockForAbstractClass();
79 $def2 = $mb->getMockForAbstractClass();
80 $this->assertNotSame( $def1, $def2, 'sanity check' );
81
82 $factory = $this->getMockBuilder( ObjectFactory::class )
83 ->setConstructorArgs( [ $this->getMockForAbstractClass( ContainerInterface::class ) ] )
84 ->setMethods( [ 'createObject' ] )
85 ->getMock();
86 $factory->expects( $this->once() )->method( 'createObject' )->willReturn( $def1 );
87
88 $validator = new ParamValidator( $callbacks, $factory, [ 'typeDefs' => [
89 'foo' => [],
90 'bar' => $def2,
91 ] ] );
92
93 $this->assertSame( $def1, $validator->getTypeDef( 'foo' ) );
94
95 // Second call doesn't re-call ObjectFactory
96 $this->assertSame( $def1, $validator->getTypeDef( 'foo' ) );
97
98 // When registered a TypeDef directly, doesn't call ObjectFactory
99 $this->assertSame( $def2, $validator->getTypeDef( 'bar' ) );
100 }
101
102 /**
103 * @expectedException \UnexpectedValueException
104 * @expectedExceptionMessage Expected instance of Wikimedia\ParamValidator\TypeDef, got stdClass
105 */
106 public function testGetTypeDef_error() {
107 $validator = new ParamValidator(
108 new SimpleCallbacks( [] ),
109 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
110 [ 'typeDefs' => [ 'foo' => [ 'class' => \stdClass::class ] ] ]
111 );
112 $validator->getTypeDef( 'foo' );
113 }
114
115 /** @dataProvider provideNormalizeSettings */
116 public function testNormalizeSettings( $input, $expect ) {
117 $callbacks = new SimpleCallbacks( [] );
118
119 $mb = $this->getMockBuilder( TypeDef::class )
120 ->setConstructorArgs( [ $callbacks ] )
121 ->setMethods( [ 'normalizeSettings' ] );
122 $mock1 = $mb->getMockForAbstractClass();
123 $mock1->method( 'normalizeSettings' )->willReturnCallback( function ( $s ) {
124 $s['foo'] = 'FooBar!';
125 return $s;
126 } );
127 $mock2 = $mb->getMockForAbstractClass();
128 $mock2->method( 'normalizeSettings' )->willReturnCallback( function ( $s ) {
129 $s['bar'] = 'FooBar!';
130 return $s;
131 } );
132
133 $validator = new ParamValidator(
134 $callbacks,
135 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
136 [ 'typeDefs' => [ 'foo' => $mock1, 'bar' => $mock2 ] ]
137 );
138
139 $this->assertSame( $expect, $validator->normalizeSettings( $input ) );
140 }
141
142 public static function provideNormalizeSettings() {
143 return [
144 'Plain value' => [
145 'ok?',
146 [ ParamValidator::PARAM_DEFAULT => 'ok?', ParamValidator::PARAM_TYPE => 'string' ],
147 ],
148 'Simple array' => [
149 [ 'test' => 'ok?' ],
150 [ 'test' => 'ok?', ParamValidator::PARAM_TYPE => 'NULL' ],
151 ],
152 'A type with overrides' => [
153 [ ParamValidator::PARAM_TYPE => 'foo', 'test' => 'ok?' ],
154 [ ParamValidator::PARAM_TYPE => 'foo', 'test' => 'ok?', 'foo' => 'FooBar!' ],
155 ],
156 ];
157 }
158
159 /** @dataProvider provideExplodeMultiValue */
160 public function testExplodeMultiValue( $value, $limit, $expect ) {
161 $this->assertSame( $expect, ParamValidator::explodeMultiValue( $value, $limit ) );
162 }
163
164 public static function provideExplodeMultiValue() {
165 return [
166 [ 'foobar', 100, [ 'foobar' ] ],
167 [ 'foo|bar|baz', 100, [ 'foo', 'bar', 'baz' ] ],
168 [ "\x1Ffoo\x1Fbar\x1Fbaz", 100, [ 'foo', 'bar', 'baz' ] ],
169 [ 'foo|bar|baz', 2, [ 'foo', 'bar|baz' ] ],
170 [ "\x1Ffoo\x1Fbar\x1Fbaz", 2, [ 'foo', "bar\x1Fbaz" ] ],
171 [ '|bar|baz', 100, [ '', 'bar', 'baz' ] ],
172 [ "\x1F\x1Fbar\x1Fbaz", 100, [ '', 'bar', 'baz' ] ],
173 [ '', 100, [] ],
174 [ "\x1F", 100, [] ],
175 ];
176 }
177
178 /**
179 * @expectedException DomainException
180 * @expectedExceptionMessage Param foo's type is unknown - string
181 */
182 public function testGetValue_badType() {
183 $validator = new ParamValidator(
184 new SimpleCallbacks( [] ),
185 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
186 [ 'typeDefs' => [] ]
187 );
188 $validator->getValue( 'foo', 'default', [] );
189 }
190
191 /** @dataProvider provideGetValue */
192 public function testGetValue(
193 $settings, $parseLimit, $get, $value, $isSensitive, $isDeprecated
194 ) {
195 $callbacks = new SimpleCallbacks( $get );
196 $dummy = (object)[];
197 $options = [ $dummy ];
198
199 $settings += [
200 ParamValidator::PARAM_TYPE => 'xyz',
201 ParamValidator::PARAM_DEFAULT => null,
202 ];
203
204 $mockDef = $this->getMockBuilder( TypeDef::class )
205 ->setConstructorArgs( [ $callbacks ] )
206 ->getMockForAbstractClass();
207
208 // Mock the validateValue method so we can test only getValue
209 $validator = $this->getMockBuilder( ParamValidator::class )
210 ->setConstructorArgs( [
211 $callbacks,
212 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
213 [ 'typeDefs' => [ 'xyz' => $mockDef ] ]
214 ] )
215 ->setMethods( [ 'validateValue' ] )
216 ->getMock();
217 $validator->expects( $this->once() )->method( 'validateValue' )
218 ->with(
219 $this->identicalTo( 'foobar' ),
220 $this->identicalTo( $value ),
221 $this->identicalTo( $settings ),
222 $this->identicalTo( $options )
223 )
224 ->willReturn( $dummy );
225
226 $this->assertSame( $dummy, $validator->getValue( 'foobar', $settings, $options ) );
227
228 $expectConditions = [];
229 if ( $isSensitive ) {
230 $expectConditions[] = new ValidationException(
231 'foobar', $value, $settings, 'param-sensitive', []
232 );
233 }
234 if ( $isDeprecated ) {
235 $expectConditions[] = new ValidationException(
236 'foobar', $value, $settings, 'param-deprecated', []
237 );
238 }
239 $this->assertEquals( $expectConditions, $callbacks->getRecordedConditions() );
240 }
241
242 public static function provideGetValue() {
243 $sen = [ ParamValidator::PARAM_SENSITIVE => true ];
244 $dep = [ ParamValidator::PARAM_DEPRECATED => true ];
245 $dflt = [ ParamValidator::PARAM_DEFAULT => 'DeFaUlT' ];
246 return [
247 'Simple case' => [ [], false, [ 'foobar' => '!!!' ], '!!!', false, false ],
248 'Not provided' => [ $sen + $dep, false, [], null, false, false ],
249 'Not provided, default' => [ $sen + $dep + $dflt, true, [], 'DeFaUlT', false, false ],
250 'Provided' => [ $dflt, false, [ 'foobar' => 'XYZ' ], 'XYZ', false, false ],
251 'Provided, sensitive' => [ $sen, false, [ 'foobar' => 'XYZ' ], 'XYZ', true, false ],
252 'Provided, deprecated' => [ $dep, false, [ 'foobar' => 'XYZ' ], 'XYZ', false, true ],
253 'Provided array' => [ $dflt, false, [ 'foobar' => [ 'XYZ' ] ], [ 'XYZ' ], false, false ],
254 ];
255 }
256
257 /**
258 * @expectedException DomainException
259 * @expectedExceptionMessage Param foo's type is unknown - string
260 */
261 public function testValidateValue_badType() {
262 $validator = new ParamValidator(
263 new SimpleCallbacks( [] ),
264 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
265 [ 'typeDefs' => [] ]
266 );
267 $validator->validateValue( 'foo', null, 'default', [] );
268 }
269
270 /** @dataProvider provideValidateValue */
271 public function testValidateValue(
272 $value, $settings, $highLimits, $valuesList, $calls, $expect, $expectConditions = [],
273 $constructorOptions = []
274 ) {
275 $callbacks = new SimpleCallbacks( [] );
276 $settings += [
277 ParamValidator::PARAM_TYPE => 'xyz',
278 ParamValidator::PARAM_DEFAULT => null,
279 ];
280 $dummy = (object)[];
281 $options = [ $dummy, 'useHighLimits' => $highLimits ];
282 $eOptions = $options;
283 $eOptions2 = $eOptions;
284 if ( $valuesList !== null ) {
285 $eOptions2['values-list'] = $valuesList;
286 }
287
288 $mockDef = $this->getMockBuilder( TypeDef::class )
289 ->setConstructorArgs( [ $callbacks ] )
290 ->setMethods( [ 'validate', 'getEnumValues' ] )
291 ->getMockForAbstractClass();
292 $mockDef->method( 'getEnumValues' )
293 ->with(
294 $this->identicalTo( 'foobar' ), $this->identicalTo( $settings ), $this->identicalTo( $eOptions )
295 )
296 ->willReturn( [ 'a', 'b', 'c', 'd', 'e', 'f' ] );
297 $mockDef->expects( $this->exactly( count( $calls ) ) )->method( 'validate' )->willReturnCallback(
298 function ( $n, $v, $s, $o ) use ( $settings, $eOptions2, $calls ) {
299 $this->assertSame( 'foobar', $n );
300 $this->assertSame( $settings, $s );
301 $this->assertSame( $eOptions2, $o );
302
303 if ( !array_key_exists( $v, $calls ) ) {
304 $this->fail( "Called with unexpected value '$v'" );
305 }
306 if ( $calls[$v] === null ) {
307 throw new ValidationException( $n, $v, $s, 'badvalue', [] );
308 }
309 return $calls[$v];
310 }
311 );
312
313 $validator = new ParamValidator(
314 $callbacks,
315 new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
316 $constructorOptions + [ 'typeDefs' => [ 'xyz' => $mockDef ] ]
317 );
318
319 if ( $expect instanceof ValidationException ) {
320 try {
321 $validator->validateValue( 'foobar', $value, $settings, $options );
322 $this->fail( 'Expected exception not thrown' );
323 } catch ( ValidationException $ex ) {
324 $this->assertSame( $expect->getFailureCode(), $ex->getFailureCode() );
325 $this->assertSame( $expect->getFailureData(), $ex->getFailureData() );
326 }
327 } else {
328 $this->assertSame(
329 $expect, $validator->validateValue( 'foobar', $value, $settings, $options )
330 );
331
332 $conditions = [];
333 foreach ( $callbacks->getRecordedConditions() as $c ) {
334 $conditions[] = array_merge( [ $c->getFailureCode() ], $c->getFailureData() );
335 }
336 $this->assertSame( $expectConditions, $conditions );
337 }
338 }
339
340 public static function provideValidateValue() {
341 return [
342 'No value' => [ null, [], false, null, [], null ],
343 'No value, required' => [
344 null,
345 [ ParamValidator::PARAM_REQUIRED => true ],
346 false,
347 null,
348 [],
349 new ValidationException( 'foobar', null, [], 'missingparam', [] ),
350 ],
351 'Non-multi value' => [ 'abc', [], false, null, [ 'abc' => 'def' ], 'def' ],
352 'Simple multi value' => [
353 'a|b|c|d',
354 [ ParamValidator::PARAM_ISMULTI => true ],
355 false,
356 [ 'a', 'b', 'c', 'd' ],
357 [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
358 [ 'A', 'B', 'C', 'D' ],
359 ],
360 'Array multi value' => [
361 [ 'a', 'b', 'c', 'd' ],
362 [ ParamValidator::PARAM_ISMULTI => true ],
363 false,
364 [ 'a', 'b', 'c', 'd' ],
365 [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
366 [ 'A', 'B', 'C', 'D' ],
367 ],
368 'Multi value with PARAM_ALL' => [
369 '*',
370 [ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
371 false,
372 null,
373 [],
374 [ 'a', 'b', 'c', 'd', 'e', 'f' ],
375 ],
376 'Multi value with PARAM_ALL = "x"' => [
377 'x',
378 [ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => "x" ],
379 false,
380 null,
381 [],
382 [ 'a', 'b', 'c', 'd', 'e', 'f' ],
383 ],
384 'Multi value with PARAM_ALL = "x", passing "*"' => [
385 '*',
386 [ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => "x" ],
387 false,
388 [ '*' ],
389 [ '*' => '?' ],
390 [ '?' ],
391 ],
392
393 'Too many values' => [
394 'a|b|c|d',
395 [
396 ParamValidator::PARAM_ISMULTI => true,
397 ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
398 ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
399 ],
400 false,
401 null,
402 [],
403 new ValidationException( 'foobar', 'a|b|c|d', [], 'toomanyvalues', [ 'limit' => 2 ] ),
404 ],
405 'Too many values as array' => [
406 [ 'a', 'b', 'c', 'd' ],
407 [
408 ParamValidator::PARAM_ISMULTI => true,
409 ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
410 ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
411 ],
412 false,
413 null,
414 [],
415 new ValidationException(
416 'foobar', [ 'a', 'b', 'c', 'd' ], [], 'toomanyvalues', [ 'limit' => 2 ]
417 ),
418 ],
419 'Not too many values for highlimits' => [
420 'a|b|c|d',
421 [
422 ParamValidator::PARAM_ISMULTI => true,
423 ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
424 ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
425 ],
426 true,
427 [ 'a', 'b', 'c', 'd' ],
428 [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
429 [ 'A', 'B', 'C', 'D' ],
430 ],
431 'Too many values for highlimits' => [
432 'a|b|c|d|e',
433 [
434 ParamValidator::PARAM_ISMULTI => true,
435 ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
436 ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
437 ],
438 true,
439 null,
440 [],
441 new ValidationException( 'foobar', 'a|b|c|d|e', [], 'toomanyvalues', [ 'limit' => 4 ] ),
442 ],
443
444 'Too many values via default' => [
445 'a|b|c|d',
446 [
447 ParamValidator::PARAM_ISMULTI => true,
448 ],
449 false,
450 null,
451 [],
452 new ValidationException( 'foobar', 'a|b|c|d', [], 'toomanyvalues', [ 'limit' => 2 ] ),
453 [],
454 [ 'ismultiLimits' => [ 2, 4 ] ],
455 ],
456 'Not too many values for highlimits via default' => [
457 'a|b|c|d',
458 [
459 ParamValidator::PARAM_ISMULTI => true,
460 ],
461 true,
462 [ 'a', 'b', 'c', 'd' ],
463 [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
464 [ 'A', 'B', 'C', 'D' ],
465 [],
466 [ 'ismultiLimits' => [ 2, 4 ] ],
467 ],
468 'Too many values for highlimits via default' => [
469 'a|b|c|d|e',
470 [
471 ParamValidator::PARAM_ISMULTI => true,
472 ],
473 true,
474 null,
475 [],
476 new ValidationException( 'foobar', 'a|b|c|d|e', [], 'toomanyvalues', [ 'limit' => 4 ] ),
477 [],
478 [ 'ismultiLimits' => [ 2, 4 ] ],
479 ],
480
481 'Invalid values' => [
482 'a|b|c|d',
483 [ ParamValidator::PARAM_ISMULTI => true ],
484 false,
485 [ 'a', 'b', 'c', 'd' ],
486 [ 'a' => 'A', 'b' => null ],
487 new ValidationException( 'foobar', 'b', [], 'badvalue', [] ),
488 ],
489 'Ignored invalid values' => [
490 'a|b|c|d',
491 [
492 ParamValidator::PARAM_ISMULTI => true,
493 ParamValidator::PARAM_IGNORE_INVALID_VALUES => true,
494 ],
495 false,
496 [ 'a', 'b', 'c', 'd' ],
497 [ 'a' => 'A', 'b' => null, 'c' => null, 'd' => 'D' ],
498 [ 'A', 'D' ],
499 [
500 [ 'unrecognizedvalues', 'values' => [ 'b', 'c' ] ],
501 ],
502 ],
503 ];
504 }
505
506 }