Follows-up I361fde0de7f4406bce6ed075ed397effa5be3359. Per T253461, not mass-changing source code, but the use of the native error silencing operator (@) is especially useful in tests because: 1. It requires any/all statements to be explicitly marked. The suppressWarnings/restoreWarnings sections encourage developers to be "lazy" and thus encapsulate more than needed if there are multiple ones near each other, which would ignore potentially important warnings in a test case, which is generally exactly the time when it is really useful to get warnings etc. 2. It avoids leaking state, for example in LBFactoryTest the assertFalse call would throw a PHPUnit assertion error (not meant to be caught by the local catch), and thus won't reach AtEase::restoreWarnings. This then causes later code to end up in a mismatching state and creates a confusing error_reporting state. See .phpcs.xml, where the at operator is allowed for all test code. Change-Id: I68d1725d685e0a7586468bc9de6dc29ceea31b8a
561 lines
15 KiB
PHP
561 lines
15 KiB
PHP
<?php
|
|
|
|
use MediaWiki\Page\PageIdentity;
|
|
use MediaWiki\Page\PageIdentityValue;
|
|
use MediaWiki\Page\PageReference;
|
|
use MediaWiki\Page\PageReferenceValue;
|
|
use MediaWiki\Permissions\PermissionStatus;
|
|
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
|
|
/**
|
|
* @group Database
|
|
*/
|
|
class RecentChangeTest extends MediaWikiIntegrationTestCase {
|
|
use MockAuthorityTrait;
|
|
use MockTitleTrait;
|
|
|
|
protected $title;
|
|
protected $target;
|
|
protected $user;
|
|
protected $user_comment;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->title = new PageIdentityValue( 17, NS_MAIN, 'SomeTitle', PageIdentity::LOCAL );
|
|
$this->target = new PageIdentityValue( 78, NS_MAIN, 'TestTarget', PageIdentity::LOCAL );
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
$this->user = new UserIdentityValue( $user->getId(), $user->getName() );
|
|
|
|
$this->user_comment = '<User comment about action>';
|
|
}
|
|
|
|
public function provideAttribs() {
|
|
$attribs = [
|
|
'rc_timestamp' => wfTimestamp( TS_MW ),
|
|
'rc_namespace' => NS_USER,
|
|
'rc_title' => 'Tony',
|
|
'rc_type' => RC_EDIT,
|
|
'rc_source' => RecentChange::SRC_EDIT,
|
|
'rc_minor' => 0,
|
|
'rc_cur_id' => 77,
|
|
'rc_user' => 858173476,
|
|
'rc_user_text' => 'Tony',
|
|
'rc_comment' => '',
|
|
'rc_comment_text' => '',
|
|
'rc_comment_data' => null,
|
|
'rc_this_oldid' => 70,
|
|
'rc_last_oldid' => 71,
|
|
'rc_bot' => 0,
|
|
'rc_ip' => '',
|
|
'rc_patrolled' => 0,
|
|
'rc_new' => 0,
|
|
'rc_old_len' => 80,
|
|
'rc_new_len' => 88,
|
|
'rc_deleted' => 0,
|
|
'rc_logid' => 0,
|
|
'rc_log_type' => null,
|
|
'rc_log_action' => '',
|
|
'rc_params' => '',
|
|
];
|
|
|
|
yield 'external user' => [
|
|
[
|
|
'rc_type' => RC_EXTERNAL,
|
|
'rc_source' => 'foo',
|
|
'rc_user' => 0,
|
|
'rc_user_text' => 'm>External User',
|
|
] + $attribs
|
|
];
|
|
|
|
yield 'anon user' => [
|
|
[
|
|
'rc_type' => RC_EXTERNAL,
|
|
'rc_source' => 'foo',
|
|
'rc_user' => 0,
|
|
'rc_user_text' => '192.168.0.1',
|
|
] + $attribs
|
|
];
|
|
|
|
yield 'special title' => [
|
|
[
|
|
'rc_namespace' => NS_SPECIAL,
|
|
'rc_title' => 'Log',
|
|
'rc_type' => RC_LOG,
|
|
'rc_source' => RecentChange::SRC_LOG,
|
|
] + $attribs
|
|
];
|
|
|
|
yield 'no title' => [
|
|
[
|
|
'rc_namespace' => NS_MAIN,
|
|
'rc_title' => '',
|
|
'rc_type' => RC_LOG,
|
|
'rc_source' => RecentChange::SRC_LOG,
|
|
] + $attribs
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::save
|
|
* @covers RecentChange::newFromId
|
|
* @covers RecentChange::getTitle
|
|
* @covers RecentChange::getPerformerIdentity
|
|
* @dataProvider provideAttribs
|
|
*/
|
|
public function testDatabaseRoundTrip( $attribs ) {
|
|
$this->hideDeprecated( 'RecentChange::getPerformer' );
|
|
$rc = new RecentChange;
|
|
$rc->mAttribs = $attribs;
|
|
$rc->mExtra = [
|
|
'pageStatus' => 'changed'
|
|
];
|
|
$rc->save();
|
|
$id = $rc->getAttribute( 'rc_id' );
|
|
|
|
$rc = RecentChange::newFromId( $id );
|
|
|
|
$actualAttribs = array_intersect_key( $rc->mAttribs, $attribs );
|
|
$this->assertArrayEquals( $attribs, $actualAttribs, false, true );
|
|
|
|
$user = new UserIdentityValue( $attribs['rc_user'] ?? 0, $attribs['rc_user_text'] );
|
|
$this->assertTrue( $user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $user->equals( $rc->getPerformer() ) );
|
|
|
|
if ( empty( $attribs['rc_title'] ) ) {
|
|
$this->assertNull( $rc->getPage() );
|
|
} else {
|
|
$title = Title::makeTitle( $attribs['rc_namespace'], $attribs['rc_title'] );
|
|
$this->assertTrue( $title->isSamePageAs( $rc->getTitle() ) );
|
|
$this->assertTrue( $title->isSamePageAs( $rc->getPage() ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::newFromRow
|
|
* @covers RecentChange::loadFromRow
|
|
* @covers RecentChange::getAttributes
|
|
* @covers RecentChange::getPerformerIdentity
|
|
* @covers RecentChange::getPerformer
|
|
*/
|
|
public function testNewFromRow() {
|
|
$this->hideDeprecated( 'RecentChange::getPerformer' );
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
$row = (object)[
|
|
'rc_foo' => 'AAA',
|
|
'rc_timestamp' => '20150921134808',
|
|
'rc_deleted' => 'bar',
|
|
'rc_comment_text' => 'comment',
|
|
'rc_comment_data' => null,
|
|
'rc_user' => $user->getId(), // lookup by id
|
|
];
|
|
|
|
$rc = RecentChange::newFromRow( $row );
|
|
|
|
$expected = [
|
|
'rc_foo' => 'AAA',
|
|
'rc_timestamp' => '20150921134808',
|
|
'rc_deleted' => 'bar',
|
|
'rc_comment' => 'comment',
|
|
'rc_comment_text' => 'comment',
|
|
'rc_comment_data' => null,
|
|
'rc_user' => $user->getId(),
|
|
'rc_user_text' => $user->getName()
|
|
];
|
|
$this->assertEquals( $expected, $rc->getAttributes() );
|
|
$this->assertTrue( $user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $user->equals( $rc->getPerformer() ) );
|
|
|
|
$row = (object)[
|
|
'rc_foo' => 'AAA',
|
|
'rc_timestamp' => '20150921134808',
|
|
'rc_deleted' => 'bar',
|
|
'rc_comment' => 'comment',
|
|
'rc_user_text' => $user->getName(), // lookup by name
|
|
];
|
|
$rc = @RecentChange::newFromRow( $row );
|
|
|
|
$expected = [
|
|
'rc_foo' => 'AAA',
|
|
'rc_timestamp' => '20150921134808',
|
|
'rc_deleted' => 'bar',
|
|
'rc_comment' => 'comment',
|
|
'rc_comment_text' => 'comment',
|
|
'rc_comment_data' => null,
|
|
'rc_user' => $user->getId(),
|
|
'rc_user_text' => $user->getName()
|
|
];
|
|
$this->assertEquals( $expected, $rc->getAttributes() );
|
|
$this->assertEquals( $expected, $rc->getAttributes() );
|
|
$this->assertTrue( $user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $user->equals( $rc->getPerformer() ) );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::notifyNew
|
|
* @covers RecentChange::newFromId
|
|
* @covers RecentChange::getAttributes
|
|
* @covers RecentChange::getPerformerIdentity
|
|
* @covers RecentChange::getPerformer
|
|
*/
|
|
public function testNotifyNew() {
|
|
$this->hideDeprecated( 'RecentChange::getPerformer' );
|
|
$now = MWTimestamp::now();
|
|
$rc = RecentChange::notifyNew(
|
|
$now,
|
|
$this->title,
|
|
false,
|
|
$this->user,
|
|
$this->user_comment,
|
|
false
|
|
);
|
|
|
|
$expected = [
|
|
'rc_timestamp' => $now,
|
|
'rc_deleted' => 0,
|
|
'rc_comment_text' => $this->user_comment,
|
|
'rc_user' => $this->user->getId(),
|
|
'rc_user_text' => $this->user->getName()
|
|
];
|
|
|
|
$actual = array_intersect_key( $rc->getAttributes(), $expected );
|
|
|
|
$this->assertEquals( $expected, $actual );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformer() ) );
|
|
|
|
$rc = RecentChange::newFromId( $rc->getAttribute( 'rc_id' ) );
|
|
|
|
$actual = array_intersect_key( $rc->getAttributes(), $expected );
|
|
|
|
$this->assertEquals( $expected, $actual );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformer() ) );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::notifyNew
|
|
* @covers RecentChange::newFromId
|
|
* @covers RecentChange::getAttributes
|
|
* @covers RecentChange::getPerformerIdentity
|
|
* @covers RecentChange::getPerformer
|
|
*/
|
|
public function testNotifyEdit() {
|
|
$this->hideDeprecated( 'RecentChange::getPerformer' );
|
|
$now = MWTimestamp::now();
|
|
$rc = RecentChange::notifyEdit(
|
|
$now,
|
|
$this->title,
|
|
false,
|
|
$this->user,
|
|
$this->user_comment,
|
|
0,
|
|
$now,
|
|
false
|
|
);
|
|
|
|
$expected = [
|
|
'rc_timestamp' => $now,
|
|
'rc_deleted' => 0,
|
|
'rc_comment_text' => $this->user_comment,
|
|
'rc_user' => $this->user->getId(),
|
|
'rc_user_text' => $this->user->getName()
|
|
];
|
|
|
|
$actual = array_intersect_key( $rc->getAttributes(), $expected );
|
|
|
|
$this->assertEquals( $expected, $actual );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformer() ) );
|
|
|
|
$rc = RecentChange::newFromId( $rc->getAttribute( 'rc_id' ) );
|
|
|
|
$actual = array_intersect_key( $rc->getAttributes(), $expected );
|
|
|
|
$this->assertEquals( $expected, $actual );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformer() ) );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::notifyNew
|
|
* @covers RecentChange::newFromId
|
|
* @covers RecentChange::getAttributes
|
|
* @covers RecentChange::getPerformerIdentity
|
|
* @covers RecentChange::getPerformer
|
|
*/
|
|
public function testNewLogEntry() {
|
|
$this->hideDeprecated( 'RecentChange::getPerformer' );
|
|
$now = MWTimestamp::now();
|
|
$logPage = new PageReferenceValue( NS_SPECIAL, 'Log/test', PageReference::LOCAL );
|
|
|
|
$rc = RecentChange::newLogEntry(
|
|
$now,
|
|
$logPage,
|
|
$this->user,
|
|
'action comment',
|
|
'192.168.0.2',
|
|
'test',
|
|
'testing',
|
|
$this->title,
|
|
$this->user_comment,
|
|
'a|b|c',
|
|
7
|
|
);
|
|
|
|
$expected = [
|
|
'rc_timestamp' => $now,
|
|
'rc_comment_text' => $this->user_comment,
|
|
'rc_user' => $this->user->getId(),
|
|
'rc_user_text' => $this->user->getName(),
|
|
'rc_title' => $this->title->getDBkey(),
|
|
'rc_logid' => 7,
|
|
'rc_log_type' => 'test',
|
|
'rc_log_action' => 'testing',
|
|
];
|
|
|
|
$actual = array_intersect_key( $rc->getAttributes(), $expected );
|
|
|
|
$this->assertEquals( $expected, $actual );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformerIdentity() ) );
|
|
$this->assertTrue( $this->user->equals( $rc->getPerformer() ) );
|
|
$this->assertTrue( $this->title->isSamePageAs( $rc->getPage() ) );
|
|
$this->assertTrue( $this->title->isSamePageAs( $rc->getTitle() ) );
|
|
}
|
|
|
|
public function provideParseParams() {
|
|
// $expected, $raw
|
|
yield 'extracting an array' => [
|
|
[
|
|
'root' => [
|
|
'A' => 1,
|
|
'B' => 'two'
|
|
]
|
|
],
|
|
'a:1:{s:4:"root";a:2:{s:1:"A";i:1;s:1:"B";s:3:"two";}}'
|
|
];
|
|
|
|
yield 'null' => [ null, null ];
|
|
yield 'false' => [ null, serialize( false ) ];
|
|
yield 'non-array' => [ null, 'not-an-array' ];
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::parseParams
|
|
* @dataProvider provideParseParams
|
|
* @param array $expectedParseParams
|
|
* @param string|null $rawRcParams
|
|
*/
|
|
public function testParseParams( $expectedParseParams, $rawRcParams ) {
|
|
$rc = new RecentChange;
|
|
$rc->setAttribs( [ 'rc_params' => $rawRcParams ] );
|
|
|
|
$actualParseParams = $rc->parseParams();
|
|
|
|
$this->assertEquals( $expectedParseParams, $actualParseParams );
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function provideIsInRCLifespan() {
|
|
return [
|
|
[ 6000, -3000, 0, true ],
|
|
[ 3000, -6000, 0, false ],
|
|
[ 6000, -3000, 6000, true ],
|
|
[ 3000, -6000, 6000, true ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::isInRCLifespan
|
|
* @dataProvider provideIsInRCLifespan
|
|
*/
|
|
public function testIsInRCLifespan( $maxAge, $offset, $tolerance, $expected ) {
|
|
$this->setMwGlobals( 'wgRCMaxAge', $maxAge );
|
|
// Calculate this here instead of the data provider because the provider
|
|
// is expanded early on and the full test suite may take longer than 100 minutes
|
|
// when coverage is enabled.
|
|
$timestamp = time() + $offset;
|
|
$this->assertEquals( $expected, RecentChange::isInRCLifespan( $timestamp, $tolerance ) );
|
|
}
|
|
|
|
public function provideRCTypes() {
|
|
return [
|
|
[ RC_EDIT, 'edit' ],
|
|
[ RC_NEW, 'new' ],
|
|
[ RC_LOG, 'log' ],
|
|
[ RC_EXTERNAL, 'external' ],
|
|
[ RC_CATEGORIZE, 'categorize' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRCTypes
|
|
* @covers RecentChange::parseFromRCType
|
|
*/
|
|
public function testParseFromRCType( $rcType, $type ) {
|
|
$this->assertEquals( $type, RecentChange::parseFromRCType( $rcType ) );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRCTypes
|
|
* @covers RecentChange::parseToRCType
|
|
*/
|
|
public function testParseToRCType( $rcType, $type ) {
|
|
$this->assertEquals( $rcType, RecentChange::parseToRCType( $type ) );
|
|
}
|
|
|
|
/**
|
|
* @return MockObject|PageProps
|
|
*/
|
|
private function getMockPageProps() {
|
|
return $this->getMockBuilder( PageProps::class )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
}
|
|
|
|
public function provideCategoryContent() {
|
|
return [
|
|
[ true ],
|
|
[ false ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideCategoryContent
|
|
* @covers RecentChange::newForCategorization
|
|
*/
|
|
public function testHiddenCategoryChange( $isHidden ) {
|
|
$categoryTitle = Title::newFromText( 'CategoryPage', NS_CATEGORY );
|
|
|
|
$pageProps = $this->getMockPageProps();
|
|
$pageProps->expects( $this->once() )
|
|
->method( 'getProperties' )
|
|
->with( $categoryTitle, 'hiddencat' )
|
|
->willReturn( $isHidden ? [ $categoryTitle->getArticleID() => '' ] : [] );
|
|
|
|
$this->setService( 'PageProps', $pageProps );
|
|
|
|
$rc = RecentChange::newForCategorization(
|
|
'0',
|
|
$categoryTitle,
|
|
$this->user,
|
|
$this->user_comment,
|
|
$this->title,
|
|
$categoryTitle->getLatestRevID(),
|
|
$categoryTitle->getLatestRevID(),
|
|
'0',
|
|
false
|
|
);
|
|
|
|
$this->assertEquals( $isHidden, $rc->getParam( 'hidden-cat' ) );
|
|
}
|
|
|
|
private function getDummyEditRecentChange(): RecentChange {
|
|
return RecentChange::notifyEdit(
|
|
MWTimestamp::now(),
|
|
$this->title,
|
|
false,
|
|
$this->user,
|
|
$this->user_comment,
|
|
0,
|
|
MWTimestamp::now(),
|
|
false
|
|
);
|
|
}
|
|
|
|
public function provideDoMarkPatrolledPermissions() {
|
|
yield 'auto, no autopatrol' => [
|
|
'lackingPermissions' => [ 'autopatrol' ],
|
|
'auto' => true,
|
|
'expectedError' => 'missing-autopatrol'
|
|
];
|
|
yield 'no patrol' => [
|
|
'lackingPermissions' => [ 'patrol' ],
|
|
'auto' => false,
|
|
'expectedError' => 'missing-patrol'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideDoMarkPatrolledPermissions
|
|
* @covers RecentChange::doMarkPatrolled
|
|
*/
|
|
public function testDoMarkPatrolledPermissions(
|
|
array $lackingPermissions,
|
|
bool $auto,
|
|
string $expectError
|
|
) {
|
|
$rc = $this->getDummyEditRecentChange();
|
|
$performer = $this->mockRegisteredAuthority( static function (
|
|
string $permission,
|
|
PageIdentity $page,
|
|
PermissionStatus $status
|
|
) use ( $lackingPermissions ) {
|
|
if ( in_array( $permission, $lackingPermissions ) ) {
|
|
$status->fatal( "missing-$permission" );
|
|
return false;
|
|
}
|
|
return true;
|
|
} );
|
|
$errors = $rc->doMarkPatrolled(
|
|
$performer,
|
|
$auto
|
|
);
|
|
$this->assertContains( [ $expectError ], $errors );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::doMarkPatrolled
|
|
*/
|
|
public function testDoMarkPatrolledPermissions_Hook() {
|
|
$rc = $this->getDummyEditRecentChange();
|
|
$this->setTemporaryHook( 'MarkPatrolled', static function () {
|
|
return false;
|
|
} );
|
|
$errors = $rc->doMarkPatrolled( $this->mockRegisteredUltimateAuthority() );
|
|
$this->assertContains( [ 'hookaborted' ], $errors );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::doMarkPatrolled
|
|
*/
|
|
public function testDoMarkPatrolledPermissions_Self() {
|
|
$rc = $this->getDummyEditRecentChange();
|
|
$errors = $rc->doMarkPatrolled(
|
|
$this->mockUserAuthorityWithoutPermissions( $this->user, [ 'autopatrol' ] )
|
|
);
|
|
$this->assertContains( [ 'markedaspatrollederror-noautopatrol' ], $errors );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::doMarkPatrolled
|
|
*/
|
|
public function testDoMarkPatrolledPermissions_NoRcPatrol() {
|
|
$this->setMwGlobals( [
|
|
'wgUseRCPatrol' => false
|
|
] );
|
|
$rc = $this->getDummyEditRecentChange();
|
|
$errors = $rc->doMarkPatrolled( $this->mockRegisteredUltimateAuthority() );
|
|
$this->assertContains( [ 'rcpatroldisabled' ], $errors );
|
|
}
|
|
|
|
/**
|
|
* @covers RecentChange::doMarkPatrolled
|
|
*/
|
|
public function testDoMarkPatrolled() {
|
|
$rc = $this->getDummyEditRecentChange();
|
|
$errors = $rc->doMarkPatrolled(
|
|
$this->mockUserAuthorityWithPermissions( $this->user, [ 'patrol', 'autopatrol' ] )
|
|
);
|
|
$this->assertEmpty( $errors );
|
|
|
|
$reloadedRC = RecentChange::newFromId( $rc->getAttribute( 'rc_id' ) );
|
|
$this->assertSame( '1', $reloadedRC->getAttribute( 'rc_patrolled' ) );
|
|
}
|
|
}
|