wiki.techinc.nl/tests/phpunit/includes/libs/ParamValidator/ParamValidatorTest.php
Brad Jorsch 002f409a0b
API: Abstract out parameter validation
With the introduction of a REST API into MediaWiki core, we're going to
want to share parameter validation logic rather than having similar code
in both the Action API and the REST API. This abstracts out parameter
validation logic as a library.

There will be at least two follow-up patches:
* One to add calls in the REST API, plus the interface for the REST API
  to do body validation. Should be reasonably straightforward.
* One to adjust the Action API to use this. That'll be much less
  straightforward, as the Action API needs some MediaWiki-specific types
  (which the REST API might use too in the future) and needs to override
  the defaults on some of the library's checks (to maintain back-compat).

Bug: T142080
Bug: T223239
Change-Id: I5c0cc3a8d686ace97596df5832c450a6a50f902c
Depends-On: Iea05dc439688871c574c639e617765ae88a75ff7
2019-06-23 10:58:44 +02:00

506 lines
16 KiB
PHP

<?php
namespace Wikimedia\ParamValidator;
use Psr\Container\ContainerInterface;
use Wikimedia\ObjectFactory;
/**
* @covers Wikimedia\ParamValidator\ParamValidator
*/
class ParamValidatorTest extends \PHPUnit\Framework\TestCase {
public function testTypeRegistration() {
$validator = new ParamValidator(
new SimpleCallbacks( [] ),
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) )
);
$this->assertSame( array_keys( ParamValidator::$STANDARD_TYPES ), $validator->knownTypes() );
$validator = new ParamValidator(
new SimpleCallbacks( [] ),
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [ 'foo' => [], 'bar' => [] ] ]
);
$validator->addTypeDef( 'baz', [] );
try {
$validator->addTypeDef( 'baz', [] );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
}
$validator->overrideTypeDef( 'bar', null );
$validator->overrideTypeDef( 'baz', [] );
$this->assertSame( [ 'foo', 'baz' ], $validator->knownTypes() );
$this->assertTrue( $validator->hasTypeDef( 'foo' ) );
$this->assertFalse( $validator->hasTypeDef( 'bar' ) );
$this->assertTrue( $validator->hasTypeDef( 'baz' ) );
$this->assertFalse( $validator->hasTypeDef( 'bazz' ) );
}
public function testGetTypeDef() {
$callbacks = new SimpleCallbacks( [] );
$factory = $this->getMockBuilder( ObjectFactory::class )
->setConstructorArgs( [ $this->getMockForAbstractClass( ContainerInterface::class ) ] )
->setMethods( [ 'createObject' ] )
->getMock();
$factory->method( 'createObject' )
->willReturnCallback( function ( $spec, $options ) use ( $callbacks ) {
$this->assertInternalType( 'array', $spec );
$this->assertSame(
[ 'extraArgs' => [ $callbacks ], 'assertClass' => TypeDef::class ], $options
);
$ret = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->getMockForAbstractClass();
$ret->spec = $spec;
return $ret;
} );
$validator = new ParamValidator( $callbacks, $factory );
$def = $validator->getTypeDef( 'boolean' );
$this->assertInstanceOf( TypeDef::class, $def );
$this->assertSame( ParamValidator::$STANDARD_TYPES['boolean'], $def->spec );
$def = $validator->getTypeDef( [] );
$this->assertInstanceOf( TypeDef::class, $def );
$this->assertSame( ParamValidator::$STANDARD_TYPES['enum'], $def->spec );
$def = $validator->getTypeDef( 'missing' );
$this->assertNull( $def );
}
public function testGetTypeDef_caching() {
$callbacks = new SimpleCallbacks( [] );
$mb = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] );
$def1 = $mb->getMockForAbstractClass();
$def2 = $mb->getMockForAbstractClass();
$this->assertNotSame( $def1, $def2, 'sanity check' );
$factory = $this->getMockBuilder( ObjectFactory::class )
->setConstructorArgs( [ $this->getMockForAbstractClass( ContainerInterface::class ) ] )
->setMethods( [ 'createObject' ] )
->getMock();
$factory->expects( $this->once() )->method( 'createObject' )->willReturn( $def1 );
$validator = new ParamValidator( $callbacks, $factory, [ 'typeDefs' => [
'foo' => [],
'bar' => $def2,
] ] );
$this->assertSame( $def1, $validator->getTypeDef( 'foo' ) );
// Second call doesn't re-call ObjectFactory
$this->assertSame( $def1, $validator->getTypeDef( 'foo' ) );
// When registered a TypeDef directly, doesn't call ObjectFactory
$this->assertSame( $def2, $validator->getTypeDef( 'bar' ) );
}
/**
* @expectedException \UnexpectedValueException
* @expectedExceptionMessage Expected instance of Wikimedia\ParamValidator\TypeDef, got stdClass
*/
public function testGetTypeDef_error() {
$validator = new ParamValidator(
new SimpleCallbacks( [] ),
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [ 'foo' => [ 'class' => \stdClass::class ] ] ]
);
$validator->getTypeDef( 'foo' );
}
/** @dataProvider provideNormalizeSettings */
public function testNormalizeSettings( $input, $expect ) {
$callbacks = new SimpleCallbacks( [] );
$mb = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->setMethods( [ 'normalizeSettings' ] );
$mock1 = $mb->getMockForAbstractClass();
$mock1->method( 'normalizeSettings' )->willReturnCallback( function ( $s ) {
$s['foo'] = 'FooBar!';
return $s;
} );
$mock2 = $mb->getMockForAbstractClass();
$mock2->method( 'normalizeSettings' )->willReturnCallback( function ( $s ) {
$s['bar'] = 'FooBar!';
return $s;
} );
$validator = new ParamValidator(
$callbacks,
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [ 'foo' => $mock1, 'bar' => $mock2 ] ]
);
$this->assertSame( $expect, $validator->normalizeSettings( $input ) );
}
public static function provideNormalizeSettings() {
return [
'Plain value' => [
'ok?',
[ ParamValidator::PARAM_DEFAULT => 'ok?', ParamValidator::PARAM_TYPE => 'string' ],
],
'Simple array' => [
[ 'test' => 'ok?' ],
[ 'test' => 'ok?', ParamValidator::PARAM_TYPE => 'NULL' ],
],
'A type with overrides' => [
[ ParamValidator::PARAM_TYPE => 'foo', 'test' => 'ok?' ],
[ ParamValidator::PARAM_TYPE => 'foo', 'test' => 'ok?', 'foo' => 'FooBar!' ],
],
];
}
/** @dataProvider provideExplodeMultiValue */
public function testExplodeMultiValue( $value, $limit, $expect ) {
$this->assertSame( $expect, ParamValidator::explodeMultiValue( $value, $limit ) );
}
public static function provideExplodeMultiValue() {
return [
[ 'foobar', 100, [ 'foobar' ] ],
[ 'foo|bar|baz', 100, [ 'foo', 'bar', 'baz' ] ],
[ "\x1Ffoo\x1Fbar\x1Fbaz", 100, [ 'foo', 'bar', 'baz' ] ],
[ 'foo|bar|baz', 2, [ 'foo', 'bar|baz' ] ],
[ "\x1Ffoo\x1Fbar\x1Fbaz", 2, [ 'foo', "bar\x1Fbaz" ] ],
[ '|bar|baz', 100, [ '', 'bar', 'baz' ] ],
[ "\x1F\x1Fbar\x1Fbaz", 100, [ '', 'bar', 'baz' ] ],
[ '', 100, [] ],
[ "\x1F", 100, [] ],
];
}
/**
* @expectedException DomainException
* @expectedExceptionMessage Param foo's type is unknown - string
*/
public function testGetValue_badType() {
$validator = new ParamValidator(
new SimpleCallbacks( [] ),
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [] ]
);
$validator->getValue( 'foo', 'default', [] );
}
/** @dataProvider provideGetValue */
public function testGetValue(
$settings, $parseLimit, $get, $value, $isSensitive, $isDeprecated
) {
$callbacks = new SimpleCallbacks( $get );
$dummy = (object)[];
$options = [ $dummy ];
$settings += [
ParamValidator::PARAM_TYPE => 'xyz',
ParamValidator::PARAM_DEFAULT => null,
];
$mockDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->getMockForAbstractClass();
// Mock the validateValue method so we can test only getValue
$validator = $this->getMockBuilder( ParamValidator::class )
->setConstructorArgs( [
$callbacks,
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [ 'xyz' => $mockDef ] ]
] )
->setMethods( [ 'validateValue' ] )
->getMock();
$validator->expects( $this->once() )->method( 'validateValue' )
->with(
$this->identicalTo( 'foobar' ),
$this->identicalTo( $value ),
$this->identicalTo( $settings ),
$this->identicalTo( $options )
)
->willReturn( $dummy );
$this->assertSame( $dummy, $validator->getValue( 'foobar', $settings, $options ) );
$expectConditions = [];
if ( $isSensitive ) {
$expectConditions[] = new ValidationException(
'foobar', $value, $settings, 'param-sensitive', []
);
}
if ( $isDeprecated ) {
$expectConditions[] = new ValidationException(
'foobar', $value, $settings, 'param-deprecated', []
);
}
$this->assertEquals( $expectConditions, $callbacks->getRecordedConditions() );
}
public static function provideGetValue() {
$sen = [ ParamValidator::PARAM_SENSITIVE => true ];
$dep = [ ParamValidator::PARAM_DEPRECATED => true ];
$dflt = [ ParamValidator::PARAM_DEFAULT => 'DeFaUlT' ];
return [
'Simple case' => [ [], false, [ 'foobar' => '!!!' ], '!!!', false, false ],
'Not provided' => [ $sen + $dep, false, [], null, false, false ],
'Not provided, default' => [ $sen + $dep + $dflt, true, [], 'DeFaUlT', false, false ],
'Provided' => [ $dflt, false, [ 'foobar' => 'XYZ' ], 'XYZ', false, false ],
'Provided, sensitive' => [ $sen, false, [ 'foobar' => 'XYZ' ], 'XYZ', true, false ],
'Provided, deprecated' => [ $dep, false, [ 'foobar' => 'XYZ' ], 'XYZ', false, true ],
'Provided array' => [ $dflt, false, [ 'foobar' => [ 'XYZ' ] ], [ 'XYZ' ], false, false ],
];
}
/**
* @expectedException DomainException
* @expectedExceptionMessage Param foo's type is unknown - string
*/
public function testValidateValue_badType() {
$validator = new ParamValidator(
new SimpleCallbacks( [] ),
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [] ]
);
$validator->validateValue( 'foo', null, 'default', [] );
}
/** @dataProvider provideValidateValue */
public function testValidateValue(
$value, $settings, $highLimits, $valuesList, $calls, $expect, $expectConditions = [],
$constructorOptions = []
) {
$callbacks = new SimpleCallbacks( [] );
$settings += [
ParamValidator::PARAM_TYPE => 'xyz',
ParamValidator::PARAM_DEFAULT => null,
];
$dummy = (object)[];
$options = [ $dummy, 'useHighLimits' => $highLimits ];
$eOptions = $options;
$eOptions2 = $eOptions;
if ( $valuesList !== null ) {
$eOptions2['values-list'] = $valuesList;
}
$mockDef = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->setMethods( [ 'validate', 'getEnumValues' ] )
->getMockForAbstractClass();
$mockDef->method( 'getEnumValues' )
->with(
$this->identicalTo( 'foobar' ), $this->identicalTo( $settings ), $this->identicalTo( $eOptions )
)
->willReturn( [ 'a', 'b', 'c', 'd', 'e', 'f' ] );
$mockDef->expects( $this->exactly( count( $calls ) ) )->method( 'validate' )->willReturnCallback(
function ( $n, $v, $s, $o ) use ( $settings, $eOptions2, $calls ) {
$this->assertSame( 'foobar', $n );
$this->assertSame( $settings, $s );
$this->assertSame( $eOptions2, $o );
if ( !array_key_exists( $v, $calls ) ) {
$this->fail( "Called with unexpected value '$v'" );
}
if ( $calls[$v] === null ) {
throw new ValidationException( $n, $v, $s, 'badvalue', [] );
}
return $calls[$v];
}
);
$validator = new ParamValidator(
$callbacks,
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
$constructorOptions + [ 'typeDefs' => [ 'xyz' => $mockDef ] ]
);
if ( $expect instanceof ValidationException ) {
try {
$validator->validateValue( 'foobar', $value, $settings, $options );
$this->fail( 'Expected exception not thrown' );
} catch ( ValidationException $ex ) {
$this->assertSame( $expect->getFailureCode(), $ex->getFailureCode() );
$this->assertSame( $expect->getFailureData(), $ex->getFailureData() );
}
} else {
$this->assertSame(
$expect, $validator->validateValue( 'foobar', $value, $settings, $options )
);
$conditions = [];
foreach ( $callbacks->getRecordedConditions() as $c ) {
$conditions[] = array_merge( [ $c->getFailureCode() ], $c->getFailureData() );
}
$this->assertSame( $expectConditions, $conditions );
}
}
public static function provideValidateValue() {
return [
'No value' => [ null, [], false, null, [], null ],
'No value, required' => [
null,
[ ParamValidator::PARAM_REQUIRED => true ],
false,
null,
[],
new ValidationException( 'foobar', null, [], 'missingparam', [] ),
],
'Non-multi value' => [ 'abc', [], false, null, [ 'abc' => 'def' ], 'def' ],
'Simple multi value' => [
'a|b|c|d',
[ ParamValidator::PARAM_ISMULTI => true ],
false,
[ 'a', 'b', 'c', 'd' ],
[ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
[ 'A', 'B', 'C', 'D' ],
],
'Array multi value' => [
[ 'a', 'b', 'c', 'd' ],
[ ParamValidator::PARAM_ISMULTI => true ],
false,
[ 'a', 'b', 'c', 'd' ],
[ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
[ 'A', 'B', 'C', 'D' ],
],
'Multi value with PARAM_ALL' => [
'*',
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
false,
null,
[],
[ 'a', 'b', 'c', 'd', 'e', 'f' ],
],
'Multi value with PARAM_ALL = "x"' => [
'x',
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => "x" ],
false,
null,
[],
[ 'a', 'b', 'c', 'd', 'e', 'f' ],
],
'Multi value with PARAM_ALL = "x", passing "*"' => [
'*',
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => "x" ],
false,
[ '*' ],
[ '*' => '?' ],
[ '?' ],
],
'Too many values' => [
'a|b|c|d',
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
],
false,
null,
[],
new ValidationException( 'foobar', 'a|b|c|d', [], 'toomanyvalues', [ 'limit' => 2 ] ),
],
'Too many values as array' => [
[ 'a', 'b', 'c', 'd' ],
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
],
false,
null,
[],
new ValidationException(
'foobar', [ 'a', 'b', 'c', 'd' ], [], 'toomanyvalues', [ 'limit' => 2 ]
),
],
'Not too many values for highlimits' => [
'a|b|c|d',
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
],
true,
[ 'a', 'b', 'c', 'd' ],
[ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
[ 'A', 'B', 'C', 'D' ],
],
'Too many values for highlimits' => [
'a|b|c|d|e',
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
],
true,
null,
[],
new ValidationException( 'foobar', 'a|b|c|d|e', [], 'toomanyvalues', [ 'limit' => 4 ] ),
],
'Too many values via default' => [
'a|b|c|d',
[
ParamValidator::PARAM_ISMULTI => true,
],
false,
null,
[],
new ValidationException( 'foobar', 'a|b|c|d', [], 'toomanyvalues', [ 'limit' => 2 ] ),
[],
[ 'ismultiLimits' => [ 2, 4 ] ],
],
'Not too many values for highlimits via default' => [
'a|b|c|d',
[
ParamValidator::PARAM_ISMULTI => true,
],
true,
[ 'a', 'b', 'c', 'd' ],
[ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
[ 'A', 'B', 'C', 'D' ],
[],
[ 'ismultiLimits' => [ 2, 4 ] ],
],
'Too many values for highlimits via default' => [
'a|b|c|d|e',
[
ParamValidator::PARAM_ISMULTI => true,
],
true,
null,
[],
new ValidationException( 'foobar', 'a|b|c|d|e', [], 'toomanyvalues', [ 'limit' => 4 ] ),
[],
[ 'ismultiLimits' => [ 2, 4 ] ],
],
'Invalid values' => [
'a|b|c|d',
[ ParamValidator::PARAM_ISMULTI => true ],
false,
[ 'a', 'b', 'c', 'd' ],
[ 'a' => 'A', 'b' => null ],
new ValidationException( 'foobar', 'b', [], 'badvalue', [] ),
],
'Ignored invalid values' => [
'a|b|c|d',
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_IGNORE_INVALID_VALUES => true,
],
false,
[ 'a', 'b', 'c', 'd' ],
[ 'a' => 'A', 'b' => null, 'c' => null, 'd' => 'D' ],
[ 'A', 'D' ],
[
[ 'unrecognizedvalues', 'values' => [ 'b', 'c' ] ],
],
],
];
}
}