wiki.techinc.nl/tests/phpunit/unit/includes/Permissions/RestrictionStoreTest.php
Amir Sarabadani d9e00a3ead RestrictionStore: Migrate Database::select usages to SelectQueryBuilder
Bug: T311866
Change-Id: I420d3904cbda6997b63bd0b2fce47926d94811d8
2022-09-30 17:48:07 +02:00

991 lines
32 KiB
PHP

<?php
namespace MediaWiki\Tests\Unit\Permissions;
use DatabaseTestHelper;
use LinkCache;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Linker\LinksMigration;
use MediaWiki\MainConfigNames;
use MediaWiki\Page\PageIdentity;
use MediaWiki\Page\PageIdentityValue;
use MediaWiki\Page\PageReferenceValue;
use MediaWiki\Page\PageStore;
use MediaWiki\Permissions\RestrictionStore;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWikiUnitTestCase;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use Title;
use UnexpectedValueException;
use WANObjectCache;
use Wikimedia\Assert\PreconditionException;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\SelectQueryBuilder;
/**
* @coversDefaultClass \MediaWiki\Permissions\RestrictionStore
*/
class RestrictionStoreTest extends MediaWikiUnitTestCase {
use DummyServicesTrait;
private const DEFAULT_RESTRICTION_TYPES = [ 'create', 'edit', 'move', 'upload' ];
/**
* @param array $expectedCalls E.g.:
* [
* DB_REPLICA => [
* 'update' => callback, # to be called exactly once
* 'delete' => [ callback, 2 ], # to be called exactly twice
* 'select' => [ callback, -1 ], # may be called 0 or more times
* ],
* ]
* @return ILoadBalancer
*/
private function newMockLoadBalancer( array $expectedCalls = [] ): ILoadBalancer {
if ( !isset( $expectedCalls[DB_REPLICA] ) ) {
$expectedCalls[DB_REPLICA] = [];
}
$expectedCalls[DB_REPLICA]['decodeExpiry'] = [
static function ( string $expiry ): string {
return $expiry;
},
-1
];
$dbs = [];
foreach ( $expectedCalls as $index => $calls ) {
$dbs[$index] = $this->createNoOpMock(
IDatabase::class,
array_merge( array_keys( $calls ), [ 'newSelectQueryBuilder' ] )
);
foreach ( $calls as $method => $callback ) {
$count = 1;
if ( is_array( $callback ) ) {
[ $callback, $count ] = $callback;
}
$dbs[$index]->expects( $count < 0 ? $this->any() : $this->exactly( $count ) )
->method( $method )->willReturnCallback( $callback );
}
$dbs[$index]
->method( 'newSelectQueryBuilder' )
->willReturnCallback( static function () use ( $dbs, $index ) {
return new SelectQueryBuilder( $dbs[$index] );
} );
}
$lb = $this->createMock( ILoadBalancer::class, [ 'getConnectionRef' ] );
$lb->method( 'getConnectionRef' )->willReturnCallback(
function ( int $index ) use ( $dbs ): IDatabase {
$this->assertArrayHasKey( $index, $dbs );
return $dbs[$index];
}
);
return $lb;
}
private function newRestrictionStore( array $options = [] ): RestrictionStore {
return new RestrictionStore(
new ServiceOptions( RestrictionStore::CONSTRUCTOR_OPTIONS, $options + [
MainConfigNames::NamespaceProtection => [],
MainConfigNames::RestrictionLevels => [ '', 'autoconfirmed', 'sysop' ],
MainConfigNames::RestrictionTypes => self::DEFAULT_RESTRICTION_TYPES,
MainConfigNames::SemiprotectedRestrictionLevels => [ 'autoconfirmed' ],
] ),
$this->createNoOpMock( WANObjectCache::class ),
$this->newMockLoadBalancer( $options['db'] ?? [] ),
// @todo test that these calls work correctly
$this->createNoOpMock( LinkCache::class, [ 'addLinkObj', 'getGoodLinkFieldObj' ] ),
$this->createNoOpMock( LinksMigration::class, [ 'getLinksConditions' ] ),
$this->getDummyCommentStore(),
$this->createHookContainer( isset( $options['hookFn'] )
? [ 'TitleGetRestrictionTypes' => $options['hookFn'] ]
: [] ),
$this->createNoOpMock( PageStore::class )
);
}
private static function newImproperPageIdentity(
int $ns, string $dbKey, $wikiId = PageIdentity::LOCAL
): PageIdentity {
// PageIdentityValue doesn't allow negative namespaces, and Title::exists accesses services
// for hooks, so we need another solution to unit-test PageIdentity objects with negative
// namespaces (until they cease to exist).
return new class( $ns, $dbKey, $wikiId ) extends PageReferenceValue implements PageIdentity {
public function getId( $wikiId = PageIdentity::LOCAL ): int {
throw new RuntimeException;
}
public function canExist(): bool {
return false;
}
public function exists(): bool {
return false;
}
};
}
/**
* @covers ::<public>
* @dataProvider provideNonLocalPage
*/
public function testNonLocalPage( string $method, ...$extraArgs ) {
$this->expectException( PreconditionException::class );
$this->expectExceptionMessage( 'otherwiki' );
$obj = $this->newRestrictionStore();
$page = new PageIdentityValue( 1, NS_MAIN, 'X', 'otherwiki' );
$obj->$method( $page, ...$extraArgs );
}
public function provideNonLocalPage() {
// We programmatically get all public methods whose first parameter is a PageIdentity. This
// way we'll make sure to include any new methods that are added in the future.
$ret = [];
$methods = ( new ReflectionClass( RestrictionStore::class ) )
->getMethods( ReflectionMethod::IS_PUBLIC );
foreach ( $methods as $method ) {
$params = $method->getParameters();
if ( !$params[0]->hasType() || $params[0]->getType()->getName() !== PageIdentity::class ) {
continue;
}
$ret[$method->getName()] = [ $method->getName() ];
foreach ( array_slice( $params, 1 ) as $param ) {
// Extra required arguments
if ( $param->isOptional() ) {
break;
}
switch ( $param->getType()->getName() ) {
case 'string':
$ret[$method->getName()][] = 'x';
break;
case 'array':
$ret[$method->getName()][] = [];
break;
default:
throw new UnexpectedValueException(
"{$param->getType()->getName} type not supported" );
}
}
}
return $ret;
}
/**
* @covers ::getRestrictions
* @dataProvider provideGetRestrictions
*/
public function testGetRestrictions(
array $expected,
PageIdentity $page,
string $action,
?array $rowsToLoad,
array $options = []
): void {
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->getRestrictions( $page, $action ) );
}
public function provideGetRestrictions(): array {
$all = $this->provideGetAllRestrictions();
$ret = [];
foreach ( $all as $name => $arr ) {
[ $expected, $page ] = $arr;
$actions = array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'vaporize' ],
array_keys( $expected ) );
foreach ( $actions as $action ) {
$ret["$name ($action)"] =
array_merge(
[ $expected[$action] ?? [], $page, $action ],
array_slice( $arr, 2 )
);
}
}
return $ret;
}
/**
* @covers ::__construct
* @covers ::getAllRestrictions
* @covers ::loadRestrictions
* @covers ::loadRestrictionsFromRows
* @dataProvider provideGetAllRestrictions
*/
public function testGetAllRestrictions(
array $expected, PageIdentity $page, ?array $rowsToLoad, array $options = []
): void {
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->getAllRestrictions( $page ) );
}
public function provideGetAllRestrictions(): array {
return [
'Special page' => [
[],
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
null,
],
'Media page' => [
[],
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
null,
],
'Simple existing unprotected page' => [
[ 'edit' => [], 'move' => [] ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[],
],
'Simple existing protected page' => [
[ 'edit' => [ 'sysop', 'bureaucrat' ], 'move' => [ 'sysop' ] ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop,bureaucrat',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
],
],
'Protection type not allowed' => [
[ 'edit' => [ 'sysop' ] ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
],
[ MainConfigNames::RestrictionTypes => [ 'create', 'edit', 'upload' ] ],
],
'Expired protection' => [
[ 'edit' => [], 'move' => [ 'sysop' ] ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => '20200101000000', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
],
],
];
}
/**
* @covers ::getRestrictionExpiry
* @dataProvider provideGetRestrictionExpiry
*/
public function testGetRestrictionExpiry(
?string $expected,
PageIdentity $page,
string $action,
?array $rowsToLoad,
array $options = []
): void {
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->getRestrictionExpiry( $page, $action ) );
}
public function provideGetRestrictionExpiry(): array {
return [
'Special page' => [
null,
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
'edit',
null,
],
'Media page' => [
null,
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
'edit',
null,
],
'Simple existing unprotected page' => [
'infinity',
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'edit',
[],
],
'Simple existing protected page (edit)' => [
'20760101000000',
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'edit',
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => '20760101000000', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
],
],
'Simple existing protected page (move)' => [
'20670101000000',
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'move',
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => '20670101000000', 'pr_cascade' => 0 ],
],
],
'Simple existing protected page (unrecognized)' => [
null,
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'unrecognized',
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
],
],
'Simple existing expired protected page (edit)' => [
'infinity',
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'edit',
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => '20160101000000', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
],
],
'Simple existing expired protected page (move)' => [
'infinity',
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'move',
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => '20170101000000', 'pr_cascade' => 0 ],
],
],
'Simple existing expired protected page (unrecognized)' => [
null,
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
'unrecognized',
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => '20170101000000', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'unrecognized', 'pr_level' => 'sysop',
'pr_expiry' => '20170101000000', 'pr_cascade' => 0 ],
],
],
];
}
/**
* @covers ::getCreateProtection
* @covers ::getCreateProtectionInternal
* @dataProvider provideGetCreateProtection
*/
public function testGetCreateProtection(
?array $expected, PageIdentity $page, $return, array $options = []
): void {
if ( $page->canExist() && !$page->exists() ) {
$options['db'] = [ DB_REPLICA => [ 'selectRow' =>
function (
$table, $vars, $conds, string $fname, $options = [], $join_conds = []
) use ( $page, $return ) {
$options = (array)$options;
$options['LIMIT'] = 1;
$db = new DatabaseTestHelper( __CLASS__ );
$sql = trim( preg_replace( '/\s+/', ' ', $db->selectSQLText(
$table, $vars, $conds, $fname, $options, $join_conds ) ) );
$this->assertSame(
'SELECT pt_user,pt_expiry,pt_create_perm,comment_pt_reason.comment_text ' .
'AS pt_reason_text,comment_pt_reason.comment_data AS pt_reason_data,' .
'comment_pt_reason.comment_id AS pt_reason_cid ' .
'FROM protected_titles JOIN comment comment_pt_reason ON ' .
'((comment_pt_reason.comment_id = pt_reason_id)) ' .
"WHERE pt_namespace = {$page->getNamespace()} AND " .
"pt_title = '{$page->getDBkey()}' LIMIT 1",
$sql );
return is_array( $return ) ? (object)$return : $return;
}
] ];
}
$obj = $this->newRestrictionStore( $options );
$this->assertSame( $expected, $obj->getCreateProtection( $page ) );
}
public function provideGetCreateProtection(): array {
$ret = [
'Special page' => [ null, self::newImproperPageIdentity( NS_SPECIAL, 'X' ), null ],
'Media page' => [ null, self::newImproperPageIdentity( NS_MEDIA, 'X' ), null ],
'Existing page' => [ null, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ), null ],
'Unprotected' => [ null, PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ), false ],
];
$protectedTests = [
'sysop' => 'editprotected',
'autoconfirmed' => 'editsemiprotected',
'editprotected' => 'editprotected',
'editsemiprotected' => 'editsemiprotected',
'custom' => 'custom',
];
foreach ( $protectedTests as $db => $returned ) {
$ret["Protected ($db)"] = [
[
'user' => 123,
'expiry' => 'infinity',
'permission' => $returned,
'reason' => 'reason',
],
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
[
'pt_user' => 123,
'pt_expiry' => 'infinity',
'pt_create_perm' => $db,
'pt_reason_id' => 456,
'pt_reason_data' => '{}',
'pt_reason_text' => 'reason',
],
];
}
return $ret;
}
/**
* @covers ::deleteCreateProtection
* @dataProvider provideDeleteCreateProtection
*/
public function testDeleteCreateProtection( PageIdentity $page ): void {
$obj = $this->newRestrictionStore( [ 'db' => [ DB_PRIMARY => [ 'delete' =>
function ( string $table, array $where, string $method ) use ( $page ): bool {
$this->assertSame( 'protected_titles', $table );
$this->assertSame(
[ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
$where
);
return true;
}
] ] ] );
$obj->deleteCreateProtection( $page );
}
public function provideDeleteCreateProtection(): array {
return [
// Most of these don't actually make sense, but test current behavior regardless.
'Special page' => [ self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ],
'Media page' => [ self::newImproperPageIdentity( NS_MEDIA, 'X' ) ],
'Nonexistent page' => [ PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ) ],
'Existing page' => [ PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ) ],
];
}
/**
* @covers ::isSemiProtected
* @dataProvider provideIsSemiProtected
*/
public function testIsSemiProtected(
bool $expected, ?array $rowsToLoad, array $options = []
): void {
$page = $options['page'] ?? PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->isSemiProtected( $page, 'edit' ) );
}
public function provideIsSemiProtected(): array {
return [
'Special page' => [ false, null,
[ 'page' => self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ] ],
'Media page' => [ false, null,
[ 'page' => self::newImproperPageIdentity( NS_MEDIA, 'X' ) ] ],
'Unprotected page' => [
false,
[ (object)[ 'pr_type' => 'move', 'pr_level' => 'autoconfirmed',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Semiprotected page' => [
true,
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
],
],
'Fully protected page' => [
false,
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'autoconfirmed',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
],
],
'No semiprotection configured' => [
false,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ MainConfigNames::SemiprotectedRestrictionLevels => [] ],
],
'Config with editsemiprotected' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ MainConfigNames::SemiprotectedRestrictionLevels => [ 'editsemiprotected' ] ],
],
'Data with editsemiprotected' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'editsemiprotected',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Config and data with editsemiprotected' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'editsemiprotected',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ MainConfigNames::SemiprotectedRestrictionLevels => [ 'editsemiprotected' ] ],
],
'Semiprotection plus other protection level' => [
false,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed,superman',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ MainConfigNames::RestrictionLevels => [ '', 'autoconfirmed', 'sysop', 'superman' ] ],
],
'Two semiprotections' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed,superman',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ MainConfigNames::RestrictionLevels => [ '', 'autoconfirmed', 'sysop', 'superman' ],
MainConfigNames::SemiprotectedRestrictionLevels => [ 'autoconfirmed', 'superman' ] ],
],
];
}
/**
* @covers ::isProtected
* @dataProvider provideIsProtected
*/
public function testIsProtected(
bool $expected, ?array $rowsToLoad, array $options = []
): void {
$page = $options['page'] ?? PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
$action = $options['action'] ?? '';
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->isProtected( $page, $action ) );
}
public function provideIsProtected(): array {
return [
'Special page' => [ true, null,
[ 'page' => self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ] ],
'Media page' => [ false, null,
[ 'page' => self::newImproperPageIdentity( NS_MEDIA, 'X' ) ] ],
'Unprotected page' => [ false, [] ],
'Semiprotected page' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Fully protected page' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Protected against empty string' => [
false,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => '',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Unrecognized protection' => [
false,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'unrecognized',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Unrecognized plus recognized protection' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop,unrecognized',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
],
'Check unrecognized protection type' => [
false,
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
(object)[ 'pr_type' => 'unrecognized', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
],
[ 'action' => 'unrecognized' ],
],
'Check custom protection type' => [
true,
[ (object)[ 'pr_type' => 'custom', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ], ],
[ 'action' => 'custom', MainConfigNames::RestrictionTypes =>
array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'custom' ] ) ],
],
'Check custom protection level' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'custom',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ], ],
[ 'action' => 'edit', 'RestrictionLevels' =>
[ '', 'autoconfirmed', 'sysop', 'custom' ] ],
],
'Check edit protection of edit-protected page' => [
true,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ 'action' => 'edit' ],
],
'Check move protection of edit-protected page' => [
false,
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ 'action' => 'move' ],
],
'Check move protection of move-protected page' => [
true,
[ (object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ 'action' => 'move' ],
],
'Check edit protection of move-protected page' => [
false,
[ (object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
[ 'action' => 'edit' ],
],
'Check edit protection of edit- and move-protected page' => [
true,
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
],
[ 'action' => 'edit' ],
],
'Check move protection of edit- and move-protected page' => [
true,
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
],
[ 'action' => 'move' ],
],
];
}
/**
* @covers ::listApplicableRestrictionTypes
* @dataProvider provideListApplicableRestrictionTypes
*/
public function testListApplicableRestrictionTypes(
array $expected, PageIdentity $page, array $options = []
): void {
$obj = $this->newRestrictionStore( $options );
$this->assertSame( $expected, $obj->listApplicableRestrictionTypes( $page ) );
}
public function provideListApplicableRestrictionTypes(): array {
$expandedRestrictions = array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'liquify' ] );
return [
'Special page' => [
[],
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
],
'Media page' => [
[],
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
],
'Nonexistent page' => [
[ 'create' ],
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
],
'Existing page' => [
[ 'edit', 'move' ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
],
'Nonexistent file' => [
[ 'create' ],
PageIdentityValue::localIdentity( 0, NS_FILE, 'X' ),
],
'Existing file' => [
[ 'edit', 'move', 'upload' ],
PageIdentityValue::localIdentity( 1, NS_FILE, 'X' ),
],
'Nonexistent page with no create' => [
[],
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
[ MainConfigNames::RestrictionTypes => [ 'edit', 'move', 'upload' ] ],
],
'Existing page with no move' => [
[ 'edit' ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[ MainConfigNames::RestrictionTypes => [ 'create', 'edit', 'upload' ] ],
],
'Nonexistent file with no upload' => [
[ 'create' ],
PageIdentityValue::localIdentity( 0, NS_FILE, 'X' ),
[ MainConfigNames::RestrictionTypes => [ 'create', 'edit', 'move' ] ],
],
'Special page with extra type' => [
[],
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Media page with extra type' => [
[],
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Nonexistent page with extra type' => [
[ 'create' ],
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Existing page with extra type' => [
[ 'edit', 'move', 'liquify' ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Nonexistent file with extra type' => [
[ 'create' ],
PageIdentityValue::localIdentity( 0, NS_FILE, 'X' ),
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Existing file with extra type' => [
[ 'edit', 'move', 'upload', 'liquify' ],
PageIdentityValue::localIdentity( 1, NS_FILE, 'X' ),
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Hook' => [
[ 'move', 'liquify' ],
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[ 'hookFn' => function ( Title $title, array &$types ): bool {
self::assertEquals( Title::castFromPageIdentity(
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ) ), $title );
self::assertSame( [ 'edit', 'move' ], $types );
$types = [ 'move', 'liquify' ];
return false;
} ],
],
'Hook not run for special page' => [
[],
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
[ 'hookFn' => function () {
$this->fail( 'Should be unreached' );
} ],
],
'Hook not run for media page' => [
[],
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
[ 'hookFn' => function () {
$this->fail( 'Should be unreached' );
} ],
],
];
}
/**
* @covers ::listAllRestrictionTypes
* @dataProvider provideListAllRestrictionTypes
*/
public function testListAllRestrictionTypes(
array $expected, array $args, array $options = []
) {
$obj = $this->newRestrictionStore( $options );
$this->assertSame( $expected, $obj->listAllRestrictionTypes( ...$args ) );
}
public function provideListAllRestrictionTypes() {
$expandedRestrictions = array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'solidify' ] );
return [
'Exists' => [ [ 'edit', 'move', 'upload' ], [ true ] ],
'Default is exists' => [ [ 'edit', 'move', 'upload' ], [] ],
'Nonexistent' => [ [ 'create' ], [ false ] ],
'Exists with extra restriction type' => [
[ 'edit', 'move', 'upload', 'solidify' ],
[ true ],
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Default is exists with extra restriction type' => [
[ 'edit', 'move', 'upload', 'solidify' ],
[],
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Nonexistent with extra restriction type' => [
[ 'create' ],
[ false ],
[ MainConfigNames::RestrictionTypes => $expandedRestrictions ],
],
'Exists with no edit' => [
[ 'move', 'upload' ],
[ true ],
[ MainConfigNames::RestrictionTypes => [ 'create', 'move', 'upload' ] ],
],
'Exists with only create' => [
[],
[ true ],
[ MainConfigNames::RestrictionTypes => [ 'create' ] ],
],
'Nonexistent with no create' => [
[],
[ false ],
[ MainConfigNames::RestrictionTypes => [ 'edit', 'move', 'upload', 'solidify' ] ],
],
'Nonexistent with no upload' => [
[ 'create' ],
[ false ],
[ MainConfigNames::RestrictionTypes => [ 'create', 'edit', 'move', 'solidify' ] ],
],
'Nonexistent with no create or upload' => [
[],
[ false ],
[ MainConfigNames::RestrictionTypes => [ 'edit', 'move', 'solidify' ] ],
],
];
}
/**
* @covers ::areRestrictionsLoaded
* @covers ::loadRestrictionsFromRows
* @dataProvider provideAreRestrictionsLoaded
*/
public function testAreRestrictionsLoaded(
bool $expected, PageIdentity $page, ?array $rowsToLoad = null, array $options = []
): void {
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->areRestrictionsLoaded( $page ) );
}
public function provideAreRestrictionsLoaded(): array {
return [
'Special page' => [ false, self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ],
'Media page' => [ false, self::newImproperPageIdentity( NS_MEDIA, 'X' ) ],
'Regular page' => [ false, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ) ],
'Regular page with no restrictions' =>
[ true, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ), [] ],
'Regular page with restrictions' => [
true,
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ] ],
],
];
}
/**
* @covers ::areRestrictionsCascading
* @covers ::loadRestrictionsFromRows
* @dataProvider provideAreRestrictionsCascading
*/
public function testAreRestrictionsCascading(
bool $expected, PageIdentity $page, ?array $rowsToLoad, array $options = []
): void {
$obj = $this->newRestrictionStore( $options );
if ( is_array( $rowsToLoad ) ) {
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
}
$this->assertSame( $expected, $obj->areRestrictionsCascading( $page ) );
}
public function provideAreRestrictionsCascading(): array {
return [
'Special page' => [ false, self::newImproperPageIdentity( NS_SPECIAL, 'X' ), null ],
'Media page' => [ false, self::newImproperPageIdentity( NS_MEDIA, 'X' ), null ],
'Regular page with no restrictions' =>
[ false, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ), [] ],
'Regular page with restrictions' => [
false,
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ] ],
],
'Regular page with cascading restrictions' => [
true,
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 1 ] ],
],
'Regular page with some cascading restrictions and some not' => [
true,
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
[
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
'pr_expiry' => 'infinity', 'pr_cascade' => 1 ],
],
],
];
}
/**
* @covers ::flushRestrictions
*/
public function testFlushRestrictions(): void {
$obj = $this->newRestrictionStore();
$page = PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
$this->assertFalse( $obj->areRestrictionsLoaded( $page ) );
$obj->loadRestrictionsFromRows( $page, [] );
$this->assertTrue( $obj->areRestrictionsLoaded( $page ) );
$obj->flushRestrictions( $page );
$this->assertFalse( $obj->areRestrictionsLoaded( $page ) );
}
/**
* @covers ::getCascadeProtectionSources
* @covers ::getCascadeProtectionSourcesInternal
*/
public function testGetCascadeProtectionSources() {
$obj = $this->newRestrictionStore( [ 'db' => [ DB_REPLICA => [ 'select' =>
static function () {
return [
(object)[ 'pr_page' => 1, 'page_namespace' => NS_MAIN, 'page_title' => 'test',
'pr_expiry' => 'infinity', 'pr_type' => 'edit', 'pr_level' => 'Sysop' ]
];
}
] ] ] );
$page = PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
[ $sources, $restrictions ] = $obj->getCascadeProtectionSources( $page );
$this->assertCount( 1, $sources );
$this->assertArrayHasKey( 'edit', $restrictions );
}
}