In some tests mergeMwGlobalArrayValue should merge the empty array,
but when have settings in LocalSettings.php the result is not the empty
array as needed for the tests.
Just start with empty array to work on top of that.
1) ApiUserrightsTest::testAddAndRemoveGroups with data set "Add with
only remove permission" (array(array(), array('sysop')),
array(array('sysop'), array()), array('bot'))
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
Array &0 (
0 => 'bot'
+ 1 => 'sysop'
)
with $wgAddGroups['bureaucrat'] = true; in LocalSettings.php
Change-Id: I092a3353aa65d53c84b765c04ec213ed8294b65d
372 lines
11 KiB
PHP
372 lines
11 KiB
PHP
<?php
|
|
|
|
use MediaWiki\Block\DatabaseBlock;
|
|
use MediaWiki\MediaWikiServices;
|
|
|
|
/**
|
|
* @group API
|
|
* @group Database
|
|
* @group medium
|
|
*
|
|
* @covers ApiUserrights
|
|
*/
|
|
class ApiUserrightsTest extends ApiTestCase {
|
|
|
|
protected function setUp() : void {
|
|
parent::setUp();
|
|
$this->tablesUsed = array_merge(
|
|
$this->tablesUsed,
|
|
[ 'change_tag', 'change_tag_def', 'logging' ]
|
|
);
|
|
$this->setMwGlobals( [
|
|
'wgAddGroups' => [],
|
|
'wgRemoveGroups' => [],
|
|
] );
|
|
}
|
|
|
|
/**
|
|
* Unsets $wgGroupPermissions['bureaucrat']['userrights'], and sets
|
|
* $wgAddGroups['bureaucrat'] and $wgRemoveGroups['bureaucrat'] to the
|
|
* specified values.
|
|
*
|
|
* @param array|bool $add Groups bureaucrats should be allowed to add, true for all
|
|
* @param array|bool $remove Groups bureaucrats should be allowed to remove, true for all
|
|
*/
|
|
protected function setPermissions( $add = [], $remove = [] ) {
|
|
$this->setGroupPermissions( 'bureaucrat', 'userrights', false );
|
|
|
|
if ( $add ) {
|
|
$this->mergeMwGlobalArrayValue( 'wgAddGroups', [ 'bureaucrat' => $add ] );
|
|
}
|
|
if ( $remove ) {
|
|
$this->mergeMwGlobalArrayValue( 'wgRemoveGroups', [ 'bureaucrat' => $remove ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform an API userrights request that's expected to be successful.
|
|
*
|
|
* @param array|string $expectedGroups Group(s) that the user is expected
|
|
* to have after the API request
|
|
* @param array $params Array to pass to doApiRequestWithToken(). 'action'
|
|
* => 'userrights' is implicit. If no 'user' or 'userid' is specified,
|
|
* we add a 'user' parameter. If no 'add' or 'remove' is specified, we
|
|
* add 'add' => 'sysop'.
|
|
* @param User|null $user The user that we're modifying. The user must be
|
|
* mutable, because we're going to change its groups! null means that
|
|
* we'll make up our own user to modify, and doesn't make sense if 'user'
|
|
* or 'userid' is specified in $params.
|
|
*/
|
|
protected function doSuccessfulRightsChange(
|
|
$expectedGroups = 'sysop', array $params = [], User $user = null
|
|
) {
|
|
$expectedGroups = (array)$expectedGroups;
|
|
$params['action'] = 'userrights';
|
|
|
|
if ( !$user ) {
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
}
|
|
|
|
$this->assertTrue( TestUserRegistry::isMutable( $user ),
|
|
'Immutable user passed to doSuccessfulRightsChange!' );
|
|
|
|
if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
|
|
$params['user'] = $user->getName();
|
|
}
|
|
if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
|
|
$params['add'] = 'sysop';
|
|
}
|
|
|
|
$res = $this->doApiRequestWithToken( $params );
|
|
|
|
$user->clearInstanceCache();
|
|
MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
|
|
$this->assertSame( $expectedGroups, $user->getGroups() );
|
|
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
/**
|
|
* Perform an API userrights request that's expected to fail.
|
|
*
|
|
* @param string $expectedException Expected exception text
|
|
* @param array $params As for doSuccessfulRightsChange()
|
|
* @param User|null $user As for doSuccessfulRightsChange(). If there's no
|
|
* user who will possibly be affected (such as if an invalid username is
|
|
* provided in $params), pass null.
|
|
*/
|
|
protected function doFailedRightsChange(
|
|
$expectedException, array $params = [], User $user = null
|
|
) {
|
|
$params['action'] = 'userrights';
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( $expectedException );
|
|
|
|
if ( !$user ) {
|
|
// If 'user' or 'userid' is specified and $user was not specified,
|
|
// the user we're creating now will have nothing to do with the API
|
|
// request, but that's okay, since we're just testing that it has
|
|
// no groups.
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
}
|
|
|
|
$this->assertTrue( TestUserRegistry::isMutable( $user ),
|
|
'Immutable user passed to doFailedRightsChange!' );
|
|
|
|
if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
|
|
$params['user'] = $user->getName();
|
|
}
|
|
if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
|
|
$params['add'] = 'sysop';
|
|
}
|
|
$expectedGroups = $user->getGroups();
|
|
|
|
try {
|
|
$this->doApiRequestWithToken( $params );
|
|
} finally {
|
|
$user->clearInstanceCache();
|
|
$this->assertSame( $expectedGroups, $user->getGroups() );
|
|
}
|
|
}
|
|
|
|
public function testAdd() {
|
|
$this->doSuccessfulRightsChange();
|
|
}
|
|
|
|
public function testBlockedWithUserrights() {
|
|
$user = $this->getTestSysop()->getUser();
|
|
|
|
$block = new DatabaseBlock( [ 'address' => $user, 'by' => $user->getId(), ] );
|
|
$blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
|
|
$blockStore->insertBlock( $block );
|
|
|
|
try {
|
|
$this->doSuccessfulRightsChange();
|
|
} finally {
|
|
$blockStore->deleteBlock( $block );
|
|
$user->clearInstanceCache();
|
|
}
|
|
}
|
|
|
|
public function testBlockedWithoutUserrights() {
|
|
$user = $this->getTestSysop()->getUser();
|
|
|
|
$this->setPermissions( true, true );
|
|
|
|
$block = new DatabaseBlock( [ 'address' => $user, 'by' => $user->getId() ] );
|
|
$blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
|
|
$blockStore->insertBlock( $block );
|
|
|
|
try {
|
|
$this->doFailedRightsChange( 'You have been blocked from editing.' );
|
|
} finally {
|
|
$blockStore->deleteBlock( $block );
|
|
$user->clearInstanceCache();
|
|
}
|
|
}
|
|
|
|
public function testAddMultiple() {
|
|
$this->doSuccessfulRightsChange(
|
|
[ 'bureaucrat', 'sysop' ],
|
|
[ 'add' => 'bureaucrat|sysop' ]
|
|
);
|
|
}
|
|
|
|
public function testTooFewExpiries() {
|
|
$this->doFailedRightsChange(
|
|
'2 expiry timestamps were provided where 3 were needed.',
|
|
[ 'add' => 'sysop|bureaucrat|bot', 'expiry' => 'infinity|tomorrow' ]
|
|
);
|
|
}
|
|
|
|
public function testTooManyExpiries() {
|
|
$this->doFailedRightsChange(
|
|
'3 expiry timestamps were provided where 2 were needed.',
|
|
[ 'add' => 'sysop|bureaucrat', 'expiry' => 'infinity|tomorrow|never' ]
|
|
);
|
|
}
|
|
|
|
public function testInvalidExpiry() {
|
|
$this->doFailedRightsChange( 'Invalid expiry time', [ 'expiry' => 'yummy lollipops!' ] );
|
|
}
|
|
|
|
public function testMultipleInvalidExpiries() {
|
|
$this->doFailedRightsChange(
|
|
'Invalid expiry time "foo".',
|
|
[ 'add' => 'sysop|bureaucrat', 'expiry' => 'foo|bar' ]
|
|
);
|
|
}
|
|
|
|
public function testWithTag() {
|
|
ChangeTags::defineTag( 'custom tag' );
|
|
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
|
|
$this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
|
|
|
|
$dbr = wfGetDB( DB_REPLICA );
|
|
$this->assertSame(
|
|
'custom tag',
|
|
$dbr->selectField(
|
|
[ 'change_tag', 'logging', 'change_tag_def' ],
|
|
'ctd_name',
|
|
[
|
|
'ct_log_id = log_id',
|
|
'log_namespace' => NS_USER,
|
|
'log_title' => strtr( $user->getName(), ' ', '_' )
|
|
],
|
|
__METHOD__,
|
|
[ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ]
|
|
)
|
|
);
|
|
}
|
|
|
|
public function testWithoutTagPermission() {
|
|
ChangeTags::defineTag( 'custom tag' );
|
|
|
|
$this->setGroupPermissions( 'user', 'applychangetags', false );
|
|
|
|
$this->doFailedRightsChange(
|
|
'You do not have permission to apply change tags along with your changes.',
|
|
[ 'tags' => 'custom tag' ]
|
|
);
|
|
}
|
|
|
|
public function testNonexistentUser() {
|
|
$this->doFailedRightsChange(
|
|
'There is no user by the name "Nonexistent user". Check your spelling.',
|
|
[ 'user' => 'Nonexistent user' ]
|
|
);
|
|
}
|
|
|
|
public function testWebToken() {
|
|
$sysop = $this->getTestSysop()->getUser();
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
|
|
$token = $sysop->getEditToken( $user->getName() );
|
|
|
|
$res = $this->doApiRequest( [
|
|
'action' => 'userrights',
|
|
'user' => $user->getName(),
|
|
'add' => 'sysop',
|
|
'token' => $token,
|
|
] );
|
|
|
|
$user->clearInstanceCache();
|
|
$this->assertSame( [ 'sysop' ], $user->getGroups() );
|
|
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
}
|
|
|
|
/**
|
|
* Helper for testCanProcessExpiries that returns a mock ApiUserrights that either can or cannot
|
|
* process expiries. Although the regular page can process expiries, we use a mock here to
|
|
* ensure that it's the result of canProcessExpiries() that makes a difference, and not some
|
|
* error in the way we construct the mock.
|
|
*
|
|
* @param bool $canProcessExpiries
|
|
*/
|
|
private function getMockForProcessingExpiries( $canProcessExpiries ) {
|
|
$sysop = $this->getTestSysop()->getUser();
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
|
|
$token = $sysop->getEditToken( 'userrights' );
|
|
|
|
$main = new ApiMain( new FauxRequest( [
|
|
'action' => 'userrights',
|
|
'user' => $user->getName(),
|
|
'add' => 'sysop',
|
|
'token' => $token,
|
|
] ) );
|
|
|
|
$mockUserRightsPage = $this->getMockBuilder( UserrightsPage::class )
|
|
->setMethods( [ 'canProcessExpiries' ] )
|
|
->getMock();
|
|
$mockUserRightsPage->method( 'canProcessExpiries' )->willReturn( $canProcessExpiries );
|
|
|
|
$mockApi = $this->getMockBuilder( ApiUserrights::class )
|
|
->setConstructorArgs( [ $main, 'userrights' ] )
|
|
->setMethods( [ 'getUserRightsPage' ] )
|
|
->getMock();
|
|
$mockApi->method( 'getUserRightsPage' )->willReturn( $mockUserRightsPage );
|
|
|
|
return $mockApi;
|
|
}
|
|
|
|
public function testCanProcessExpiries() {
|
|
$mock1 = $this->getMockForProcessingExpiries( true );
|
|
$this->assertArrayHasKey( 'expiry', $mock1->getAllowedParams() );
|
|
|
|
$mock2 = $this->getMockForProcessingExpiries( false );
|
|
$this->assertArrayNotHasKey( 'expiry', $mock2->getAllowedParams() );
|
|
}
|
|
|
|
/**
|
|
* Tests adding and removing various groups with various permissions.
|
|
*
|
|
* @dataProvider addAndRemoveGroupsProvider
|
|
* @param array|null $permissions [ [ $wgAddGroups, $wgRemoveGroups ] ] or null for 'userrights'
|
|
* to be set in $wgGroupPermissions
|
|
* @param array $groupsToChange [ [ groups to add ], [ groups to remove ] ]
|
|
* @param array $expectedGroups Array of expected groups
|
|
*/
|
|
public function testAddAndRemoveGroups(
|
|
?array $permissions, array $groupsToChange, array $expectedGroups
|
|
) {
|
|
if ( $permissions !== null ) {
|
|
$this->setPermissions( $permissions[0], $permissions[1] );
|
|
}
|
|
|
|
$params = [
|
|
'add' => implode( '|', $groupsToChange[0] ),
|
|
'remove' => implode( '|', $groupsToChange[1] ),
|
|
];
|
|
|
|
// We'll take a bot so we have a group to remove
|
|
$user = $this->getMutableTestUser( [ 'bot' ] )->getUser();
|
|
|
|
$this->doSuccessfulRightsChange( $expectedGroups, $params, $user );
|
|
}
|
|
|
|
public function addAndRemoveGroupsProvider() {
|
|
return [
|
|
'Simple add' => [
|
|
[ [ 'sysop' ], [] ],
|
|
[ [ 'sysop' ], [] ],
|
|
[ 'bot', 'sysop' ]
|
|
], 'Add with only remove permission' => [
|
|
[ [], [ 'sysop' ] ],
|
|
[ [ 'sysop' ], [] ],
|
|
[ 'bot' ],
|
|
], 'Add with global remove permission' => [
|
|
[ [], true ],
|
|
[ [ 'sysop' ], [] ],
|
|
[ 'bot' ],
|
|
], 'Simple remove' => [
|
|
[ [], [ 'bot' ] ],
|
|
[ [], [ 'bot' ] ],
|
|
[],
|
|
], 'Remove with only add permission' => [
|
|
[ [ 'bot' ], [] ],
|
|
[ [], [ 'bot' ] ],
|
|
[ 'bot' ],
|
|
], 'Remove with global add permission' => [
|
|
[ true, [] ],
|
|
[ [], [ 'bot' ] ],
|
|
[ 'bot' ],
|
|
], 'Add and remove same new group' => [
|
|
null,
|
|
[ [ 'sysop' ], [ 'sysop' ] ],
|
|
// The userrights code does removals before adds, so it doesn't remove the sysop
|
|
// group here and only adds it.
|
|
[ 'bot', 'sysop' ],
|
|
], 'Add and remove same existing group' => [
|
|
null,
|
|
[ [ 'bot' ], [ 'bot' ] ],
|
|
// But here it first removes the existing group and then re-adds it.
|
|
[ 'bot' ],
|
|
],
|
|
];
|
|
}
|
|
}
|