diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/utils')
10 files changed, 1239 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php b/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php new file mode 100644 index 00000000..cf45f9fd --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Tests for IP validity functions. + * + * Ported from /t/inc/IP.t by avar. + * + * @todo Test methods in this call should be split into a method and a + * dataprovider. + */ + +/** + * @group IP + * @covers AvroValidator + */ +class AvroValidatorTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function setUp() { + if ( !class_exists( 'AvroSchema' ) ) { + $this->markTestSkipped( 'Avro is required to run the AvroValidatorTest' ); + } + parent::setUp(); + } + + public function getErrorsProvider() { + $stringSchema = AvroSchema::parse( json_encode( [ 'type' => 'string' ] ) ); + $stringArraySchema = AvroSchema::parse( json_encode( [ + 'type' => 'array', + 'items' => 'string', + ] ) ); + $recordSchema = AvroSchema::parse( json_encode( [ + 'type' => 'record', + 'name' => 'ut', + 'fields' => [ + [ 'name' => 'id', 'type' => 'int', 'required' => true ], + ], + ] ) ); + $enumSchema = AvroSchema::parse( json_encode( [ + 'type' => 'record', + 'name' => 'ut', + 'fields' => [ + [ 'name' => 'count', 'type' => [ 'int', 'null' ] ], + ], + ] ) ); + + return [ + [ + 'No errors with a simple string serialization', + $stringSchema, 'foobar', [], + ], + + [ + 'Cannot serialize integer into string', + $stringSchema, 5, 'Expected string, but recieved integer', + ], + + [ + 'Cannot serialize array into string', + $stringSchema, [], 'Expected string, but recieved array', + ], + + [ + 'allows and ignores extra fields', + $recordSchema, [ 'id' => 4, 'foo' => 'bar' ], [], + ], + + [ + 'detects missing fields', + $recordSchema, [], [ 'id' => 'Missing expected field' ], + ], + + [ + 'handles first element in enum', + $enumSchema, [ 'count' => 4 ], [], + ], + + [ + 'handles second element in enum', + $enumSchema, [ 'count' => null ], [], + ], + + [ + 'rejects element not in union', + $enumSchema, [ 'count' => 'invalid' ], [ 'count' => [ + 'Expected any one of these to be true', + [ + 'Expected integer, but recieved string', + 'Expected null, but recieved string', + ] + ] ] + ], + [ + 'Empty array is accepted', + $stringArraySchema, [], [] + ], + [ + 'correct array element accepted', + $stringArraySchema, [ 'fizzbuzz' ], [] + ], + [ + 'incorrect array element rejected', + $stringArraySchema, [ '12', 34 ], [ 'Expected string, but recieved integer' ] + ], + ]; + } + + /** + * @dataProvider getErrorsProvider + */ + public function testGetErrors( $message, $schema, $datum, $expected ) { + $this->assertEquals( + $expected, + AvroValidator::getErrors( $schema, $datum ), + $message + ); + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php b/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php new file mode 100644 index 00000000..52b14339 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php @@ -0,0 +1,252 @@ +<?php + +/** + * Tests for BatchRowUpdate and its components + * + * @group db + * + * @covers BatchRowUpdate + * @covers BatchRowIterator + * @covers BatchRowWriter + */ +class BatchRowUpdateTest extends MediaWikiTestCase { + + public function testWriterBasicFunctionality() { + $db = $this->mockDb( [ 'update' ] ); + $writer = new BatchRowWriter( $db, 'echo_event' ); + + $updates = [ + self::mockUpdate( [ 'something' => 'changed' ] ), + self::mockUpdate( [ 'otherthing' => 'changed' ] ), + self::mockUpdate( [ 'and' => 'something', 'else' => 'changed' ] ), + ]; + + $db->expects( $this->exactly( count( $updates ) ) ) + ->method( 'update' ); + + $writer->write( $updates ); + } + + protected static function mockUpdate( array $changes ) { + static $i = 0; + return [ + 'primaryKey' => [ 'event_id' => $i++ ], + 'changes' => $changes, + ]; + } + + public function testReaderBasicIterate() { + $batchSize = 2; + $response = $this->genSelectResult( $batchSize, /*numRows*/ 5, function () { + static $i = 0; + return [ 'id_field' => ++$i ]; + } ); + $db = $this->mockDbConsecutiveSelect( $response ); + $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize ); + + $pos = 0; + foreach ( $reader as $rows ) { + $this->assertEquals( $response[$pos], $rows, "Testing row in position $pos" ); + $pos++; + } + // -1 is because the final array() marks the end and isnt included + $this->assertEquals( count( $response ) - 1, $pos ); + } + + public static function provider_readerGetPrimaryKey() { + $row = [ + 'id_field' => 42, + 'some_col' => 'dvorak', + 'other_col' => 'samurai', + ]; + return [ + + [ + 'Must return single column pk when requested', + [ 'id_field' => 42 ], + $row + ], + + [ + 'Must return multiple column pks when requested', + [ 'id_field' => 42, 'other_col' => 'samurai' ], + $row + ], + + ]; + } + + /** + * @dataProvider provider_readerGetPrimaryKey + */ + public function testReaderGetPrimaryKey( $message, array $expected, array $row ) { + $reader = new BatchRowIterator( $this->mockDb(), 'some_table', array_keys( $expected ), 8675309 ); + $this->assertEquals( $expected, $reader->extractPrimaryKeys( (object)$row ), $message ); + } + + public static function provider_readerSetFetchColumns() { + return [ + + [ + 'Must merge primary keys into select conditions', + // Expected column select + [ 'foo', 'bar' ], + // primary keys + [ 'foo' ], + // setFetchColumn + [ 'bar' ] + ], + + [ + 'Must not merge primary keys into the all columns selector', + // Expected column select + [ '*' ], + // primary keys + [ 'foo' ], + // setFetchColumn + [ '*' ], + ], + + [ + 'Must not duplicate primary keys into column selector', + // Expected column select. + // TODO: figure out how to only assert the array_values portion and not the keys + [ 0 => 'foo', 1 => 'bar', 3 => 'baz' ], + // primary keys + [ 'foo', 'bar', ], + // setFetchColumn + [ 'bar', 'baz' ], + ], + ]; + } + + /** + * @dataProvider provider_readerSetFetchColumns + */ + public function testReaderSetFetchColumns( + $message, array $columns, array $primaryKeys, array $fetchColumns + ) { + $db = $this->mockDb( [ 'select' ] ); + $db->expects( $this->once() ) + ->method( 'select' ) + // only testing second parameter of Database::select + ->with( 'some_table', $columns ) + ->will( $this->returnValue( new ArrayIterator( [] ) ) ); + + $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, 22 ); + $reader->setFetchColumns( $fetchColumns ); + // triggers first database select + $reader->rewind(); + } + + public static function provider_readerSelectConditions() { + return [ + + [ + "With single primary key must generate id > 'value'", + // Expected second iteration + [ "( id_field > '3' )" ], + // Primary key(s) + 'id_field', + ], + + [ + 'With multiple primary keys the first conditions ' . + 'must use >= and the final condition must use >', + // Expected second iteration + [ "( id_field = '3' AND foo > '103' ) OR ( id_field > '3' )" ], + // Primary key(s) + [ 'id_field', 'foo' ], + ], + + ]; + } + + /** + * Slightly hackish to use reflection, but asserting different parameters + * to consecutive calls of Database::select in phpunit is error prone + * + * @dataProvider provider_readerSelectConditions + */ + public function testReaderSelectConditionsMultiplePrimaryKeys( + $message, $expectedSecondIteration, $primaryKeys, $batchSize = 3 + ) { + $results = $this->genSelectResult( $batchSize, $batchSize * 3, function () { + static $i = 0, $j = 100, $k = 1000; + return [ 'id_field' => ++$i, 'foo' => ++$j, 'bar' => ++$k ]; + } ); + $db = $this->mockDbConsecutiveSelect( $results ); + + $conditions = [ 'bar' => 42, 'baz' => 'hai' ]; + $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, $batchSize ); + $reader->addConditions( $conditions ); + + $buildConditions = new ReflectionMethod( $reader, 'buildConditions' ); + $buildConditions->setAccessible( true ); + + // On first iteration only the passed conditions must be used + $this->assertEquals( $conditions, $buildConditions->invoke( $reader ), + 'First iteration must return only the conditions passed in addConditions' ); + $reader->rewind(); + + // Second iteration must use the maximum primary key of last set + $this->assertEquals( + $conditions + $expectedSecondIteration, + $buildConditions->invoke( $reader ), + $message + ); + } + + protected function mockDbConsecutiveSelect( array $retvals ) { + $db = $this->mockDb( [ 'select', 'addQuotes' ] ); + $db->expects( $this->any() ) + ->method( 'select' ) + ->will( $this->consecutivelyReturnFromSelect( $retvals ) ); + $db->expects( $this->any() ) + ->method( 'addQuotes' ) + ->will( $this->returnCallback( function ( $value ) { + return "'$value'"; // not real quoting: doesn't matter in test + } ) ); + + return $db; + } + + protected function consecutivelyReturnFromSelect( array $results ) { + $retvals = []; + foreach ( $results as $rows ) { + // The Database::select method returns iterators, so we do too. + $retvals[] = $this->returnValue( new ArrayIterator( $rows ) ); + } + + return call_user_func_array( [ $this, 'onConsecutiveCalls' ], $retvals ); + } + + protected function genSelectResult( $batchSize, $numRows, $rowGenerator ) { + $res = []; + for ( $i = 0; $i < $numRows; $i += $batchSize ) { + $rows = []; + for ( $j = 0; $j < $batchSize && $i + $j < $numRows; $j++ ) { + $rows [] = (object)call_user_func( $rowGenerator ); + } + $res[] = $rows; + } + $res[] = []; // termination condition requires empty result for last row + return $res; + } + + protected function mockDb( $methods = [] ) { + // @TODO: mock from Database + // FIXME: the constructor normally sets mAtomicLevels and mSrvCache + $databaseMysql = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( array_merge( [ 'isOpen', 'getApproximateLagStatus' ], $methods ) ) + ->getMock(); + $databaseMysql->expects( $this->any() ) + ->method( 'isOpen' ) + ->will( $this->returnValue( true ) ); + $databaseMysql->expects( $this->any() ) + ->method( 'getApproximateLagStatus' ) + ->will( $this->returnValue( [ 'lag' => 0, 'since' => 0 ] ) ); + return $databaseMysql; + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php b/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php new file mode 100644 index 00000000..9e5163f9 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php @@ -0,0 +1,56 @@ +<?php + +/** + * @covers ClassCollector + */ +class ClassCollectorTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public static function provideCases() { + return [ + [ + "class Foo {}", + [ 'Foo' ], + ], + [ + "namespace Example;\nclass Foo {}\nclass Bar {}", + [ 'Example\Foo', 'Example\Bar' ], + ], + [ + "class_alias( 'Foo', 'Bar' );", + [ 'Bar' ], + ], + [ + "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Foo' );", + [ 'Example\Foo', 'Foo' ], + ], + [ + "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Bar' );", + [ 'Example\Foo', 'Bar' ], + ], + [ + "class_alias( Foo::class, 'Bar' );", + [ 'Bar' ], + ], + [ + // Namespaced class is not currently supported. Must use namespace declaration + // earlier in the file. + "class_alias( Example\Foo::class, 'Bar' );", + [], + ], + [ + "namespace Example;\nclass Foo {}\nclass_alias( Foo::class, 'Bar' );", + [ 'Example\Foo', 'Bar' ], + ], + ]; + } + + /** + * @dataProvider provideCases + */ + public function testGetClasses( $code, array $classes, $message = null ) { + $cc = new ClassCollector(); + $this->assertEquals( $classes, $cc->getClasses( "<?php\n$code" ), $message ); + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php b/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php new file mode 100644 index 00000000..316d9f42 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php @@ -0,0 +1,57 @@ +<?php + +/** + * @covers FileContentsHasherTest + */ +class FileContentsHasherTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function provideSingleFile() { + return array_map( function ( $file ) { + return [ $file, file_get_contents( $file ) ]; + }, glob( __DIR__ . '/../../data/filecontentshasher/*.*' ) ); + } + + public function provideMultipleFiles() { + return [ + [ $this->provideSingleFile() ] + ]; + } + + /** + * @covers FileContentsHasher::getFileContentsHash + * @covers FileContentsHasher::getFileContentsHashInternal + * @dataProvider provideSingleFile + */ + public function testSingleFileHash( $fileName, $contents ) { + foreach ( [ 'md4', 'md5' ] as $algo ) { + $expectedHash = hash( $algo, $contents ); + $actualHash = FileContentsHasher::getFileContentsHash( $fileName, $algo ); + $this->assertEquals( $expectedHash, $actualHash ); + $actualHashRepeat = FileContentsHasher::getFileContentsHash( $fileName, $algo ); + $this->assertEquals( $expectedHash, $actualHashRepeat ); + } + } + + /** + * @covers FileContentsHasher::getFileContentsHash + * @covers FileContentsHasher::getFileContentsHashInternal + * @dataProvider provideMultipleFiles + */ + public function testMultipleFileHash( $files ) { + $fileNames = []; + $hashes = []; + foreach ( $files as $fileInfo ) { + list( $fileName, $contents ) = $fileInfo; + $fileNames[] = $fileName; + $hashes[] = md5( $contents ); + } + + $expectedHash = md5( implode( '', $hashes ) ); + $actualHash = FileContentsHasher::getFileContentsHash( $fileNames, 'md5' ); + $this->assertEquals( $expectedHash, $actualHash ); + $actualHashRepeat = FileContentsHasher::getFileContentsHash( $fileNames, 'md5' ); + $this->assertEquals( $expectedHash, $actualHashRepeat ); + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php b/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php new file mode 100644 index 00000000..05a33c5a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php @@ -0,0 +1,98 @@ +<?php +/** + * @group HKDF + * @covers CryptHKDF + * @covers MWCryptHKDF + */ +class MWCryptHKDFTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( 'wgSecretKey', '5bf1945342e67799cb50704a7fa19ac6' ); + } + + /** + * Test basic usage works + */ + public function testGenerate() { + $a = MWCryptHKDF::generateHex( 64 ); + $b = MWCryptHKDF::generateHex( 64 ); + + $this->assertTrue( strlen( $a ) == 64, "MWCryptHKDF produced fewer bytes than expected" ); + $this->assertTrue( strlen( $b ) == 64, "MWCryptHKDF produced fewer bytes than expected" ); + $this->assertFalse( $a == $b, "Two runs of MWCryptHKDF produced the same result." ); + } + + /** + * @dataProvider providerRfc5869 + */ + public function testRfc5869( $hash, $ikm, $salt, $info, $L, $prk, $okm ) { + $ikm = hex2bin( $ikm ); + $salt = hex2bin( $salt ); + $info = hex2bin( $info ); + $okm = hex2bin( $okm ); + $result = MWCryptHKDF::HKDF( $hash, $ikm, $salt, $info, $L ); + $this->assertEquals( $okm, $result ); + } + + /** + * Test vectors from Appendix A on https://tools.ietf.org/html/rfc5869 + */ + public static function providerRfc5869() { + // phpcs:disable Generic.Files.LineLength + return [ + // A.1 + [ + 'sha256', + '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', // ikm + '000102030405060708090a0b0c', // salt + 'f0f1f2f3f4f5f6f7f8f9', // context + 42, // bytes + '077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5', // prk + '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865' // okm + ], + // A.2 + [ + 'sha256', + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f', + '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf', + 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', + 82, + '06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244', + 'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87' + ], + // A.3 + [ + 'sha256', + '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', // ikm + '', // salt + '', // context + 42, // bytes + '19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04', // prk + '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8' // okm + ], + // A.4 + [ + 'sha1', + '0b0b0b0b0b0b0b0b0b0b0b', // ikm + '000102030405060708090a0b0c', // salt + 'f0f1f2f3f4f5f6f7f8f9', // context + 42, // bytes + '9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243', // prk + '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896' // okm + ], + // A.5 + [ + 'sha1', + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f', // ikm + '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf', // salt + 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', // context + 82, // bytes + '8adae09a2a307059478d309b26c4115a224cfaf6', // prk + '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4' // okm + ], + ]; + // phpcs:enable + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php b/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php new file mode 100644 index 00000000..94705bff --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php @@ -0,0 +1,64 @@ +<?php + +/** + * @group Hash + * + * @covers MWCryptHash + */ +class MWCryptHashTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function testHashLength() { + if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) { + $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' ); + } + + $this->assertEquals( 64, MWCryptHash::hashLength(), 'Raw hash length' ); + $this->assertEquals( 128, MWCryptHash::hashLength( false ), 'Hex hash length' ); + } + + public function testHash() { + if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) { + $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' ); + } + + $data = 'foobar'; + // phpcs:ignore Generic.Files.LineLength + $hash = '9923afaec3a86f865bb231a588f453f84e8151a2deb4109aebc6de4284be5bebcff4fab82a7e51d920237340a043736e9d13bab196006dcca0fe65314d68eab9'; + + $this->assertEquals( + hex2bin( $hash ), + MWCryptHash::hash( $data ), + 'Raw hash' + ); + $this->assertEquals( + $hash, + MWCryptHash::hash( $data, false ), + 'Hex hash' + ); + } + + public function testHmac() { + if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) { + $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' ); + } + + $data = 'foobar'; + $key = 'secret'; + // phpcs:ignore Generic.Files.LineLength + $hash = 'ddc94177b2020e55ce2049199fd9cc6327f416ff6dc621cc34cb43d9bec61d73372b4790c0e24957f565ecaf2d42821e6303619093e99cbe14a3b9250bda5f81'; + + $this->assertEquals( + hex2bin( $hash ), + MWCryptHash::hmac( $data, $key ), + 'Raw hmac' + ); + $this->assertEquals( + $hash, + MWCryptHash::hmac( $data, $key, false ), + 'Hex hmac' + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/utils/MWGrantsTest.php b/www/wiki/tests/phpunit/includes/utils/MWGrantsTest.php new file mode 100644 index 00000000..eae9c15d --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/MWGrantsTest.php @@ -0,0 +1,117 @@ +<?php +class MWGrantsTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + 'wgGrantPermissions' => [ + 'hidden1' => [ 'read' => true, 'autoconfirmed' => false ], + 'hidden2' => [ 'autoconfirmed' => true ], + 'normal' => [ 'edit' => true ], + 'normal2' => [ 'edit' => true, 'create' => true ], + 'admin' => [ 'protect' => true, 'delete' => true ], + ], + 'wgGrantPermissionGroups' => [ + 'hidden1' => 'hidden', + 'hidden2' => 'hidden', + 'normal' => 'normal-group', + 'admin' => 'admin', + ], + ] ); + } + + /** + * @covers MWGrants::getValidGrants + */ + public function testGetValidGrants() { + $this->assertSame( + [ 'hidden1', 'hidden2', 'normal', 'normal2', 'admin' ], + MWGrants::getValidGrants() + ); + } + + /** + * @covers MWGrants::getRightsByGrant + */ + public function testGetRightsByGrant() { + $this->assertSame( + [ + 'hidden1' => [ 'read' ], + 'hidden2' => [ 'autoconfirmed' ], + 'normal' => [ 'edit' ], + 'normal2' => [ 'edit', 'create' ], + 'admin' => [ 'protect', 'delete' ], + ], + MWGrants::getRightsByGrant() + ); + } + + /** + * @dataProvider provideGetGrantRights + * @covers MWGrants::getGrantRights + * @param array|string $grants + * @param array $rights + */ + public function testGetGrantRights( $grants, $rights ) { + $this->assertSame( $rights, MWGrants::getGrantRights( $grants ) ); + } + + public static function provideGetGrantRights() { + return [ + [ 'hidden1', [ 'read' ] ], + [ [ 'hidden1', 'hidden2', 'hidden3' ], [ 'read', 'autoconfirmed' ] ], + [ [ 'normal1', 'normal2' ], [ 'edit', 'create' ] ], + ]; + } + + /** + * @dataProvider provideGrantsAreValid + * @covers MWGrants::grantsAreValid + * @param array $grants + * @param bool $valid + */ + public function testGrantsAreValid( $grants, $valid ) { + $this->assertSame( $valid, MWGrants::grantsAreValid( $grants ) ); + } + + public static function provideGrantsAreValid() { + return [ + [ [ 'hidden1', 'hidden2' ], true ], + [ [ 'hidden1', 'hidden3' ], false ], + ]; + } + + /** + * @dataProvider provideGetGrantGroups + * @covers MWGrants::getGrantGroups + * @param array|null $grants + * @param array $expect + */ + public function testGetGrantGroups( $grants, $expect ) { + $this->assertSame( $expect, MWGrants::getGrantGroups( $grants ) ); + } + + public static function provideGetGrantGroups() { + return [ + [ null, [ + 'hidden' => [ 'hidden1', 'hidden2' ], + 'normal-group' => [ 'normal' ], + 'other' => [ 'normal2' ], + 'admin' => [ 'admin' ], + ] ], + [ [ 'hidden1', 'normal' ], [ + 'hidden' => [ 'hidden1' ], + 'normal-group' => [ 'normal' ], + ] ], + ]; + } + + /** + * @covers MWGrants::getHiddenGrants + */ + public function testGetHiddenGrants() { + $this->assertSame( [ 'hidden1', 'hidden2' ], MWGrants::getHiddenGrants() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php b/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php new file mode 100644 index 00000000..abdfbb14 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php @@ -0,0 +1,217 @@ +<?php +class MWRestrictionsTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + protected static $restrictionsForChecks; + + public static function setUpBeforeClass() { + self::$restrictionsForChecks = MWRestrictions::newFromArray( [ + 'IPAddresses' => [ + '10.0.0.0/8', + '172.16.0.0/12', + '2001:db8::/33', + ] + ] ); + } + + /** + * @covers MWRestrictions::newDefault + * @covers MWRestrictions::__construct + */ + public function testNewDefault() { + $ret = MWRestrictions::newDefault(); + $this->assertInstanceOf( MWRestrictions::class, $ret ); + $this->assertSame( + '{"IPAddresses":["0.0.0.0/0","::/0"]}', + $ret->toJson() + ); + } + + /** + * @covers MWRestrictions::newFromArray + * @covers MWRestrictions::__construct + * @covers MWRestrictions::loadFromArray + * @covers MWRestrictions::toArray + * @dataProvider provideArray + * @param array $data + * @param bool|InvalidArgumentException $expect True if the call succeeds, + * otherwise the exception that should be thrown. + */ + public function testArray( $data, $expect ) { + if ( $expect === true ) { + $ret = MWRestrictions::newFromArray( $data ); + $this->assertInstanceOf( MWRestrictions::class, $ret ); + $this->assertSame( $data, $ret->toArray() ); + } else { + try { + MWRestrictions::newFromArray( $data ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertEquals( $expect, $ex ); + } + } + } + + public static function provideArray() { + return [ + [ [ 'IPAddresses' => [] ], true ], + [ [ 'IPAddresses' => [ '127.0.0.1/32' ] ], true ], + [ + [ 'IPAddresses' => [ '256.0.0.1/32' ] ], + new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' ) + ], + [ + [ 'IPAddresses' => '127.0.0.1/32' ], + new InvalidArgumentException( 'IPAddresses is not an array' ) + ], + [ + [], + new InvalidArgumentException( 'Array is missing required keys: IPAddresses' ) + ], + [ + [ 'foo' => 'bar', 'bar' => 42 ], + new InvalidArgumentException( 'Array contains invalid keys: foo, bar' ) + ], + ]; + } + + /** + * @covers MWRestrictions::newFromJson + * @covers MWRestrictions::__construct + * @covers MWRestrictions::loadFromArray + * @covers MWRestrictions::toJson + * @covers MWRestrictions::__toString + * @dataProvider provideJson + * @param string $json + * @param array|InvalidArgumentException $expect + */ + public function testJson( $json, $expect ) { + if ( is_array( $expect ) ) { + $ret = MWRestrictions::newFromJson( $json ); + $this->assertInstanceOf( MWRestrictions::class, $ret ); + $this->assertSame( $expect, $ret->toArray() ); + + $this->assertSame( $json, $ret->toJson( false ) ); + $this->assertSame( $json, (string)$ret ); + + $this->assertSame( + FormatJson::encode( $expect, true, FormatJson::ALL_OK ), + $ret->toJson( true ) + ); + } else { + try { + MWRestrictions::newFromJson( $json ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertTrue( true ); + } + } + } + + public static function provideJson() { + return [ + [ + '{"IPAddresses":[]}', + [ 'IPAddresses' => [] ] + ], + [ + '{"IPAddresses":["127.0.0.1/32"]}', + [ 'IPAddresses' => [ '127.0.0.1/32' ] ] + ], + [ + '{"IPAddresses":["256.0.0.1/32"]}', + new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' ) + ], + [ + '{"IPAddresses":"127.0.0.1/32"}', + new InvalidArgumentException( 'IPAddresses is not an array' ) + ], + [ + '{}', + new InvalidArgumentException( 'Array is missing required keys: IPAddresses' ) + ], + [ + '{"foo":"bar","bar":42}', + new InvalidArgumentException( 'Array contains invalid keys: foo, bar' ) + ], + [ + '{"IPAddresses":[]', + new InvalidArgumentException( 'Invalid restrictions JSON' ) + ], + [ + '"IPAddresses"', + new InvalidArgumentException( 'Invalid restrictions JSON' ) + ], + ]; + } + + /** + * @covers MWRestrictions::checkIP + * @dataProvider provideCheckIP + * @param string $ip + * @param bool $pass + */ + public function testCheckIP( $ip, $pass ) { + $this->assertSame( $pass, self::$restrictionsForChecks->checkIP( $ip ) ); + } + + public static function provideCheckIP() { + return [ + [ '10.0.0.1', true ], + [ '172.16.0.0', true ], + [ '192.0.2.1', false ], + [ '2001:db8:1::', true ], + [ '2001:0db8:0000:0000:0000:0000:0000:0000', true ], + [ '2001:0DB8:8000::', false ], + ]; + } + + /** + * @covers MWRestrictions::check + * @dataProvider provideCheck + * @param WebRequest $request + * @param Status $expect + */ + public function testCheck( $request, $expect ) { + $this->assertEquals( $expect, self::$restrictionsForChecks->check( $request ) ); + } + + public function provideCheck() { + $ret = []; + + $mockBuilder = $this->getMockBuilder( FauxRequest::class ) + ->setMethods( [ 'getIP' ] ); + + foreach ( self::provideCheckIP() as $checkIP ) { + $ok = []; + $request = $mockBuilder->getMock(); + + $request->expects( $this->any() )->method( 'getIP' ) + ->will( $this->returnValue( $checkIP[0] ) ); + $ok['ip'] = $checkIP[1]; + + /* If we ever add more restrictions, add nested for loops here: + * foreach ( self::provideCheckFoo() as $checkFoo ) { + * $request->expects( $this->any() )->method( 'getFoo' ) + * ->will( $this->returnValue( $checkFoo[0] ); + * $ok['foo'] = $checkFoo[1]; + * + * foreach ( self::provideCheckBar() as $checkBar ) { + * $request->expects( $this->any() )->method( 'getBar' ) + * ->will( $this->returnValue( $checkBar[0] ); + * $ok['bar'] = $checkBar[1]; + * + * // etc. + * } + * } + */ + + $status = Status::newGood(); + $status->setResult( $ok === array_filter( $ok ), $ok ); + $ret[] = [ $request, $status ]; + } + + return $ret; + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php b/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php new file mode 100644 index 00000000..d335a93a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php @@ -0,0 +1,173 @@ +<?php + +class UIDGeneratorTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + protected function tearDown() { + // Bug: 44850 + UIDGenerator::unitTestTearDown(); + parent::tearDown(); + } + + /** + * Test that generated UIDs have the expected properties + * + * @dataProvider provider_testTimestampedUID + * @covers UIDGenerator::newTimestampedUID128 + * @covers UIDGenerator::newTimestampedUID88 + */ + public function testTimestampedUID( $method, $digitlen, $bits, $tbits, $hostbits ) { + $id = call_user_func( [ UIDGenerator::class, $method ] ); + $this->assertEquals( true, ctype_digit( $id ), "UID made of digit characters" ); + $this->assertLessThanOrEqual( $digitlen, strlen( $id ), + "UID has the right number of digits" ); + $this->assertLessThanOrEqual( $bits, strlen( Wikimedia\base_convert( $id, 10, 2 ) ), + "UID has the right number of bits" ); + + $ids = []; + for ( $i = 0; $i < 300; $i++ ) { + $ids[] = call_user_func( [ UIDGenerator::class, $method ] ); + } + + $lastId = array_shift( $ids ); + + $this->assertSame( array_unique( $ids ), $ids, "All generated IDs are unique." ); + + foreach ( $ids as $id ) { + // Convert string to binary and pad to full length so we can + // extract segments + $id_bin = Wikimedia\base_convert( $id, 10, 2, $bits ); + $lastId_bin = Wikimedia\base_convert( $lastId, 10, 2, $bits ); + + $timestamp_bin = substr( $id_bin, 0, $tbits ); + $last_timestamp_bin = substr( $lastId_bin, 0, $tbits ); + + $this->assertGreaterThanOrEqual( + $last_timestamp_bin, + $timestamp_bin, + "timestamp ($timestamp_bin) of current ID ($id_bin) >= timestamp ($last_timestamp_bin) " . + "of prior one ($lastId_bin)" ); + + $hostbits_bin = substr( $id_bin, -$hostbits ); + $last_hostbits_bin = substr( $lastId_bin, -$hostbits ); + + if ( $hostbits ) { + $this->assertEquals( + $hostbits_bin, + $last_hostbits_bin, + "Host ID ($hostbits_bin) of current ID ($id_bin) is same as host ID ($last_hostbits_bin) " . + "of prior one ($lastId_bin)." ); + } + + $lastId = $id; + } + } + + /** + * array( method, length, bits, hostbits ) + * NOTE: When adding a new method name here please update the covers tags for the tests! + */ + public static function provider_testTimestampedUID() { + return [ + [ 'newTimestampedUID128', 39, 128, 46, 48 ], + [ 'newTimestampedUID128', 39, 128, 46, 48 ], + [ 'newTimestampedUID88', 27, 88, 46, 32 ], + ]; + } + + /** + * @covers UIDGenerator::newUUIDv1 + */ + public function testUUIDv1() { + $ids = []; + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newUUIDv1(); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ), + "UID $id has the right format" ); + $ids[] = $id; + + $id = UIDGenerator::newRawUUIDv1(); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{12}1[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ), + "UID $id has the right format" ); + + $id = UIDGenerator::newRawUUIDv1(); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{12}1[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ), + "UID $id has the right format" ); + } + + $this->assertEquals( array_unique( $ids ), $ids, "All generated IDs are unique." ); + } + + /** + * @covers UIDGenerator::newUUIDv4 + */ + public function testUUIDv4() { + $ids = []; + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newUUIDv4(); + $ids[] = $id; + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ), + "UID $id has the right format" ); + } + + $this->assertEquals( array_unique( $ids ), $ids, 'All generated IDs are unique.' ); + } + + /** + * @covers UIDGenerator::newRawUUIDv4 + */ + public function testRawUUIDv4() { + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newRawUUIDv4(); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ), + "UID $id has the right format" ); + } + } + + /** + * @covers UIDGenerator::newRawUUIDv4 + */ + public function testRawUUIDv4QuickRand() { + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND ); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ), + "UID $id has the right format" ); + } + } + + /** + * @covers UIDGenerator::newSequentialPerNodeID + */ + public function testNewSequentialID() { + $id1 = UIDGenerator::newSequentialPerNodeID( 'test', 32 ); + $id2 = UIDGenerator::newSequentialPerNodeID( 'test', 32 ); + + $this->assertInternalType( 'float', $id1, "ID returned as float" ); + $this->assertInternalType( 'float', $id2, "ID returned as float" ); + $this->assertGreaterThan( 0, $id1, "ID greater than 1" ); + $this->assertGreaterThan( $id1, $id2, "IDs increasing in value" ); + } + + /** + * @covers UIDGenerator::newSequentialPerNodeIDs + */ + public function testNewSequentialIDs() { + $ids = UIDGenerator::newSequentialPerNodeIDs( 'test', 32, 5 ); + $lastId = null; + foreach ( $ids as $id ) { + $this->assertInternalType( 'float', $id, "ID returned as float" ); + $this->assertGreaterThan( 0, $id, "ID greater than 1" ); + if ( $lastId ) { + $this->assertGreaterThan( $lastId, $id, "IDs increasing in value" ); + } + $lastId = $id; + } + } +} diff --git a/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php new file mode 100644 index 00000000..9f18e5af --- /dev/null +++ b/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php @@ -0,0 +1,87 @@ +<?php + +/** + * @covers ZipDirectoryReader + * NOTE: this test is more like an integration test than a unit test + */ +class ZipDirectoryReaderTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + protected $zipDir; + protected $entries; + + protected function setUp() { + parent::setUp(); + $this->zipDir = __DIR__ . '/../../data/zip'; + } + + function zipCallback( $entry ) { + $this->entries[] = $entry; + } + + function readZipAssertError( $file, $error, $assertMessage ) { + $this->entries = []; + $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] ); + $this->assertTrue( $status->hasMessage( $error ), $assertMessage ); + } + + function readZipAssertSuccess( $file, $assertMessage ) { + $this->entries = []; + $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] ); + $this->assertTrue( $status->isOK(), $assertMessage ); + } + + public function testEmpty() { + $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' ); + } + + public function testMultiDisk0() { + $this->readZipAssertError( 'split.zip', 'zip-unsupported', + 'Split zip error' ); + } + + public function testNoSignature() { + $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format', + 'No signature should give "wrong format" error' ); + } + + public function testSimple() { + $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' ); + $this->assertEquals( $this->entries, [ [ + 'name' => 'Class.class', + 'mtime' => '20010115000000', + 'size' => 1, + ] ] ); + } + + public function testBadCentralEntrySignature() { + $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad', + 'Bad central entry error' ); + } + + public function testTrailingBytes() { + $this->readZipAssertError( 'trail.zip', 'zip-bad', + 'Trailing bytes error' ); + } + + public function testWrongCDStart() { + $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported', + 'Wrong CD start disk error' ); + } + + public function testCentralDirectoryGap() { + $this->readZipAssertError( 'cd-gap.zip', 'zip-bad', + 'CD gap error' ); + } + + public function testCentralDirectoryTruncated() { + $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad', + 'CD truncated error (should hit unpack() overrun)' ); + } + + public function testLooksLikeZip64() { + $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported', + 'A file which looks like ZIP64 but isn\'t, should give error' ); + } +} |