Viewing File: /home/omtekel/www/wp-content/upgrade/backup/integration.tar
Maintenance/PurgeOldLogIPDataTest.php 0000666 00000003760 15133473510 0013561 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use HashConfig;
use MediaWiki\Extension\AbuseFilter\Maintenance\PurgeOldLogIPData;
use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* @group Test
* @group AbuseFilter
* @group Database
* @covers \MediaWiki\Extension\AbuseFilter\Maintenance\PurgeOldLogIPData
*/
class PurgeOldLogIPDataTest extends MaintenanceBaseTestCase {
private const FAKE_TIME = '20200115000000';
private const MAX_AGE = 3600;
/** @inheritDoc */
protected $tablesUsed = [ 'abuse_filter_log' ];
/**
* @inheritDoc
*/
protected function getMaintenanceClass() {
return PurgeOldLogIPData::class;
}
/**
* @inheritDoc
*/
public function addDBData() {
$defaultRow = [
'afl_ip' => '1.1.1.1',
'afl_global' => 0,
'afl_filter_id' => 1,
'afl_user' => 1,
'afl_user_text' => 'User',
'afl_action' => 'edit',
'afl_actions' => '',
'afl_var_dump' => 'xxx',
'afl_namespace' => 0,
'afl_title' => 'Title',
'afl_wiki' => null,
'afl_deleted' => 0,
'afl_patrolled_by' => 0,
'afl_rev_id' => 42,
];
$oldTS = ConvertibleTimestamp::convert(
TS_MW,
ConvertibleTimestamp::convert( TS_UNIX, self::FAKE_TIME ) - 2 * self::MAX_AGE
);
$rows = [
[ 'afl_id' => 1, 'afl_timestamp' => $this->db->timestamp( $oldTS ) ] + $defaultRow,
[ 'afl_id' => 2, 'afl_timestamp' => $this->db->timestamp( $oldTS ), 'afl_ip' => '' ] + $defaultRow,
[ 'afl_id' => 3, 'afl_timestamp' => $this->db->timestamp( self::FAKE_TIME ) ] + $defaultRow,
[ 'afl_id' => 4, 'afl_timestamp' => $this->db->timestamp( self::FAKE_TIME ), 'afl_ip' => '' ] + $defaultRow,
];
$this->db->insert( 'abuse_filter_log', $rows, __METHOD__ );
}
public function testExecute() {
ConvertibleTimestamp::setFakeTime( self::FAKE_TIME );
$this->maintenance->setConfig( new HashConfig( [ 'AbuseFilterLogIPMaxAge' => self::MAX_AGE ] ) );
$this->expectOutputRegex( '/1 rows/' );
$this->maintenance->execute();
}
}
Maintenance/SearchFiltersTest.php 0000666 00000005775 15133473510 0013121 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use Generator;
use MediaWiki\Extension\AbuseFilter\Maintenance\SearchFilters;
use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase;
/**
* @group Test
* @group AbuseFilter
* @group Database
* @covers \MediaWiki\Extension\AbuseFilter\Maintenance\SearchFilters
*/
class SearchFiltersTest extends MaintenanceBaseTestCase {
/** @inheritDoc */
protected $tablesUsed = [ 'abuse_filter' ];
protected function setUp(): void {
global $wgDBtype;
parent::setUp();
if ( $wgDBtype !== 'mysql' ) {
$this->markTestSkipped( 'The script only works on MySQL' );
}
}
/**
* @inheritDoc
*/
protected function getMaintenanceClass() {
return SearchFilters::class;
}
/**
* @inheritDoc
*/
public function addDBData() {
$defaultRow = [
'af_user' => 0,
'af_user_text' => 'FilterTester',
'af_actor' => 1,
'af_timestamp' => $this->db->timestamp( '20190826000000' ),
'af_enabled' => 1,
'af_comments' => '',
'af_public_comments' => 'Test filter',
'af_hidden' => 0,
'af_hit_count' => 0,
'af_throttled' => 0,
'af_deleted' => 0,
'af_actions' => '',
'af_global' => 0,
'af_group' => 'default'
];
$rows = [
[ 'af_id' => 1, 'af_pattern' => '' ] + $defaultRow,
[ 'af_id' => 2, 'af_pattern' => 'rmspecials(page_title) === "foo"' ] + $defaultRow,
[ 'af_id' => 3, 'af_pattern' => 'user_editcount % 3 !== 1' ] + $defaultRow,
[ 'af_id' => 4, 'af_pattern' => 'rmspecials(added_lines_pst) !== ""' ] + $defaultRow
];
$this->db->insert( 'abuse_filter', $rows, __METHOD__ );
}
private function getExpectedOutput( array $ids, bool $withHeader = true ): string {
global $wgDBname;
$expected = $withHeader ? "wiki\tfilter\n" : '';
foreach ( $ids as $id ) {
$expected .= "$wgDBname\t$id\n";
}
return $expected;
}
public static function provideSearches(): Generator {
yield 'single filter' => [ 'page_title', [ 2 ] ];
yield 'multiple filters' => [ 'rmspecials', [ 2, 4 ] ];
yield 'regex' => [ '[a-z]\(', [ 2, 4 ] ];
}
/**
* @param string $pattern
* @param array $expectedIDs
* @dataProvider provideSearches
*/
public function testExecute_singleWiki( string $pattern, array $expectedIDs ) {
$this->setMwGlobals( [ 'wgConf' => (object)[ 'wikis' => [] ] ] );
$this->maintenance->loadParamsAndArgs( null, [ 'pattern' => $pattern ] );
$this->expectOutputString( $this->getExpectedOutput( $expectedIDs ) );
$this->maintenance->execute();
}
/**
* @param string $pattern
* @param array $expectedIDs
* @dataProvider provideSearches
*/
public function testExecute_multipleWikis( string $pattern, array $expectedIDs ) {
global $wgDBname;
$this->setMwGlobals( [ 'wgConf' => (object)[ 'wikis' => [ $wgDBname, $wgDBname ] ] ] );
$this->maintenance->loadParamsAndArgs( null, [ 'pattern' => $pattern ] );
$expectedText = $this->getExpectedOutput( $expectedIDs ) . $this->getExpectedOutput( $expectedIDs, false );
$this->expectOutputString( $expectedText );
$this->maintenance->execute();
}
}
ChangeTags/ChangeTagValidatorTest.php 0000666 00000002703 15133473510 0013620 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\ChangeTags;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWikiIntegrationTestCase;
/**
* @group Test
* @group AbuseFilter
* @group Database
* @covers \MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagValidator
*/
class ChangeTagValidatorTest extends MediaWikiIntegrationTestCase {
/**
* @todo Make this a unit test once static methods in ChangeTags are moved to a service
* @todo When the above is possible, use mocks to test canAddTagsAccompanyingChange and canCreateTag
* @param string $tag The tag to validate
* @param string|null $expectedError
* @covers \MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagValidator::validateTag
* @dataProvider provideTags
*/
public function testValidateTag( string $tag, ?string $expectedError ) {
$validator = AbuseFilterServices::getChangeTagValidator();
$status = $validator->validateTag( $tag );
$actualError = $status->isGood() ? null : $status->getErrors()[0]['message'];
$this->assertSame( $expectedError, $actualError );
}
/**
* Data provider for testValidateTag
* @return array
*/
public static function provideTags() {
return [
'invalid chars' => [ 'a|b', 'tags-create-invalid-chars' ],
'core-reserved tag' => [ 'mw-undo', 'abusefilter-edit-bad-tags' ],
'AF-reserved tag' => [ 'abusefilter-condition-limit', 'abusefilter-tag-reserved' ],
'valid' => [ 'my_tag', null ],
];
}
}
ActionVariablesIntegrationTest.php 0000666 00000033717 15133473510 0013410 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use AbuseFilterCreateAccountTestTrait;
use ApiTestCase;
use ApiUsageException;
use Content;
use FormatJson;
use Generator;
use JsonContent;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\AbuseLogger;
use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesLookup;
use MediaWiki\Extension\AbuseFilter\EditRevUpdater;
use MediaWiki\Extension\AbuseFilter\EmergencyCache;
use MediaWiki\Extension\AbuseFilter\Filter\ExistingFilter;
use MediaWiki\Extension\AbuseFilter\Filter\Flags;
use MediaWiki\Extension\AbuseFilter\Filter\LastEditInfo;
use MediaWiki\Extension\AbuseFilter\Filter\Specs;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\FilterProfiler;
use MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Watcher\EmergencyWatcher;
use MediaWiki\Extension\AbuseFilter\Watcher\UpdateHitCountWatcher;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use NullStatsdDataFactory;
use WikitextContent;
/**
* @group Database
* @group medium
* @group AbuseFilter
* @group AbuseFilterGeneric
*/
class ActionVariablesIntegrationTest extends ApiTestCase {
use AbuseFilterCreateAccountTestTrait;
public function setUp(): void {
parent::setUp();
$this->tablesUsed[] = 'externallinks';
$this->tablesUsed[] = 'page';
$this->tablesUsed[] = 'revision';
}
private function prepareServices(): void {
$this->setService(
FilterProfiler::SERVICE_NAME,
$this->createMock( FilterProfiler::class )
);
$this->setService(
EmergencyCache::SERVICE_NAME,
$this->createMock( EmergencyCache::class )
);
$this->setService(
EmergencyWatcher::SERVICE_NAME,
$this->createMock( EmergencyWatcher::class )
);
$this->setService(
UpdateHitCountWatcher::SERVICE_NAME,
$this->createMock( UpdateHitCountWatcher::class )
);
$this->setService(
EditRevUpdater::SERVICE_NAME,
$this->createMock( EditRevUpdater::class )
);
$this->overrideConfigValues( [
MainConfigNames::PageCreationLog => false,
'AbuseFilterCentralDB' => false,
] );
$filter = new ExistingFilter(
new Specs( '1 === 1', '', 'Test Filter', [ 'disallow' ], 'default' ),
new Flags( true, false, false, false ),
[ 'disallow' => [ 'abusefilter-disallow' ] ],
new LastEditInfo( 1, 'Filter User', '20220713000000' ),
1,
0,
false
);
$filterLookup = $this->createMock( FilterLookup::class );
$filterLookup->expects( $this->once() )
->method( 'getAllActiveFiltersInGroup' )
->with( 'default', false )
->willReturn( [ $filter ] );
$filterLookup->method( 'getFilter' )
->with( 1, false )
->willReturn( $filter );
$this->setService( FilterLookup::SERVICE_NAME, $filterLookup );
$consequencesLookup = $this->createMock( ConsequencesLookup::class );
$consequencesLookup->method( 'getConsequencesForFilters' )
->with( $this->logicalOr( [ 1 ], [ '1' ] ) )
->willReturn( [ 1 => $filter->getActions() ] );
$this->setService( ConsequencesLookup::SERVICE_NAME, $consequencesLookup );
}
private function setAbuseLoggerFactoryWithEavesdrop( VariableHolder &$varHolder = null ): void {
$factory = $this->createMock( AbuseLoggerFactory::class );
$factory->method( 'newLogger' )
->willReturnCallback( function ( $title, $user, $vars ) use ( &$varHolder ) {
$varHolder = $vars;
$logger = $this->createMock( AbuseLogger::class );
$logger->method( 'addLogEntries' )
->willReturn( [ 'local' => [ 1 ], 'global' => [] ] );
return $logger;
} );
$this->setService( AbuseLoggerFactory::SERVICE_NAME, $factory );
}
private function assertVariables( array $expected, array $export ) {
foreach ( $expected as $var => $value ) {
$this->assertArrayHasKey( $var, $export, "Variable '$var' not set" );
$actual = $export[$var];
if ( $var === 'new_html' && is_array( $value ) ) {
// Special case for new_html: avoid flaky tests, and only check containment
$this->assertStringContainsString( '<div class="mw-parser-output', $actual );
$this->assertDoesNotMatchRegularExpression( "/<!--\s*NewPP limit/", $actual );
$this->assertDoesNotMatchRegularExpression( "/<!--\s*Transclusion/", $actual );
foreach ( $value as $needle ) {
$this->assertStringContainsString( $needle, $actual, 'Checking new_html' );
}
} else {
$this->assertSame( $value, $actual, $var );
}
}
}
public static function provideEditVariables(): Generator {
$summary = __METHOD__;
$new = '[https://a.com Test] foo';
yield 'create page' => [
'expected' => [
'action' => 'edit',
'old_wikitext' => '',
'old_content_model' => '',
'new_wikitext' => $new,
'new_content_model' => 'wikitext',
'summary' => $summary,
'new_pst' => $new,
'new_text' => "Test foo",
'edit_diff' => "@@ -1,0 +1,1 @@\n+$new\n",
'edit_diff_pst' => "@@ -1,0 +1,1 @@\n+$new\n",
'new_size' => strlen( $new ),
'old_size' => 0,
'edit_delta' => strlen( $new ),
'added_lines' => [ $new ],
'removed_lines' => [],
'added_lines_pst' => [ $new ],
'all_links' => [ 'https://a.com/' ],
'old_links' => [],
'added_links' => [ 'https://a.com/' ],
'removed_links' => [],
],
'params' => [ 'text' => $new, 'summary' => $summary, 'createonly' => true ],
];
// phpcs:disable Generic.Files.LineLength
$old = '[https://a.com Test] foo';
$new = "'''Random'''.\nSome ''special'' chars: àèìòù 名探偵コナン.\n[[Help:PST|]] test, [//www.b.com link]";
yield 'PST and special chars' => [
'expected' => [
'action' => 'edit',
'old_wikitext' => $old,
'old_content_model' => 'wikitext',
'new_wikitext' => $new,
'new_content_model' => 'wikitext',
'summary' => $summary,
'new_pst' => "'''Random'''.\nSome ''special'' chars: àèìòù 名探偵コナン.\n[[Help:PST|PST]] test, [//www.b.com link]",
'new_text' => "Random.\nSome special chars: àèìòù 名探偵コナン.\nPST test, link",
'edit_diff' => "@@ -1,1 +1,3 @@\n-[https://a.com Test] foo\n+'''Random'''.\n+Some ''special'' chars: àèìòù 名探偵コナン.\n+[[Help:PST|]] test, [//www.b.com link]\n",
'edit_diff_pst' => "@@ -1,1 +1,3 @@\n-[https://a.com Test] foo\n+'''Random'''.\n+Some ''special'' chars: àèìòù 名探偵コナン.\n+[[Help:PST|PST]] test, [//www.b.com link]\n",
'new_size' => strlen( $new ),
'old_size' => strlen( $old ),
'edit_delta' => strlen( $new ) - strlen( $old ),
'added_lines' => explode( "\n", $new ),
'removed_lines' => [ $old ],
'added_lines_pst' => [ "'''Random'''.", "Some ''special'' chars: àèìòù 名探偵コナン.", '[[Help:PST|PST]] test, [//www.b.com link]' ],
'old_links' => [ 'https://a.com/' ],
'all_links' => [ 'https://www.b.com/' ],
'removed_links' => [ 'https://a.com/' ],
'added_links' => [ 'https://www.b.com/' ],
],
'params' => [ 'text' => $new, 'summary' => $summary ],
'oldContent' => new WikitextContent( $old ),
];
$old = "'''Random'''.\nSome ''special'' chars: àèìòù 名探偵コナン.\n[[Help:PST|PST]] test, [//www.b.com link]";
$new = '[https://a.com Test] foo';
yield 'PST and special chars, reverse' => [
'expected' => [
'action' => 'edit',
'old_wikitext' => $old,
'old_content_model' => 'wikitext',
'new_wikitext' => $new,
'new_content_model' => 'wikitext',
'summary' => $summary,
'new_html' => [ 'Test</a>' ],
'new_pst' => '[https://a.com Test] foo',
'new_text' => 'Test foo',
'edit_diff' => "@@ -1,3 +1,1 @@\n-'''Random'''.\n-Some ''special'' chars: àèìòù 名探偵コナン.\n-[[Help:PST|PST]] test, [//www.b.com link]\n+[https://a.com Test] foo\n",
'edit_diff_pst' => "@@ -1,3 +1,1 @@\n-'''Random'''.\n-Some ''special'' chars: àèìòù 名探偵コナン.\n-[[Help:PST|PST]] test, [//www.b.com link]\n+[https://a.com Test] foo\n",
'new_size' => strlen( $new ),
'old_size' => strlen( $old ),
'edit_delta' => strlen( $new ) - strlen( $old ),
'added_lines' => [ $new ],
'removed_lines' => explode( "\n", $old ),
'added_lines_pst' => [ $new ],
'old_links' => [ 'https://www.b.com/' ],
'all_links' => [ 'https://a.com/' ],
'removed_links' => [ 'https://www.b.com/' ],
'added_links' => [ 'https://a.com/' ],
],
'params' => [ 'text' => $new, 'summary' => $summary ],
'oldContent' => new WikitextContent( $old ),
];
// phpcs:enable Generic.Files.LineLength
$old = 'This edit will be pretty smal';
$new = $old . 'l';
yield 'Small edit' => [
'expected' => [
'action' => 'edit',
'old_wikitext' => $old,
'old_content_model' => 'wikitext',
'new_wikitext' => $new,
'new_content_model' => 'wikitext',
'summary' => $summary,
'new_html' => [ "<p>This edit will be pretty small\n</p>" ],
'new_pst' => $new,
'new_text' => $new,
'edit_diff' => "@@ -1,1 +1,1 @@\n-$old\n+$new\n",
'edit_diff_pst' => "@@ -1,1 +1,1 @@\n-$old\n+$new\n",
'new_size' => strlen( $new ),
'old_size' => strlen( $old ),
'edit_delta' => 1,
'removed_lines' => [ $old ],
'added_lines' => [ $new ],
'added_lines_pst' => [ $new ],
'old_links' => [],
'all_links' => [],
'removed_links' => [],
'added_links' => [],
],
'params' => [ 'text' => $new, 'summary' => $summary ],
'oldContent' => new WikitextContent( $old ),
];
yield 'content model change to wikitext' => [
'expected' => [
'action' => 'edit',
'old_wikitext' => "{\n \"key\": \"value\"\n}",
'old_content_model' => 'json',
'new_wikitext' => 'new test https://en.wikipedia.org',
'new_content_model' => 'wikitext',
'old_links' => [],
'all_links' => [ 'https://en.wikipedia.org/' ],
],
'params' => [
'text' => 'new test https://en.wikipedia.org',
'contentmodel' => 'wikitext',
],
'oldContent' => new JsonContent( FormatJson::encode( [ 'key' => 'value' ] ) ),
];
yield 'content model change from wikitext' => [
'expected' => [
'action' => 'edit',
'old_wikitext' => 'test https://en.wikipedia.org',
'old_content_model' => 'wikitext',
'new_wikitext' => '{"key": "value"}',
'new_content_model' => 'json',
'old_links' => [ 'https://en.wikipedia.org/' ],
'all_links' => [],
],
'params' => [
'text' => '{"key": "value"}',
'contentmodel' => 'json',
],
'oldContent' => new WikitextContent( 'test https://en.wikipedia.org' ),
];
}
/**
* @dataProvider provideEditVariables
* @covers \MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RunVariableGenerator
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGenerator
* @covers \MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer
*/
public function testEditVariables(
array $expected, array $params, Content $oldContent = null
) {
$varHolder = null;
$this->prepareServices();
$this->setAbuseLoggerFactoryWithEavesdrop( $varHolder );
$title = 'My test page';
$page = $this->getNonexistingTestPage( $title );
if ( $oldContent ) {
$status = $this->editPage( $page, $oldContent, 'Creating test page' );
$this->assertStatusGood( $status );
}
$handler = new FilteredActionsHandler(
new NullStatsdDataFactory(),
AbuseFilterServices::getFilterRunnerFactory(),
AbuseFilterServices::getVariableGeneratorFactory(),
AbuseFilterServices::getEditRevUpdater(),
AbuseFilterServices::getBlockedDomainFilter(),
MediaWikiServices::getInstance()->getPermissionManager()
);
$this->setTemporaryHook(
'EditFilterMergedContent',
[ $handler, 'onEditFilterMergedContent' ],
true
);
$ex = null;
try {
$this->doApiRequestWithToken(
[ 'action' => 'edit', 'title' => $title ] + $params
);
} catch ( ApiUsageException $ex ) {
}
$this->assertNotNull( $ex, 'Exception should be thrown' );
$this->assertNotNull( $varHolder, 'Variables should be set' );
$export = AbuseFilterServices::getVariablesManager()->dumpAllVars(
$varHolder,
array_keys( $expected )
);
$this->assertVariables( $expected, $export );
}
public static function provideAccountCreationVars(): Generator {
yield 'create account anonymously' => [
'expected' => [
'action' => 'createaccount',
'accountname' => 'New account',
]
];
yield 'create account by an existing user' => [
'expected' => [
'action' => 'createaccount',
'accountname' => 'New account',
'user_name' => 'Account creator',
'user_editcount' => 0,
],
'accountName' => 'New account',
'autocreate' => false,
'creatorName' => 'Account creator'
];
yield 'autocreate an account' => [
'expected' => [
'action' => 'autocreateaccount',
'accountname' => 'New account',
],
'accountName' => 'New account',
'autocreate' => true,
];
}
/**
* @dataProvider provideAccountCreationVars
* @covers \MediaWiki\Extension\AbuseFilter\AbuseFilterPreAuthenticationProvider
* @covers \MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RunVariableGenerator
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGenerator
*/
public function testAccountCreationVars(
array $expected,
string $accountName = 'New account',
bool $autocreate = false,
string $creatorName = null
) {
$varHolder = null;
$this->prepareServices();
$this->setAbuseLoggerFactoryWithEavesdrop( $varHolder );
$creator = null;
if ( $creatorName !== null ) {
$creator = $this->getServiceContainer()->getUserFactory()->newFromName( $creatorName );
$creator->addToDatabase();
}
$status = $this->createAccount( $accountName, $autocreate, $creator );
$this->assertStatusNotOK( $status );
$this->assertNotNull( $varHolder, 'Variables should be set' );
$export = AbuseFilterServices::getVariablesManager()->dumpAllVars(
$varHolder,
array_keys( $expected )
);
$this->assertVariables( $expected, $export );
}
}
Watcher/UpdateHitCountWatcherTest.php 0000666 00000003013 15133473510 0013733 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Watcher;
use MediaWiki\Extension\AbuseFilter\CentralDBManager;
use MediaWiki\Extension\AbuseFilter\Watcher\UpdateHitCountWatcher;
use MediaWikiIntegrationTestCase;
use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LBFactory;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Watcher\UpdateHitCountWatcher
* @covers ::__construct
*/
class UpdateHitCountWatcherTest extends MediaWikiIntegrationTestCase {
/**
* @covers ::run
* @covers ::updateHitCounts
*/
public function testRun() {
$localFilters = [ 1, 2, 3 ];
$globalFilters = [ 4, 5, 6 ];
$localDB = $this->createMock( DBConnRef::class );
$localDB->expects( $this->once() )->method( 'update' )->with(
'abuse_filter',
[ 'af_hit_count=af_hit_count+1' ],
[ 'af_id' => $localFilters ]
);
$lb = $this->createMock( LBFactory::class );
$lb->method( 'getPrimaryDatabase' )->willReturn( $localDB );
$globalDB = $this->createMock( IDatabase::class );
$globalDB->expects( $this->once() )->method( 'update' )->with(
'abuse_filter',
[ 'af_hit_count=af_hit_count+1' ],
[ 'af_id' => $globalFilters ]
);
$centralDBManager = $this->createMock( CentralDBManager::class );
$centralDBManager->method( 'getConnection' )->willReturn( $globalDB );
$watcher = new UpdateHitCountWatcher( $lb, $centralDBManager );
$watcher->run( $localFilters, $globalFilters, 'default' );
// Two soft assertions done above
$this->addToAssertionCount( 2 );
}
}
FilterStoreTest.php 0000666 00000011615 15133473510 0010371 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
use MediaWiki\Extension\AbuseFilter\Filter\Flags;
use MediaWiki\Extension\AbuseFilter\Filter\LastEditInfo;
use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter;
use MediaWiki\Extension\AbuseFilter\Filter\Specs;
use MediaWiki\Extension\AbuseFilter\FilterStore;
use MediaWikiIntegrationTestCase;
use Wikimedia\TestingAccessWrapper;
/**
* @group Test
* @group AbuseFilter
* @group Database
* @covers \MediaWiki\Extension\AbuseFilter\FilterStore
*/
class FilterStoreTest extends MediaWikiIntegrationTestCase {
private const DEFAULT_VALUES = [
'rules' => '/**/',
'user' => 0,
'user_text' => 'FilterTester',
'timestamp' => '20190826000000',
'enabled' => 1,
'comments' => '',
'name' => 'Mock filter',
'hidden' => 0,
'hit_count' => 0,
'throttled' => 0,
'deleted' => 0,
'actions' => [],
'global' => 0,
'group' => 'default'
];
/** @inheritDoc */
protected $tablesUsed = [
'abuse_filter',
'actor',
];
/**
* @param int $id
*/
private function createFilter( int $id ): void {
$row = self::DEFAULT_VALUES;
$row['timestamp'] = $this->db->timestamp( $row['timestamp'] );
$filter = $this->getFilterFromSpecs( [ 'id' => $id ] + $row );
// Use some black magic to bypass checks
/** @var FilterStore $filterStore */
$filterStore = TestingAccessWrapper::newFromObject( AbuseFilterServices::getFilterStore() );
$this->db->insert(
'abuse_filter',
$filterStore->filterToDatabaseRow( $filter ),
__METHOD__
);
}
/**
* @param array $filterSpecs
* @param array $actions
* @return Filter
*/
private function getFilterFromSpecs( array $filterSpecs, array $actions = [] ): Filter {
$filterSpecs += self::DEFAULT_VALUES;
return new Filter(
new Specs(
$filterSpecs['rules'],
$filterSpecs['comments'],
$filterSpecs['name'],
array_keys( $filterSpecs['actions'] ),
$filterSpecs['group']
),
new Flags(
$filterSpecs['enabled'],
$filterSpecs['deleted'],
$filterSpecs['hidden'],
$filterSpecs['global']
),
$actions,
new LastEditInfo(
$filterSpecs['user'],
$filterSpecs['user_text'],
$filterSpecs['timestamp']
),
$filterSpecs['id'],
$filterSpecs['hit_count'],
$filterSpecs['throttled']
);
}
public static function provideSaveFilter_valid(): array {
return [
[ SCHEMA_COMPAT_OLD ],
[ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD ],
[ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW ],
[ SCHEMA_COMPAT_NEW ],
];
}
/**
* @dataProvider provideSaveFilter_valid
*/
public function testSaveFilter_valid( int $stage ) {
$this->overrideConfigValue( 'AbuseFilterActorTableSchemaMigrationStage', $stage );
$row = [
'id' => null,
'rules' => '/* My rules */',
'name' => 'Some new filter',
'enabled' => false,
'deleted' => true
];
$origFilter = MutableFilter::newDefault();
$newFilter = $this->getFilterFromSpecs( $row );
$status = AbuseFilterServices::getFilterStore()->saveFilter(
$this->getTestSysop()->getUser(), $row['id'], $newFilter, $origFilter
);
$this->assertStatusGood( $status );
$value = $status->getValue();
$this->assertIsArray( $value );
$this->assertCount( 2, $value );
$this->assertContainsOnly( 'int', $value );
}
public function testSaveFilter_invalid() {
$this->overrideConfigValue(
'AbuseFilterActorTableSchemaMigrationStage',
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
$row = [
'id' => null,
'rules' => '1==1',
'name' => 'Restricted action',
];
$actions = [
'degroup' => []
];
// We use restricted actions because that's the last check
$expectedError = 'abusefilter-edit-restricted';
$origFilter = MutableFilter::newDefault();
$newFilter = $this->getFilterFromSpecs( $row, $actions );
$user = $this->getTestUser()->getUser();
// Assign -modify and -modify-global, but not -modify-restricted
$this->overrideUserPermissions( $user, [ 'abusefilter-modify' ] );
$status = AbuseFilterServices::getFilterStore()->saveFilter( $user, $row['id'], $newFilter, $origFilter );
$this->assertStatusWarning( $expectedError, $status );
}
public function testSaveFilter_noChange() {
$this->overrideConfigValue(
'AbuseFilterActorTableSchemaMigrationStage',
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
$row = [
'id' => '1',
'rules' => '/**/',
'name' => 'Mock filter'
];
$filter = $row['id'];
$this->createFilter( $filter );
$origFilter = AbuseFilterServices::getFilterLookup()->getFilter( $filter, false );
$newFilter = $this->getFilterFromSpecs( $row );
$status = AbuseFilterServices::getFilterStore()->saveFilter(
$this->getTestSysop()->getUser(), $filter, $newFilter, $origFilter
);
$this->assertStatusGood( $status );
$this->assertFalse( $status->getValue(), 'Status value should be false' );
}
}
EchoNotifierTest.php 0000666 00000005716 15133473510 0010512 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
use MediaWiki\Extension\AbuseFilter\EchoNotifier;
use MediaWiki\Extension\AbuseFilter\Filter\ExistingFilter;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\Notifications\Model\Event;
use MediaWiki\Title\Title;
use MediaWikiIntegrationTestCase;
/**
* @group Database
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\EchoNotifier
*/
class EchoNotifierTest extends MediaWikiIntegrationTestCase {
private const USER_IDS = [
'1' => 1,
'2' => 42,
];
private function getFilterLookup( int $userID = null ): FilterLookup {
$lookup = $this->createMock( FilterLookup::class );
$lookup->method( 'getFilter' )
->willReturnCallback( function ( $filter, $global ) use ( $userID ) {
$userID ??= self::USER_IDS[ $global ? "global-$filter" : $filter ] ?? 0;
$filterObj = $this->createMock( ExistingFilter::class );
$filterObj->method( 'getUserID' )->willReturn( $userID );
return $filterObj;
} );
return $lookup;
}
public static function provideDataForEvent(): array {
return [
[ true, 1, 1 ],
[ true, 2, 42 ],
[ false, 1, 1 ],
[ false, 2, 42 ],
];
}
/**
* @dataProvider provideDataForEvent
* @covers ::__construct
* @covers ::getDataForEvent
* @covers ::getFilterObject
* @covers ::getTitleForFilter
*/
public function testGetDataForEvent( bool $loaded, int $filter, int $userID ) {
$expectedThrottledActions = [];
$notifier = new EchoNotifier(
$this->getFilterLookup(),
$this->createMock( ConsequencesRegistry::class ),
$loaded
);
[
'type' => $type,
'title' => $title,
'extra' => $extra
] = $notifier->getDataForEvent( $filter );
$this->assertSame( EchoNotifier::EVENT_TYPE, $type );
$this->assertInstanceOf( Title::class, $title );
$this->assertSame( -1, $title->getNamespace() );
[ , $subpage ] = explode( '/', $title->getText(), 2 );
$this->assertSame( (string)$filter, $subpage );
$this->assertSame( [ 'user' => $userID, 'throttled-actions' => $expectedThrottledActions ], $extra );
}
/**
* @covers ::notifyForFilter
*/
public function testNotifyForFilter() {
$this->markTestSkippedIfExtensionNotLoaded( 'Echo' );
// Use a real user, or Echo will throw an exception.
$user = $this->getTestUser()->getUserIdentity();
$notifier = new EchoNotifier(
$this->getFilterLookup( $user->getId() ),
$this->createMock( ConsequencesRegistry::class ),
true
);
$this->assertInstanceOf( Event::class, $notifier->notifyForFilter( 1 ) );
}
/**
* @covers ::notifyForFilter
*/
public function testNotifyForFilter_EchoNotLoaded() {
$lookup = $this->createMock( FilterLookup::class );
$lookup->expects( $this->never() )->method( $this->anything() );
$notifier = new EchoNotifier(
$lookup,
$this->createMock( ConsequencesRegistry::class ),
false
);
$this->assertFalse( $notifier->notifyForFilter( 1 ) );
}
}
FilteredActionsHandlerTest.php 0000666 00000011251 15133473510 0012500 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use Content;
use MediaWiki\Extension\AbuseFilter\BlockedDomainFilter;
use MediaWiki\Extension\AbuseFilter\BlockedDomainStorage;
use MediaWiki\Extension\AbuseFilter\EditRevUpdater;
use MediaWiki\Extension\AbuseFilter\FilterRunner;
use MediaWiki\Extension\AbuseFilter\FilterRunnerFactory;
use MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\VariableGenerator\RunVariableGenerator;
use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Status\Status;
use MediaWiki\Title\Title;
use Message;
use NullStatsdDataFactory;
use RequestContext;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler
* @group Database
*/
class FilteredActionsHandlerTest extends \MediaWikiIntegrationTestCase {
private array $blockedDomains = [ 'foo.com' => true ];
/**
* @dataProvider provideOnEditFilterMergedContent
* @covers ::onEditFilterMergedContent
* @covers \MediaWiki\Extension\AbuseFilter\BlockedDomainFilter
*/
public function testOnEditFilterMergedContent( $urlsAdded, $expected ) {
$this->setMwGlobals( 'wgAbuseFilterEnableBlockedExternalDomain', true );
$filteredActionsHandler = $this->getFilteredActionsHandler( $urlsAdded );
$context = RequestContext::getMain();
$context->setTitle( Title::newFromText( 'TestPage' ) );
$content = $this->createMock( Content::class );
$user = $this->getTestUser()->getUser();
$status = Status::newGood();
$res = $filteredActionsHandler->onEditFilterMergedContent(
$context,
$content,
$status,
'Edit summary',
$user,
false
);
$this->assertSame( $expected, $res );
$this->assertSame( $expected, $status->isOK() );
if ( !$expected ) {
// If it's failing, it should report the URL somewhere
$this->assertStringContainsString(
'foo.com',
$status->getErrors()[0]['message']->toString( Message::FORMAT_PLAIN )
);
}
}
public static function provideOnEditFilterMergedContent() {
return [
'subdomain of blocked domain' => [ 'https://bar.foo.com', false ],
'bare domain with nothing' => [ 'https://foo.com', false ],
'blocked domain with path' => [ 'https://foo.com/foo/', false ],
'blocked domain with parameters' => [ 'https://foo.com?foo=bar', false ],
'blocked domain with path and parameters' => [ 'https://foo.com/foo/?foo=bar', false ],
'blocked domain with port' => [ 'https://foo.com:9000', false ],
'blocked domain as uppercase' => [ 'https://FOO.com', false ],
'unusual protocol' => [ 'ftp://foo.com', false ],
'mailto is special' => [ 'mailto://user@foo.com', false ],
'domain not blocked' => [ 'https://foo.bar.com', true ],
'domain not blocked but it might mistake the subdomain' => [ 'https://foo.com.bar.com', true ],
];
}
private function getFilteredActionsHandler( $urlsAdded ): FilteredActionsHandler {
$mockRunner = $this->createMock( FilterRunner::class );
$mockRunner->method( 'run' )
->willReturn( Status::newGood() );
$filterRunnerFactory = $this->createMock( FilterRunnerFactory::class );
$filterRunnerFactory->method( 'newRunner' )
->willReturn( $mockRunner );
$vars = new VariableHolder();
$vars->setVar( 'added_links', AFPData::newFromPHPVar( $urlsAdded ) );
$variableGenerator = $this->createMock( RunVariableGenerator::class );
$variableGenerator->method( 'getEditVars' )
->willReturn( $vars );
$variableGeneratorFactory = $this->createMock( VariableGeneratorFactory::class );
$variableGeneratorFactory->method( 'newRunGenerator' )
->willReturn( $variableGenerator );
$editRevUpdater = $this->createMock( EditRevUpdater::class );
$variablesManager = $this->createMock( VariablesManager::class );
$variablesManager->method( 'getVar' )
->willReturnCallback( fn( $unused, $vars ) => AFPData::newFromPHPVar( $urlsAdded ) );
$blockedDomainStorage = $this->createMock( BlockedDomainStorage::class );
$blockedDomainStorage->method( 'loadComputed' )
->willReturn( $this->blockedDomains );
$blockedDomainFilter = new BlockedDomainFilter( $variablesManager, $blockedDomainStorage );
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->method( 'userHasRight' )
->willReturn( false );
return new FilteredActionsHandler(
new NullStatsdDataFactory(),
$filterRunnerFactory,
$variableGeneratorFactory,
$editRevUpdater,
$blockedDomainFilter,
$permissionManager
);
}
}
Parser/ParserEquivsetTest.php 0000666 00000006361 15133473510 0012347 0 ustar 00 <?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*
* @license GPL-2.0-or-later
*/
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Parser;
use EmptyBagOStuff;
use LanguageEn;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\Parser\FilterEvaluator;
use MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWikiIntegrationTestCase;
use NullStatsdDataFactory;
use Wikimedia\Equivset\Equivset;
/**
* Tests that require Equivset, separated from the parser unit tests.
*
* @group Test
* @group AbuseFilter
* @group AbuseFilterParser
*
* @covers \MediaWiki\Extension\AbuseFilter\Parser\FilterEvaluator
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AFPTreeParser
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AFPTreeNode
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AFPSyntaxTree
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AFPParserState
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AbuseFilterTokenizer
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AFPToken
* @covers \MediaWiki\Extension\AbuseFilter\Parser\AFPData
* @covers \MediaWiki\Extension\AbuseFilter\Parser\SyntaxChecker
*/
class ParserEquivsetTest extends MediaWikiIntegrationTestCase {
private function getParser(): FilterEvaluator {
// We're not interested in caching or logging; tests should call respectively setCache
// and setLogger if they want to test any of those.
$contLang = new LanguageEn();
$cache = new EmptyBagOStuff();
$logger = new \Psr\Log\NullLogger();
$keywordsManager = AbuseFilterServices::getKeywordsManager();
$varManager = new VariablesManager(
$keywordsManager,
$this->createMock( LazyVariableComputer::class )
);
$evaluator = new FilterEvaluator(
$contLang,
$cache,
$logger,
$keywordsManager,
$varManager,
new NullStatsdDataFactory(),
new Equivset(),
1000
);
$evaluator->toggleConditionLimit( false );
return $evaluator;
}
/**
* @param string $rule The rule to parse
* @dataProvider provideGenericTests
*/
public function testGeneric( $rule ) {
$this->assertTrue( $this->getParser()->parse( $rule ) );
}
public static function provideGenericTests() {
$testPath = __DIR__ . "/../../../parserTestsEquivset";
$testFiles = glob( $testPath . "/*.t" );
foreach ( $testFiles as $testFile ) {
$testName = basename( substr( $testFile, 0, -2 ) );
$rule = trim( file_get_contents( $testFile ) );
yield $testName => [ $rule ];
}
}
}
AbuseFilterExtensionJsonTest.php 0000666 00000001260 15133473510 0013056 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use ExtensionRegistry;
use MediaWiki\Tests\ExtensionJsonTestBase;
/**
* @group Test
* @group AbuseFilter
* @coversNothing
*/
class AbuseFilterExtensionJsonTest extends ExtensionJsonTestBase {
/** @inheritDoc */
protected string $extensionJsonPath = __DIR__ . '/../../../extension.json';
public function provideHookHandlerNames(): iterable {
foreach ( $this->getExtensionJson()['HookHandlers'] ?? [] as $hookHandlerName => $specification ) {
if ( $hookHandlerName === 'UserMerge' && !ExtensionRegistry::getInstance()->isLoaded( 'UserMerge' ) ) {
continue;
}
yield [ $hookHandlerName ];
}
}
}
FilterRunnerTest.php 0000666 00000007734 15133473510 0010555 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory;
use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagger;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutorFactory;
use MediaWiki\Extension\AbuseFilter\EditStashCache;
use MediaWiki\Extension\AbuseFilter\EmergencyCache;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\FilterProfiler;
use MediaWiki\Extension\AbuseFilter\FilterRunner;
use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWiki\Title\Title;
use MediaWikiIntegrationTestCase;
use Psr\Log\NullLogger;
use User;
use WebRequest;
/**
* @group Test
* @group AbuseFilter
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\FilterRunner
* @covers ::__construct
*/
class FilterRunnerTest extends MediaWikiIntegrationTestCase {
/**
* @param ChangeTagger|null $changeTagger
* @param EditStashCache|null $cache
* @param VariableHolder|null $vars
* @param string $group
* @return FilterRunner
*/
private function getRunner(
ChangeTagger $changeTagger = null,
EditStashCache $cache = null,
VariableHolder $vars = null,
$group = 'default'
): FilterRunner {
$opts = new ServiceOptions(
FilterRunner::CONSTRUCTOR_OPTIONS,
[
'AbuseFilterValidGroups' => [ 'default' ],
'AbuseFilterCentralDB' => false,
'AbuseFilterIsCentral' => false,
'AbuseFilterConditionLimit' => 1000,
]
);
if ( $cache === null ) {
$cache = $this->createMock( EditStashCache::class );
$cache->method( 'seek' )->willReturn( false );
}
$request = $this->createMock( WebRequest::class );
$request->method( 'getIP' )->willReturn( '127.0.0.1' );
$user = $this->createMock( User::class );
$user->method( 'getRequest' )->willReturn( $request );
return new FilterRunner(
new AbuseFilterHookRunner( $this->createHookContainer() ),
$this->createMock( FilterProfiler::class ),
$changeTagger ?? $this->createMock( ChangeTagger::class ),
$this->createMock( FilterLookup::class ),
$this->createMock( RuleCheckerFactory::class ),
$this->createMock( ConsequencesExecutorFactory::class ),
$this->createMock( AbuseLoggerFactory::class ),
$this->createMock( VariablesManager::class ),
$this->createMock( VariableGeneratorFactory::class ),
$this->createMock( EmergencyCache::class ),
[],
$cache,
new NullLogger(),
$opts,
$user,
$this->createMock( Title::class ),
$vars ?? VariableHolder::newFromArray( [ 'action' => 'edit' ] ),
$group
);
}
/**
* @covers ::__construct
*/
public function testConstructor_invalidGroup() {
$invalidGroup = 'invalid-group';
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( $invalidGroup );
$this->getRunner( null, null, new VariableHolder(), $invalidGroup );
}
/**
* @covers ::__construct
*/
public function testConstructor_noAction() {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( 'variable is not set' );
$this->getRunner( null, null, new VariableHolder() );
}
/**
* @covers ::run
* @covers ::checkAllFilters
*/
public function testConditionsLimit() {
$cache = $this->createMock( EditStashCache::class );
$cache->method( 'seek' )->willReturn( [
'vars' => [],
'data' => [
'matches' => [],
'condCount' => 2000,
'runtime' => 100.0,
'profiling' => []
]
] );
$changeTagger = $this->createMock( ChangeTagger::class );
$changeTagger->expects( $this->once() )->method( 'addConditionsLimitTag' );
$runner = $this->getRunner( $changeTagger, $cache );
$this->assertStatusGood( $runner->run() );
}
}
Hooks/CheckUserHandlerTest.php 0000666 00000007477 15133473510 0012377 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit\Hooks;
use MediaWiki\Extension\AbuseFilter\FilterUser;
use MediaWiki\Extension\AbuseFilter\Hooks\Handlers\CheckUserHandler;
use MediaWiki\User\UserIdentityUtils;
use MediaWiki\User\UserIdentityValue;
use MediaWikiIntegrationTestCase;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Hooks\Handlers\CheckUserHandler
* @covers ::__construct
*/
class CheckUserHandlerTest extends MediaWikiIntegrationTestCase {
protected function setUp(): void {
parent::setUp();
$this->markTestSkippedIfExtensionNotLoaded( 'CheckUser' );
}
private function getCheckUserHandler(): CheckUserHandler {
$filterUser = $this->createMock( FilterUser::class );
$filterUser->method( 'getUserIdentity' )
->willReturn( new UserIdentityValue( 1, 'Abuse filter' ) );
$userIdentityUtils = $this->createMock( UserIdentityUtils::class );
$userIdentityUtils->method( 'isNamed' )
->willReturnCallback( static function ( $name ) {
return $name !== '*12345';
} );
return new CheckUserHandler( $filterUser, $userIdentityUtils );
}
private function commonInsertHookAssertions( $shouldChange, $agentField, $ip, $xff, $row ) {
if ( $shouldChange ) {
$this->assertSame(
'127.0.0.1',
$ip,
'IP should have changed to 127.0.0.1 because the abuse filter user is making the action.'
);
$this->assertFalse(
$xff,
'XFF string should have been blanked because the abuse filter user is making the action.'
);
$this->assertSame(
'',
$row[$agentField],
'User agent should have been blanked because the abuse filter is making the action.'
);
} else {
$this->assertSame(
'1.2.3.4',
$ip,
'IP should have not been modified by AbuseFilter handling the checkuser insert row hook.'
);
$this->assertSame(
'1.2.3.5',
$xff,
'XFF should have not been modified by AbuseFilter handling the checkuser insert row hook.'
);
$this->assertArrayNotHasKey(
$agentField,
$row,
'User agent should have not been modified by AbuseFilter handling the checkuser insert row hook.'
);
}
}
/**
* @covers ::onCheckUserInsertChangesRow
* @dataProvider provideDataForCheckUserInsertHooks
*/
public function testOnCheckUserInsertChangesRow( $user, $shouldChange ) {
$checkUserHandler = $this->getCheckUserHandler();
$ip = '1.2.3.4';
$xff = '1.2.3.5';
$row = [];
$checkUserHandler->onCheckUserInsertChangesRow( $ip, $xff, $row, $user, null );
$this->commonInsertHookAssertions( $shouldChange, 'cuc_agent', $ip, $xff, $row );
}
/**
* @covers ::onCheckUserInsertPrivateEventRow
* @dataProvider provideDataForCheckUserInsertHooks
*/
public function testOnCheckUserInsertPrivateEventRow( $user, $shouldChange ) {
$checkUserHandler = $this->getCheckUserHandler();
$ip = '1.2.3.4';
$xff = '1.2.3.5';
$row = [];
$checkUserHandler->onCheckUserInsertPrivateEventRow( $ip, $xff, $row, $user, null );
$this->commonInsertHookAssertions( $shouldChange, 'cupe_agent', $ip, $xff, $row );
}
/**
* @covers ::onCheckUserInsertLogEventRow
* @dataProvider provideDataForCheckUserInsertHooks
*/
public function testOnCheckUserInsertLogEventRow( $user, $shouldChange ) {
$checkUserHandler = $this->getCheckUserHandler();
$ip = '1.2.3.4';
$xff = '1.2.3.5';
$row = [];
$checkUserHandler->onCheckUserInsertLogEventRow( $ip, $xff, $row, $user, 1, null );
$this->commonInsertHookAssertions( $shouldChange, 'cule_agent', $ip, $xff, $row );
}
public static function provideDataForCheckUserInsertHooks() {
return [
'Anonymous user' => [ UserIdentityValue::newAnonymous( '127.0.0.1' ), false ],
'Temporary user' => [ new UserIdentityValue( 3, '*12345' ), false ],
'Registered user' => [ new UserIdentityValue( 2, 'Test' ), false ],
'Abuse filter user' => [ new UserIdentityValue( 1, 'Abuse filter' ), true ],
];
}
}
FilterValidatorTest.php 0000666 00000003337 15133473510 0011224 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\FilterValidator;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
use MediaWikiIntegrationTestCase;
/**
* @group Test
* @group AbuseFilter
* @group Database
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\FilterValidator
* @covers ::__construct()
*/
class FilterValidatorTest extends MediaWikiIntegrationTestCase {
/**
* @todo Make this a unit test once static methods in ChangeTags are moved to a service
* @param string[] $tags
* @param string|null $expected
* @covers ::checkAllTags
* @dataProvider provideAllTags
*/
public function testCheckAllTags( array $tags, ?string $expected ) {
$validator = new FilterValidator(
AbuseFilterServices::getChangeTagValidator(),
$this->createMock( RuleCheckerFactory::class ),
$this->createMock( AbuseFilterPermissionManager::class ),
new ServiceOptions(
FilterValidator::CONSTRUCTOR_OPTIONS,
[
'AbuseFilterActionRestrictions' => [],
'AbuseFilterValidGroups' => [ 'default' ]
]
)
);
$status = $validator->checkAllTags( $tags );
$actualError = $status->isGood() ? null : $status->getErrors()[0]['message'];
$this->assertSame( $expected, $actualError );
}
public static function provideAllTags() {
$invalidTags = [
'a|b',
'mw-undo',
'abusefilter-condition-limit',
'valid_tag',
];
$firstTagError = 'tags-create-invalid-chars';
yield 'invalid' => [ $invalidTags, $firstTagError ];
yield 'valid' => [ [ 'fooooobar', 'foooobaz' ], null ];
}
}
AbuseFilterServicesTest.php 0000666 00000001143 15133473510 0012033 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Tests\ExtensionServicesTestBase;
/**
* @group Test
* @group AbuseFilter
* @covers \MediaWiki\Extension\AbuseFilter\AbuseFilterServices
*/
class AbuseFilterServicesTest extends ExtensionServicesTestBase {
/** @inheritDoc */
protected string $className = AbuseFilterServices::class;
/** @inheritDoc */
protected string $serviceNamePrefix = 'AbuseFilter';
/** @inheritDoc */
protected array $serviceNamesWithoutMethods = [
'AbuseFilterRunnerFactory',
];
}
Api/EvalExpressionTest.php 0000666 00000006503 15133473510 0011607 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Api;
use ApiTestCase;
use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException;
use MediaWiki\Extension\AbuseFilter\Parser\FilterEvaluator;
use MediaWiki\Extension\AbuseFilter\Parser\ParserStatus;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Api\EvalExpression
* @covers ::__construct
* @group medium
*/
class EvalExpressionTest extends ApiTestCase {
use AbuseFilterApiTestTrait;
use MockAuthorityTrait;
/**
* @covers ::execute
*/
public function testExecute_noPermissions() {
$this->expectApiErrorCode( 'permissiondenied' );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory() );
$this->doApiRequest( [
'action' => 'abusefilterevalexpression',
'expression' => 'sampleExpression',
], null, null, $this->mockRegisteredNullAuthority() );
}
/**
* @covers ::execute
* @covers ::evaluateExpression
*/
public function testExecute_error() {
$this->expectApiErrorCode( 'abusefilter-tools-syntax-error' );
$expression = 'sampleExpression';
$status = new ParserStatus( $this->createMock( InternalException::class ), [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->method( 'checkSyntax' )->with( $expression )
->willReturn( $status );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$this->doApiRequest( [
'action' => 'abusefilterevalexpression',
'expression' => $expression,
] );
}
/**
* @covers ::execute
* @covers ::evaluateExpression
*/
public function testExecute_Ok() {
$expression = 'sampleExpression';
$status = new ParserStatus( null, [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->method( 'checkSyntax' )->with( $expression )
->willReturn( $status );
$ruleChecker->expects( $this->once() )->method( 'evaluateExpression' )
->willReturn( 'output' );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$result = $this->doApiRequest( [
'action' => 'abusefilterevalexpression',
'expression' => $expression,
'prettyprint' => false,
] );
$this->assertArrayEquals(
[
'abusefilterevalexpression' => [
'result' => "'output'"
]
],
$result[0],
false,
true
);
}
/**
* @covers ::execute
* @covers ::evaluateExpression
*/
public function testExecute_OkAndPrettyPrint() {
$expression = 'sampleExpression';
$status = new ParserStatus( null, [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->method( 'checkSyntax' )->with( $expression )
->willReturn( $status );
$ruleChecker->expects( $this->once() )->method( 'evaluateExpression' )
->willReturn( [ 'value1', 2 ] );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$result = $this->doApiRequest( [
'action' => 'abusefilterevalexpression',
'expression' => $expression,
'prettyprint' => true,
] );
$this->assertArrayEquals(
[
'abusefilterevalexpression' => [
'result' => "[\n\t0 => 'value1',\n\t1 => 2\n]"
]
],
$result[0],
false,
true
);
}
}
Api/UnblockAutopromoteTest.php 0000666 00000007224 15133473510 0012475 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Api;
use ApiTestCase;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\Extension\AbuseFilter\BlockAutopromoteStore;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
use MediaWiki\User\UserIdentityValue;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Api\UnblockAutopromote
* @covers ::__construct
* @group medium
* @group Database
*/
class UnblockAutopromoteTest extends ApiTestCase {
use MockAuthorityTrait;
/**
* @covers ::execute
*/
public function testExecute_noPermissions() {
$this->expectApiErrorCode( 'permissiondenied' );
$store = $this->createMock( BlockAutopromoteStore::class );
$store->expects( $this->never() )->method( 'unblockAutopromote' );
$this->setService( BlockAutopromoteStore::SERVICE_NAME, $store );
$this->doApiRequestWithToken( [
'action' => 'abusefilterunblockautopromote',
'user' => 'User'
], null, $this->mockRegisteredAuthorityWithoutPermissions( [ 'abusefilter-modify' ] ), 'csrf' );
}
/**
* @covers ::execute
*/
public function testExecute_invalidUser() {
$invalid = 'invalid#username';
$this->expectApiErrorCode( 'baduser' );
$store = $this->createMock( BlockAutopromoteStore::class );
$store->expects( $this->never() )->method( 'unblockAutopromote' );
$this->setService( BlockAutopromoteStore::SERVICE_NAME, $store );
$this->doApiRequestWithToken( [
'action' => 'abusefilterunblockautopromote',
'user' => $invalid
], null, $this->mockRegisteredNullAuthority(), 'csrf' );
}
/**
* @covers ::execute
*/
public function testExecute_blocked() {
$this->expectApiErrorCode( 'blocked' );
$block = $this->createMock( DatabaseBlock::class );
$block->method( 'getExpiry' )->willReturn( wfTimestamp( TS_MW, time() + 100000 ) );
$block->method( 'isSitewide' )->willReturn( true );
$block->method( 'getReasonComment' )->willReturn( CommentStoreComment::newUnsavedComment( 'test' ) );
$blockedUser = $this->mockUserAuthorityWithBlock(
new UserIdentityValue( 42, 'Blocked user' ),
$block,
[ 'writeapi', 'abusefilter-modify' ]
);
$store = $this->createMock( BlockAutopromoteStore::class );
$store->expects( $this->never() )->method( 'unblockAutopromote' );
$this->setService( BlockAutopromoteStore::SERVICE_NAME, $store );
$this->doApiRequestWithToken( [
'action' => 'abusefilterunblockautopromote',
'user' => 'User'
], null, $blockedUser, 'csrf' );
}
/**
* @covers ::execute
*/
public function testExecute_nothingToDo() {
$target = 'User';
$user = $this->mockRegisteredUltimateAuthority();
$this->expectApiErrorCode( 'notsuspended' );
$store = $this->createMock( BlockAutopromoteStore::class );
$store->expects( $this->once() )
->method( 'unblockAutopromote' )
->willReturn( false );
$this->setService( BlockAutopromoteStore::SERVICE_NAME, $store );
$this->doApiRequestWithToken( [
'action' => 'abusefilterunblockautopromote',
'user' => $target
], null, $user, 'csrf' );
}
/**
* @covers ::execute
*/
public function testExecute_success() {
$target = 'User';
$user = $this->mockRegisteredUltimateAuthority();
$store = $this->createMock( BlockAutopromoteStore::class );
$store->expects( $this->once() )
->method( 'unblockAutopromote' )
->willReturn( true );
$this->setService( BlockAutopromoteStore::SERVICE_NAME, $store );
$result = $this->doApiRequestWithToken( [
'action' => 'abusefilterunblockautopromote',
'user' => $target
], null, $user, 'csrf' );
$this->assertArrayEquals(
[ 'abusefilterunblockautopromote' => [ 'user' => $target ] ],
$result[0],
false,
true
);
}
}
Api/QueryAbuseLogTest.php 0000666 00000000726 15133473510 0011370 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Api;
use ApiTestCase;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Api\QueryAbuseLog
* @group medium
* @group Database
* @todo Extend this
*/
class QueryAbuseLogTest extends ApiTestCase {
/**
* @covers ::__construct
*/
public function testConstruct() {
$this->doApiRequest( [
'action' => 'query',
'list' => 'abuselog',
] );
$this->addToAssertionCount( 1 );
}
}
Api/CheckMatchTest.php 0000666 00000005454 15133473510 0010636 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Api;
use ApiTestCase;
use FormatJson;
use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException;
use MediaWiki\Extension\AbuseFilter\Parser\FilterEvaluator;
use MediaWiki\Extension\AbuseFilter\Parser\ParserStatus;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerStatus;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Api\CheckMatch
* @covers ::__construct
* @group medium
*/
class CheckMatchTest extends ApiTestCase {
use AbuseFilterApiTestTrait;
use MockAuthorityTrait;
/**
* @covers ::execute
*/
public function testExecute_noPermissions() {
$this->expectApiErrorCode( 'permissiondenied' );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory() );
$this->doApiRequest( [
'action' => 'abusefiltercheckmatch',
'filter' => 'sampleFilter',
'vars' => FormatJson::encode( [] ),
], null, null, $this->mockRegisteredNullAuthority() );
}
public static function provideExecuteOk() {
return [
'matched' => [ true ],
'no match' => [ false ],
];
}
/**
* @dataProvider provideExecuteOk
* @covers ::execute
*/
public function testExecute_Ok( bool $expected ) {
$filter = 'sampleFilter';
$checkStatus = new ParserStatus( null, [], 1 );
$resultStatus = new RuleCheckerStatus( $expected, false, null, [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->expects( $this->once() )
->method( 'checkSyntax' )->with( $filter )
->willReturn( $checkStatus );
$ruleChecker->expects( $this->once() )
->method( 'checkConditions' )->with( $filter )
->willReturn( $resultStatus );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$result = $this->doApiRequest( [
'action' => 'abusefiltercheckmatch',
'filter' => $filter,
'vars' => FormatJson::encode( [] ),
] );
$this->assertArrayEquals(
[
'abusefiltercheckmatch' => [
'result' => $expected
]
],
$result[0],
false,
true
);
}
/**
* @covers ::execute
*/
public function testExecute_error() {
$this->expectApiErrorCode( 'badsyntax' );
$filter = 'sampleFilter';
$status = new ParserStatus( $this->createMock( InternalException::class ), [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->expects( $this->once() )
->method( 'checkSyntax' )->with( $filter )
->willReturn( $status );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$this->doApiRequest( [
'action' => 'abusefiltercheckmatch',
'filter' => $filter,
'vars' => FormatJson::encode( [] ),
] );
}
}
Api/CheckSyntaxTest.php 0000666 00000006720 15133473510 0011065 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Api;
use ApiTestCase;
use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException;
use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleWarning;
use MediaWiki\Extension\AbuseFilter\Parser\FilterEvaluator;
use MediaWiki\Extension\AbuseFilter\Parser\ParserStatus;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Api\CheckSyntax
* @covers ::__construct
* @group medium
*/
class CheckSyntaxTest extends ApiTestCase {
use AbuseFilterApiTestTrait;
use MockAuthorityTrait;
/**
* @covers ::execute
*/
public function testExecute_noPermissions() {
$this->expectApiErrorCode( 'permissiondenied' );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory() );
$this->doApiRequest( [
'action' => 'abusefilterchecksyntax',
'filter' => 'sampleFilter',
], null, null, $this->mockRegisteredNullAuthority() );
}
/**
* @covers ::execute
*/
public function testExecute_Ok() {
$input = 'sampleFilter';
$status = new ParserStatus( null, [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->method( 'checkSyntax' )->with( $input )
->willReturn( $status );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$result = $this->doApiRequest( [
'action' => 'abusefilterchecksyntax',
'filter' => $input,
] );
$this->assertArrayEquals(
[ 'abusefilterchecksyntax' => [ 'status' => 'ok' ] ],
$result[0],
false,
true
);
}
/**
* @covers ::execute
*/
public function testExecute_OkAndWarnings() {
$input = 'sampleFilter';
$warnings = [
new UserVisibleWarning( 'exception-1', 3, [] ),
new UserVisibleWarning( 'exception-2', 8, [ 'param' ] ),
];
$status = new ParserStatus( null, $warnings, 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->method( 'checkSyntax' )->with( $input )
->willReturn( $status );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$result = $this->doApiRequest( [
'action' => 'abusefilterchecksyntax',
'filter' => $input,
] );
$this->assertArrayEquals(
[
'abusefilterchecksyntax' => [
'status' => 'ok',
'warnings' => [
[
'message' => '⧼abusefilter-parser-warning-exception-1⧽',
'character' => 3,
],
[
'message' => '⧼abusefilter-parser-warning-exception-2⧽',
'character' => 8,
],
]
]
],
$result[0],
false,
true
);
}
/**
* @covers ::execute
*/
public function testExecute_error() {
$input = 'sampleFilter';
$exception = new UserVisibleException( 'error-id', 4, [] );
$status = new ParserStatus( $exception, [], 1 );
$ruleChecker = $this->createMock( FilterEvaluator::class );
$ruleChecker->method( 'checkSyntax' )->with( $input )
->willReturn( $status );
$this->setService( RuleCheckerFactory::SERVICE_NAME, $this->getRuleCheckerFactory( $ruleChecker ) );
$result = $this->doApiRequest( [
'action' => 'abusefilterchecksyntax',
'filter' => $input,
] );
$this->assertArrayEquals(
[
'abusefilterchecksyntax' => [
'status' => 'error',
'message' => '⧼abusefilter-exception-error-id⧽',
'character' => 4
]
],
$result[0],
false,
true
);
}
}
Api/AbuseFilterApiTestTrait.php 0000666 00000001445 15133473510 0012503 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Api;
use MediaWiki\Extension\AbuseFilter\Parser\FilterEvaluator;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
/**
* This trait contains helper methods for Api integration tests.
*/
trait AbuseFilterApiTestTrait {
/**
* @param FilterEvaluator|null $ruleChecker
* @return RuleCheckerFactory
*/
protected function getRuleCheckerFactory( FilterEvaluator $ruleChecker = null ): RuleCheckerFactory {
$factory = $this->createMock( RuleCheckerFactory::class );
if ( $ruleChecker !== null ) {
$factory->expects( $this->atLeastOnce() )
->method( 'newRuleChecker' )
->willReturn( $ruleChecker );
} else {
$factory->expects( $this->never() )->method( 'newRuleChecker' );
}
return $factory;
}
}
Special/SpecialAbuseLogTest.php 0000666 00000011420 15133473510 0012503 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit\Special;
use Generator;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog;
use MediaWiki\Page\PageIdentityValue;
use MediaWiki\Permissions\SimpleAuthority;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\User\UserIdentity;
use MediaWikiIntegrationTestCase;
use stdClass;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog
*/
class SpecialAbuseLogTest extends MediaWikiIntegrationTestCase {
/**
* @param stdClass $row
* @param RevisionRecord $revRec
* @param bool $canSeeHidden
* @param bool $canSeeSuppressed
* @param string $expected
* @dataProvider provideEntryAndVisibility
* @covers ::getEntryVisibilityForUser
*/
public function testGetEntryVisibilityForUser(
stdClass $row,
RevisionRecord $revRec,
bool $canSeeHidden,
bool $canSeeSuppressed,
string $expected
) {
$user = $this->createMock( UserIdentity::class );
$authority = new SimpleAuthority( $user, $canSeeSuppressed ? [ 'viewsuppressed' ] : [] );
$afPermManager = $this->createMock( AbuseFilterPermissionManager::class );
$afPermManager->method( 'canSeeHiddenLogEntries' )->with( $authority )->willReturn( $canSeeHidden );
$revLookup = $this->createMock( RevisionLookup::class );
$revLookup->method( 'getRevisionById' )->willReturn( $revRec );
$this->setService( 'RevisionLookup', $revLookup );
$this->assertSame(
$expected,
SpecialAbuseLog::getEntryVisibilityForUser( $row, $authority, $afPermManager )
);
}
public static function provideEntryAndVisibility(): Generator {
$visibleRow = (object)[ 'afl_rev_id' => 1, 'afl_deleted' => 0 ];
$hiddenRow = (object)[ 'afl_rev_id' => 1, 'afl_deleted' => 1 ];
$page = new PageIdentityValue( 1, NS_MAIN, 'Foo', PageIdentityValue::LOCAL );
$visibleRev = new MutableRevisionRecord( $page );
yield 'Visible entry and rev, cannot see hidden, cannot see suppressed' =>
[ $visibleRow, $visibleRev, false, false, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Visible entry and rev, can see hidden, cannot see suppressed' =>
[ $visibleRow, $visibleRev, true, false, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Visible entry and rev, cannot see hidden, can see suppressed' =>
[ $visibleRow, $visibleRev, false, false, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Visible entry and rev, can see hidden, can see suppressed' =>
[ $visibleRow, $visibleRev, true, false, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Hidden entry, visible rev, can see hidden, cannot see suppressed' =>
[ $hiddenRow, $visibleRev, true, false, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Hidden entry, visible rev, cannot see hidden, cannot see suppressed' =>
[ $hiddenRow, $visibleRev, false, false, SpecialAbuseLog::VISIBILITY_HIDDEN ];
yield 'Hidden entry, visible rev, can see hidden, can see suppressed' =>
[ $hiddenRow, $visibleRev, true, true, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Hidden entry, visible rev, cannot see hidden, can see suppressed' =>
[ $hiddenRow, $visibleRev, false, true, SpecialAbuseLog::VISIBILITY_HIDDEN ];
$userSupRev = new MutableRevisionRecord( $page );
$userSupRev->setVisibility( RevisionRecord::SUPPRESSED_USER );
yield 'Hidden entry, user suppressed rev, can see hidden, cannot see suppressed' =>
[ $hiddenRow, $userSupRev, true, false, SpecialAbuseLog::VISIBILITY_HIDDEN_IMPLICIT ];
yield 'Hidden entry, user suppressed rev, cannot see hidden, cannot see suppressed' =>
[ $hiddenRow, $userSupRev, false, false, SpecialAbuseLog::VISIBILITY_HIDDEN ];
yield 'Hidden entry, user suppressed rev, can see hidden, can see suppressed' =>
[ $hiddenRow, $userSupRev, true, true, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Hidden entry, user suppressed rev, cannot see hidden, can see suppressed' =>
[ $hiddenRow, $userSupRev, false, true, SpecialAbuseLog::VISIBILITY_HIDDEN ];
$allSuppRev = new MutableRevisionRecord( $page );
$allSuppRev->setVisibility( RevisionRecord::SUPPRESSED_ALL );
yield 'Hidden entry, all suppressed rev, can see hidden, cannot see suppressed' =>
[ $hiddenRow, $allSuppRev, true, false, SpecialAbuseLog::VISIBILITY_HIDDEN_IMPLICIT ];
yield 'Hidden entry, all suppressed rev, cannot see hidden, cannot see suppressed' =>
[ $hiddenRow, $allSuppRev, false, false, SpecialAbuseLog::VISIBILITY_HIDDEN ];
yield 'Hidden entry, all suppressed rev, can see hidden, can see suppressed' =>
[ $hiddenRow, $allSuppRev, true, true, SpecialAbuseLog::VISIBILITY_VISIBLE ];
yield 'Hidden entry, all suppressed rev, cannot see hidden, can see suppressed' =>
[ $hiddenRow, $allSuppRev, false, true, SpecialAbuseLog::VISIBILITY_HIDDEN ];
}
}
Special/SpecialAbuseFilterTest.php 0000666 00000005752 15133473510 0013222 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Integration\Special;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseFilter;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewDiff;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewEdit;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewExamine;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewHistory;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewImport;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewList;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewRevert;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTestBatch;
use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTools;
use MediaWiki\MediaWikiServices;
use SpecialPageTestBase;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseFilter
*/
class SpecialAbuseFilterTest extends SpecialPageTestBase {
/**
* @covers ::instantiateView
* @covers ::__construct
* @covers \MediaWiki\Extension\AbuseFilter\Special\AbuseFilterSpecialPage::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterView::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewDiff::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewEdit::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewExamine::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewHistory::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewImport::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewList::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewRevert::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTestBatch::__construct
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTools::__construct
* @dataProvider provideInstantiateView
*/
public function testInstantiateView( string $viewClass, array $params = [] ) {
$sp = $this->newSpecialPage();
$view = $sp->instantiateView( $viewClass, $params );
$this->assertInstanceOf( $viewClass, $view );
}
public static function provideInstantiateView(): array {
return [
[ AbuseFilterViewDiff::class ],
[ AbuseFilterViewEdit::class, [ 'filter' => 1 ] ],
[ AbuseFilterViewExamine::class ],
[ AbuseFilterViewHistory::class ],
[ AbuseFilterViewImport::class ],
[ AbuseFilterViewList::class ],
[ AbuseFilterViewRevert::class ],
[ AbuseFilterViewTestBatch::class ],
[ AbuseFilterViewTools::class ],
];
}
/**
* @inheritDoc
*/
protected function newSpecialPage(): SpecialAbuseFilter {
$services = MediaWikiServices::getInstance();
$sp = new SpecialAbuseFilter(
$services->getService( AbuseFilterPermissionManager::SERVICE_NAME ),
$services->getObjectFactory()
);
$sp->setLinkRenderer(
$services->getLinkRendererFactory()->create()
);
return $sp;
}
}
includes/Rest/Handler/PageRedirectHandlerTest.php 0000666 00000012724 15133511011 0016052 0 ustar 00 <?php
namespace MediaWiki\Tests\Rest\Handler;
use HashBagOStuff;
use InvalidArgumentException;
use MediaWiki\Rest\RequestData;
use MediaWiki\Rest\RequestInterface;
use MediaWikiIntegrationTestCase;
/**
* @covers \MediaWiki\Rest\Handler\PageSourceHandler
* @covers \MediaWiki\Rest\Handler\PageHTMLHandler
* @covers \MediaWiki\Rest\Handler\Helper\PageRedirectHelper
* @group Database
*/
class PageRedirectHandlerTest extends MediaWikiIntegrationTestCase {
use PageHandlerTestTrait;
use HandlerTestTrait;
use HTMLHandlerTestTrait;
private const WIKITEXT = 'Hello \'\'\'World\'\'\'';
private const HTML = '<p>Hello <b>World</b></p>';
/** @var HashBagOStuff */
private $parserCacheBagOStuff;
protected function setUp(): void {
parent::setUp();
// Clean up these tables after each test
$this->tablesUsed = [
'page',
'revision',
'comment',
'text',
'content'
];
$this->parserCacheBagOStuff = new HashBagOStuff();
}
private function getHandler( $name, RequestInterface $request ) {
switch ( $name ) {
case 'source':
return $this->newPageSourceHandler();
case 'bare':
return $this->newPageSourceHandler();
case 'html':
return $this->newPageHtmlHandler( $request );
case 'with_html':
return $this->newPageHtmlHandler( $request );
case 'history':
return $this->newPageHistoryHandler();
case 'history_count':
return $this->newPageHistoryCountHandler();
case 'links_language':
return $this->newLanguageLinksHandler();
default:
throw new InvalidArgumentException( "Unknown handler: $name" );
}
}
/**
* @dataProvider temporaryRedirectProvider
*/
public function testTemporaryRedirect(
$format, $path, $queryParams, $expectedStatus, $hasBodyRedirectTarget = true
) {
$targetPageTitle = 'PageEndpointTestPage';
$redirectPageTitle = 'RedirectPage';
$this->getExistingTestPage( $targetPageTitle );
$status = $this->editPage( $redirectPageTitle, "#REDIRECT [[$targetPageTitle]]" );
$this->assertStatusOK( $status );
$request = new RequestData(
[
'pathParams' => [ 'title' => $redirectPageTitle ],
'queryParams' => $queryParams
]
);
$handler = $this->getHandler( $format, $request );
$response = $this->executeHandler( $handler, $request, [
'format' => $format,
'path' => $path,
] );
$headerLocation = $response->getHeaderLine( 'location' );
$this->assertEquals( $expectedStatus, $response->getStatusCode() );
if ( $hasBodyRedirectTarget && $expectedStatus === 200 ) {
$body = json_decode( $response->getBody()->getContents() );
$this->assertStringContainsString( $targetPageTitle, $body->redirect_target );
$this->assertUrlQueryParameters( $body->redirect_target, $queryParams );
}
if ( $expectedStatus !== 200 ) {
$this->assertStringContainsString( $targetPageTitle, $headerLocation );
if ( $headerLocation ) {
$this->assertUrlQueryParameters( $headerLocation, $queryParams );
}
}
}
public function temporaryRedirectProvider() {
yield [
'source',
'/page/{title}',
[],
200
];
yield [
'bare',
'/page/{title}/bare',
[],
200
];
yield [
'html',
'/page/{title}/html',
[],
307,
false
];
yield [
'html',
'/page/{title}/html',
[ 'flavor' => 'edit', 'dummy' => 'test' ],
307,
false
];
yield [
'html',
'/page/{title}/html',
[ 'redirect' => 'no' ],
200,
false
];
yield [
'with_html',
'/page/{title}/with_html',
[],
307,
];
yield [
'with_html',
'/page/{title}/with_html',
[ 'flavor' => 'edit', 'dummy' => 'test', 'redirect' => 'no' ],
200
];
}
/**
* @dataProvider permanentRedirectProvider
*/
public function testPermanentRedirect( $format, $path, $extraPathParams = [], $queryParams = [] ) {
$page = $this->getExistingTestPage( 'SourceEndpointTestPage with spaces' );
$this->assertStatusGood( $this->editPage( $page, self::WIKITEXT ),
'Edited a page'
);
$pathParams = [ 'title' => $page->getTitle()->getPrefixedText() ] + $extraPathParams;
$request = new RequestData(
[
'pathParams' => $pathParams,
'queryParams' => $queryParams
]
);
$handler = $this->getHandler( $format, $request );
$response = $this->executeHandler( $handler, $request, [
'format' => $format,
'path' => $path
] );
$headerLocation = $response->getHeaderLine( 'location' );
$this->assertEquals( 301, $response->getStatusCode() );
$this->assertStringContainsString( $page->getTitle()->getPrefixedDBkey(), $headerLocation );
$this->assertUrlQueryParameters( $headerLocation, $queryParams );
}
public static function permanentRedirectProvider() {
yield [ 'source', '/page/{title}', [], [ 'flavor' => 'edit', 'dummy' => 'test' ] ];
yield [ 'bare', '/page/{title}/bare' ];
yield [ 'html', '/page/{title}/html' ];
yield [ 'with_html', '/page/{title}/with_html' ];
yield [ 'history', '/page/{title}/history' ];
yield [ 'history_count', '/page/{title}/history/counts/{type}', [ 'type' => 'edits' ] ];
yield [ 'links_language', '/page/{title}/links/language' ];
}
/**
* @param string $url
* @param array $queryParams
* @return void
*/
private function assertUrlQueryParameters( string $url, array $queryParams ): void {
$parsedUrl = $this->getServiceContainer()->getUrlUtils()->parse( $url );
$urlParameters = [];
if ( is_array( $parsedUrl ) ) {
if ( array_key_exists( 'query', $parsedUrl ) ) {
$urlParameters = wfCgiToArray(
$parsedUrl['query']
);
}
}
$this->assertArrayEquals( $queryParams, $urlParameters );
}
}
includes/Rest/Handler/Helper/PageRedirectHelperTest.php 0000666 00000013560 15133511011 0017132 0 ustar 00 <?php
namespace MediaWiki\Tests\Rest\Handler\Helper;
use MediaWiki\Page\PageIdentity;
use MediaWiki\Page\PageIdentityValue;
use MediaWiki\Page\PageReferenceValue;
use MediaWiki\Page\RedirectStore;
use MediaWiki\Rest\Handler\Helper\PageRedirectHelper;
use MediaWiki\Rest\RequestData;
use MediaWiki\Rest\ResponseFactory;
use MediaWiki\Tests\Rest\Handler\PageHandlerTestTrait;
use MediaWikiIntegrationTestCase;
use Title;
/**
* @covers \MediaWiki\Rest\Handler\Helper\PageRedirectHelper
* @group Database
*/
class PageRedirectHelperTest extends MediaWikiIntegrationTestCase {
use PageHandlerTestTrait;
private function newRedirectHelper( $queryParams = [] ) {
$baseUrl = 'https://example.test/api';
$services = $this->getServiceContainer();
$redirectStore = $this->createNoOpMock( RedirectStore::class, [ 'getRedirectTarget' ] );
$redirectStore->method( 'getRedirectTarget' )
->willReturnCallback( static function ( PageIdentity $page ) use ( $services ) {
if ( str_starts_with( $page->getDBkey(), 'Redirect_to_' ) ) {
$titleParser = $services->getTitleParser();
return $titleParser->parseTitle( substr( $page->getDBkey(), 12 ), $page->getNamespace() );
}
return null;
} );
$responseFactory = new ResponseFactory( [] );
$router = $this->newRouter( $baseUrl );
$request = new RequestData( [ 'queryParams' => $queryParams ] );
return new PageRedirectHelper(
$redirectStore,
$services->getTitleFormatter(),
$responseFactory,
$router,
'/test/{title}',
$request,
$services->getLanguageConverterFactory()
);
}
public static function provideGetTargetUrl() {
yield [ 'Föö+Bar', null, 'https://example.test/api/test/F%C3%B6%C3%B6%2BBar' ];
yield [ 'Föö+Bar', [ 'a' => 1 ], 'https://example.test/api/test/F%C3%B6%C3%B6%2BBar?a=1' ];
$page = PageReferenceValue::localReference( NS_TALK, 'Q/A' );
yield [ $page, null, 'https://example.test/api/test/Talk%3AQ%2FA' ];
}
/**
* @dataProvider provideGetTargetUrl
*/
public function testGetTargetUrl( $title, $queryParams, $expectedUrl ) {
$helper = $this->newRedirectHelper( $queryParams ?: [] );
$this->assertSame( $expectedUrl, $helper->getTargetUrl( $title ) );
}
public static function provideNormalizationRedirect() {
$page = new PageIdentityValue( 7, NS_MAIN, 'Foo', false );
yield [ $page, 'foo', 'https://example.test/api/test/Foo' ];
$page = new PageIdentityValue( 7, NS_MAIN, 'Foo', false );
yield [ $page, 'Foo', null ];
$page = new PageIdentityValue( 7, NS_TALK, 'Foo_bar/baz', false );
yield [ $page, 'Talk:Foo bar/baz', 'https://example.test/api/test/Talk%3AFoo_bar%2Fbaz' ];
$page = new PageIdentityValue( 7, NS_TALK, 'Foo_bar/baz', false );
yield [ $page, 'Talk:Foo_bar/baz', null ];
}
/**
* @dataProvider provideNormalizationRedirect
*/
public function testNormalizationRedirect(
PageIdentity $page,
string $title,
?string $expectedUrl
) {
$helper = $this->newRedirectHelper();
$resp = $helper->createNormalizationRedirectResponseIfNeeded( $page, $title );
if ( $expectedUrl === null ) {
$this->assertNull( $resp );
} else {
$this->assertNotNull( $resp );
$this->assertSame( $expectedUrl, $resp->getHeaderLine( 'Location' ) );
$this->assertSame( 301, $resp->getStatusCode() );
}
}
public static function provideWikiRedirect() {
$page = new PageIdentityValue( 7, NS_MAIN, 'Redirect_to_foo', false );
yield [ $page, 'https://example.test/api/test/Foo' ];
$page = new PageIdentityValue( 7, NS_MAIN, 'foo', false );
yield [ $page, null ];
}
/**
* @dataProvider provideWikiRedirect
*/
public function testWikiRedirect(
PageIdentity $page,
?string $expectedUrl
) {
$helper = $this->newRedirectHelper();
$helper->setFollowWikiRedirects( true );
$target = $helper->getWikiRedirectTargetUrl( $page );
$resp1 = $helper->createWikiRedirectResponseIfNeeded( $page );
$resp2 = $helper->createRedirectResponseIfNeeded( $page, $page->getDBkey() );
if ( $expectedUrl === null ) {
$this->assertNull( $target );
$this->assertNull( $resp1 );
$this->assertNull( $resp2 );
} else {
$this->assertSame( $expectedUrl, $target );
$this->assertNotNull( $resp1 );
$this->assertSame( $expectedUrl, $resp1->getHeaderLine( 'Location' ) );
$this->assertSame( 307, $resp1->getStatusCode() );
$this->assertNotNull( $resp2 );
$this->assertSame( $expectedUrl, $resp2->getHeaderLine( 'Location' ) );
$this->assertSame( 307, $resp2->getStatusCode() );
}
}
public function testVariantRedirect() {
$page = $this->getNonexistingTestPage( 'TestPage' );
// NOTE: "TestPage" variant to en-x-piglatin is "EsttayAgepay"
$this->insertPage( Title::newFromText( 'EsttayAgepay' ) );
$helper = $this->newRedirectHelper();
$helper->setFollowWikiRedirects( true );
$resp = $helper->createRedirectResponseIfNeeded( $page, $page->getDBkey() );
$this->assertNotNull( $resp );
$this->assertSame(
'https://example.test/api/test/EsttayAgepay',
$resp->getHeaderLine( 'Location' )
);
$this->assertSame( 307, $resp->getStatusCode() );
}
public function testWikiRedirectDisabled() {
$page = new PageIdentityValue( 7, NS_MAIN, 'Redirect_to_foo', false );
// We assume that wiki redirect handling is disabled per default.
$helper = $this->newRedirectHelper();
$target = $helper->getWikiRedirectTargetUrl( $page );
$this->assertNotNull( $target, 'getWikiRedirectTargetUrl() should not be disabled' );
$resp = $helper->createWikiRedirectResponseIfNeeded( $page );
$this->assertNotNull( $resp, 'createWikiRedirectResponseIfNeeded() should not be disabled' );
$resp = $helper->createRedirectResponseIfNeeded( $page, null );
$this->assertNull( $resp, 'createRedirectResponseIfNeeded() should not follow wiki redirect' );
$resp = $helper->createRedirectResponseIfNeeded( $page, 'redirect to foo' );
$this->assertNotNull( $resp, 'createRedirectResponseIfNeeded() should still follow normalization redirect' );
}
}
includes/editpage/Constraint/ChangeTagsConstraintTest.php 0000666 00000004312 15133511011 0017675 0 ustar 00 <?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\EditPage\Constraint\ChangeTagsConstraint;
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
/**
* Tests the ChangeTagsConstraint
*
* @author DannyS712
*
* @covers \MediaWiki\EditPage\Constraint\ChangeTagsConstraint
* @group Database
*/
class ChangeTagsConstraintTest extends MediaWikiIntegrationTestCase {
use EditConstraintTestTrait;
use MockAuthorityTrait;
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'change_tag', 'change_tag_def', 'logging' ]
);
}
public function testPass() {
$tagName = 'tag-for-constraint-test-pass';
ChangeTags::defineTag( $tagName );
$constraint = new ChangeTagsConstraint(
$this->mockRegisteredUltimateAuthority(),
[ $tagName ]
);
$this->assertConstraintPassed( $constraint );
}
public function testNoTags() {
// Early return for no tags being added
$constraint = new ChangeTagsConstraint(
$this->mockRegisteredUltimateAuthority(),
[]
);
$this->assertConstraintPassed( $constraint );
}
public function testFailure() {
$tagName = 'tag-for-constraint-test-fail';
ChangeTags::defineTag( $tagName );
$constraint = new ChangeTagsConstraint(
$this->mockRegisteredAuthorityWithoutPermissions( [ 'applychangetags' ] ),
[ $tagName ]
);
$this->assertConstraintFailed(
$constraint,
IEditConstraint::AS_CHANGE_TAG_ERROR
);
}
}
includes/editpage/Constraint/EditFilterMergedContentHookConstraintTest.php 0000666 00000007263 15133511011 0023234 0 ustar 00 <?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\EditPage\Constraint\EditFilterMergedContentHookConstraint;
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Status\Status;
use MediaWiki\User\User;
use Wikimedia\TestingAccessWrapper;
/**
* Tests the EditFilterMergedContentHookConstraint
*
* @author DannyS712
*
* @covers \MediaWiki\EditPage\Constraint\EditFilterMergedContentHookConstraint
* @todo Make this a unit test when Message will no longer use the global state.
*/
class EditFilterMergedContentHookConstraintTest extends MediaWikiIntegrationTestCase {
use EditConstraintTestTrait;
private function getConstraint( $hookResult ) {
$hookContainer = $this->createMock( HookContainer::class );
$hookContainer->expects( $this->once() )
->method( 'run' )
->with(
'EditFilterMergedContent',
$this->anything() // Not worrying about the hook call here
)
->willReturn( $hookResult );
$language = $this->createMock( Language::class );
$language->method( 'getCode' )
->willReturn( 'en' );
$constraint = new EditFilterMergedContentHookConstraint(
$hookContainer,
$this->getMockForAbstractClass( Content::class ),
$this->createMock( RequestContext::class ),
'EditSummaryGoesHere',
true, // Minor edit
$language,
$this->createMock( User::class )
);
return $constraint;
}
public function testPass() {
$constraint = $this->getConstraint( true );
$this->assertConstraintPassed( $constraint );
$this->assertSame( '', $constraint->getHookError() );
}
public function testFailure_goodStatus() {
// Code path 1: Hook returns false, but status is still good
// Status has no value set, falls back to AS_HOOK_ERROR_EXPECTED
$constraint = $this->getConstraint( false );
$this->assertConstraintFailed( $constraint, IEditConstraint::AS_HOOK_ERROR_EXPECTED );
}
public function testFailure_badStatus() {
// Code path 2: Hook returns false, status is bad
// To avoid using the real Status::getWikiText, which can use global state, etc.,
// replace the status object with a mock
$constraint = $this->getConstraint( false );
$mockStatus = $this->getMockBuilder( Status::class )
->onlyMethods( [ 'isGood', 'getWikiText' ] )
->getMock();
$mockStatus->method( 'isGood' )->willReturn( false );
$mockStatus->method( 'getWikiText' )->willReturn( 'WIKITEXT' );
$mockStatus->value = 12345;
TestingAccessWrapper::newFromObject( $constraint )->status = $mockStatus;
$this->assertConstraintFailed(
$constraint,
12345 // Value is set in hook (or in this case in the mock)
);
}
public function testFailure_notOKStatus() {
// Code path 3: Hook returns true, but status is not okay
$constraint = $this->getConstraint( true );
$status = Status::newGood();
$status->setOK( false );
TestingAccessWrapper::newFromObject( $constraint )->status = $status;
$this->assertConstraintFailed(
$constraint,
IEditConstraint::AS_HOOK_ERROR_EXPECTED
);
}
}
includes/Storage/RevertedTagUpdateIntegrationTest.php 0000666 00000025003 15133511011 0017105 0 ustar 00 <?php
namespace MediaWiki\Tests\Storage;
use ChangeTags;
use DeferredUpdates;
use FormatJson;
use MediaWiki\Config\HashConfig;
use MediaWiki\MainConfigNames;
use MediaWikiIntegrationTestCase;
use RecentChange;
use WikiPage;
/**
* @covers \MediaWiki\Storage\RevertedTagUpdate
* @covers \RevertedTagUpdateJob
* @covers \MediaWiki\Storage\RevertedTagUpdateManager
*
* @group Database
* @group medium
* @see RevertedTagUpdateTest for non-DB tests
*/
class RevertedTagUpdateIntegrationTest extends MediaWikiIntegrationTestCase {
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[
'page',
'revision',
'comment',
'text',
'content',
'change_tag',
'objectcache',
'job'
]
);
}
/**
* This test ensures the update is not performed at the end of the web request, but
* enqueued as a job for later execution instead.
*
* The reverting user here has autopatrol rights, so the update should be enqueued
* immediately.
*/
public function testWithJobQueue() {
$num = 5;
$page = $this->getExistingTestPage();
$revisionIds = $this->setupEditsOnPage( $page, $num );
// Make a manual revert to revision with content '0'
// The user HAS the 'autopatrol' right
$revertRevId = $this->editPage(
$page,
'0',
'',
NS_MAIN,
$this->getTestSysop()->getUser()
)->value['revision-record']->getId();
$revertedRevs = array_slice( $revisionIds, 1 );
DeferredUpdates::doUpdates();
// the tags should not have been populated yet
$this->verifyNoRevertedTags( $revertedRevs );
// run the job
$this->runJobs( [], [
'type' => 'revertedTagUpdate'
] );
// now the tags should be populated
$this->verifyRevertedTags( $revertedRevs, $revertRevId );
}
/**
* In this scenario, only the patrol mechanism is used for delaying the execution of
* the RevertedTagUpdate.
*/
public function testDelayedJobExecutionWithPatrol() {
$num = 5;
$page = $this->getExistingTestPage();
$revisionIds = $this->setupEditsOnPage( $page, $num );
// Make a manual revert to revision with content '0'
// The user DOES NOT have the 'autopatrol' right
$revertRevId = $this->editPage(
$page,
'0',
'',
NS_MAIN,
$this->getTestUser()->getUser()
)->value['revision-record']->getId();
$revertedRevs = array_slice( $revisionIds, 1 );
DeferredUpdates::doUpdates();
// the tags should not have been populated yet
$this->verifyNoRevertedTags( $revertedRevs );
// try to run the job
$this->runJobs( [ 'numJobs' => 0 ], [
'type' => 'revertedTagUpdate'
] );
// the tags still should not be present as the edit is pending approval
$this->verifyNoRevertedTags( $revertedRevs );
// approve the edit – this should enqueue the job
$rc = RecentChange::newFromConds( [ 'rc_this_oldid' => $revertRevId ] );
$rc->reallyMarkPatrolled();
// run the job
$this->runJobs( [ 'numJobs' => 1 ], [
'type' => 'revertedTagUpdate'
] );
// now the tags should be populated
$this->verifyRevertedTags( $revertedRevs, $revertRevId );
}
/**
* Ensure the update is not performed even after the edit is approved if it
* was reverted in the meantime.
*/
public function testNoJobExecutionWhenRevertIsReverted() {
$num = 5;
$page = $this->getExistingTestPage();
$revisionIds = $this->setupEditsOnPage( $page, $num );
// Make a manual revert to revision with content '0'
// The user DOES NOT have the 'autopatrol' right
$revertId1 = $this->editPage(
$page,
'0',
'',
NS_MAIN,
$this->getTestUser()->getUser()
)->value['revision-record']->getId();
$revertedRevs = array_slice( $revisionIds, 1 );
DeferredUpdates::doUpdates();
$this->runJobs( [ 'numJobs' => 0 ], [
'type' => 'revertedTagUpdate'
] );
// the tags should not be present as the edit is pending approval
$this->verifyNoRevertedTags( $revertedRevs );
// now a sysop reverts the revert made by a regular user
$revertId2 = $this->editPage(
$page,
'5',
'',
NS_MAIN,
$this->getTestSysop()->getUser()
)->value['revision-record']->getId();
DeferredUpdates::doUpdates();
$this->runJobs( [], [
'type' => 'revertedTagUpdate'
] );
$this->verifyRevertedTags( [ $revertId1 ], $revertId2 );
// approve the edit – this should enqueue the job
$rc = RecentChange::newFromConds( [ 'rc_this_oldid' => $revertId1 ] );
$rc->reallyMarkPatrolled();
// Run the job.
// The job should notice that the revert is reverted and refuse to perform
// the update.
$this->runJobs( [], [
'type' => 'revertedTagUpdate'
] );
// the tags should not be populated
$this->verifyNoRevertedTags( $revertedRevs );
}
/**
* Ensure the patrolling-related job delay mechanism is not used when patrolling
* is disabled.
*/
public function testNoDelayedJobExecutionWhenPatrollingIsDisabled() {
$num = 5;
// disable patrolling
$this->overrideMwServices( new HashConfig( [ MainConfigNames::UseRCPatrol => false ] ) );
$page = $this->getExistingTestPage();
$revisionIds = $this->setupEditsOnPage( $page, $num );
// Make a manual revert to revision with content '0'
// The user DOES NOT have the 'autopatrol' right, but that should not matter here
$revertRevId = $this->editPage(
$page,
'0',
'',
NS_MAIN,
$this->getTestUser()->getUser()
)->value['revision-record']->getId();
$revertedRevs = array_slice( $revisionIds, 1 );
DeferredUpdates::doUpdates();
// the tags should not have been populated yet
$this->verifyNoRevertedTags( $revertedRevs );
// run the job
$this->runJobs( [], [
'type' => 'revertedTagUpdate'
] );
// now the tags should be populated
$this->verifyRevertedTags( $revertedRevs, $revertRevId );
}
/**
* In this scenario an extension hook prevents the update from executing.
* We also check if the hook is able to override the decision made by the patrol
* subsystem.
* The update is then re-enqueued when the edit is approved.
*/
public function testDelayedJobExecutionWithHook() {
$num = 5;
$page = $this->getExistingTestPage();
$revisionIds = $this->setupEditsOnPage( $page, $num );
$this->setTemporaryHook(
'BeforeRevertedTagUpdate',
function (
$wikiPage,
$user,
$summary,
$flags,
$revisionRecord,
$editResult,
&$approved
) {
$this->assertTrue(
$approved,
'$approved parameter of BeforeRevertedTagUpdate'
);
$approved = false;
}
);
// Make a manual revert to revision with content '0'
// The user HAS the 'autopatrol' right, but that should be vetoed by the hook
$revertRevId = $this->editPage(
$page,
'0',
'',
NS_MAIN,
$this->getTestSysop()->getUser()
)->value['revision-record']->getId();
$revertedRevs = array_slice( $revisionIds, 1 );
DeferredUpdates::doUpdates();
// the tags should not have been populated yet
$this->verifyNoRevertedTags( $revertedRevs );
// try to run the job
$this->runJobs( [ 'numJobs' => 0 ], [
'type' => 'revertedTagUpdate'
] );
// the tags still should not be present as the edit is pending approval
$this->verifyNoRevertedTags( $revertedRevs );
// simulate the approval of the edit
$manager = $this->getServiceContainer()->getRevertedTagUpdateManager();
$manager->approveRevertedTagForRevision( $revertRevId );
// run the job
$this->runJobs( [], [
'type' => 'revertedTagUpdate'
] );
// now the tags should be populated
$this->verifyRevertedTags( $revertedRevs, $revertRevId );
}
/**
* Here the patrol subsystem says the edit is not approved, but an extension hook
* decides to run the update immediately anyway.
*/
public function testNoDelayedJobExecutionWithHook() {
$num = 5;
$page = $this->getExistingTestPage();
$revisionIds = $this->setupEditsOnPage( $page, $num );
$this->setTemporaryHook(
'BeforeRevertedTagUpdate',
function (
$wikiPage,
$user,
$summary,
$flags,
$revisionRecord,
$editResult,
&$approved
) {
$this->assertFalse(
$approved,
'$approved parameter of BeforeRevertedTagUpdate'
);
$approved = true;
}
);
// Make a manual revert to revision with content '0'
// The user DOES NOT have the 'autopatrol' right, but that should be
// overridden by the hook.
$revertRevId = $this->editPage(
$page,
'0',
'',
NS_MAIN,
$this->getTestUser()->getUser()
)->value['revision-record']->getId();
$revertedRevs = array_slice( $revisionIds, 1 );
DeferredUpdates::doUpdates();
// the tags should not have been populated yet
$this->verifyNoRevertedTags( $revertedRevs );
// run the job
$this->runJobs( [], [
'type' => 'revertedTagUpdate'
] );
// now the tags should be populated
$this->verifyRevertedTags( $revertedRevs, $revertRevId );
}
/**
* Sets up a set number of edits on a page.
*
* @param WikiPage $page the page to set up
* @param int $editCount
*
* @return array
*/
private function setupEditsOnPage( WikiPage $page, int $editCount ): array {
$revIds = [];
for ( $i = 0; $i <= $editCount; $i++ ) {
$revIds[] = $this->editPage( $page, strval( $i ) )
->value['revision-record']->getId();
}
return $revIds;
}
/**
* Ensures that the reverted tag is not set for given revisions.
*
* @param array $revisionIds
*/
private function verifyNoRevertedTags( array $revisionIds ) {
$dbw = wfGetDB( DB_PRIMARY );
foreach ( $revisionIds as $revisionId ) {
$this->assertNotContains(
'mw-reverted',
ChangeTags::getTags( $dbw, null, $revisionId ),
'ChangeTags::getTags()'
);
}
}
/**
* Checks if the provided revisions have their reverted tag set properly.
*
* @param array $revisionIds
* @param int $revertRevId
*/
private function verifyRevertedTags(
array $revisionIds,
int $revertRevId
) {
$dbw = wfGetDB( DB_PRIMARY );
// for each reverted revision
foreach ( $revisionIds as $revisionId ) {
$this->assertContains(
'mw-reverted',
ChangeTags::getTags( $dbw, null, $revisionId ),
'ChangeTags::getTags()'
);
// do basic checks for the ct_params field
$extraParams = $dbw->newSelectQueryBuilder()
->select( 'ct_params' )
->from( 'change_tag' )
->join( 'change_tag_def', null, 'ct_tag_id = ctd_id' )
->where( [ 'ct_rev_id' => $revisionId, 'ctd_name' => 'mw-reverted' ] )
->caller( __METHOD__ )->fetchField();
$this->assertNotEmpty( $extraParams, 'change_tag.ct_params' );
$this->assertJson( $extraParams, 'change_tag.ct_params' );
$parsedParams = FormatJson::decode( $extraParams, true );
$this->assertArraySubmapSame(
[ 'revertId' => $revertRevId ],
$parsedParams,
'change_tag.ct_params'
);
}
}
}
Back to Directory
File Manager