Page MenuHomeWickedGov Phorge

FilterLookupTest.php
No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None

FilterLookupTest.php

<?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use AbuseFilterRowsAndFiltersTestTrait;
use Generator;
use MediaWiki\Extension\AbuseFilter\CentralDBManager;
use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException;
use MediaWiki\Extension\AbuseFilter\Filter\ClosestFilterVersionNotFoundException;
use MediaWiki\Extension\AbuseFilter\Filter\ExistingFilter;
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException;
use MediaWiki\Extension\AbuseFilter\Filter\FilterVersionNotFoundException;
use MediaWiki\Extension\AbuseFilter\Filter\Flags;
use MediaWiki\Extension\AbuseFilter\Filter\HistoryFilter;
use MediaWiki\Extension\AbuseFilter\Filter\LastEditInfo;
use MediaWiki\Extension\AbuseFilter\Filter\Specs;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWikiUnitTestCase;
use stdClass;
use Wikimedia\ObjectCache\HashBagOStuff;
use Wikimedia\ObjectCache\WANObjectCache;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\SelectQueryBuilder;
/**
* @group Test
* @group AbuseFilter
* @covers \MediaWiki\Extension\AbuseFilter\FilterLookup
* @todo Some integration tests with a real DB might be helpful
*/
class FilterLookupTest extends MediaWikiUnitTestCase {
use AbuseFilterRowsAndFiltersTestTrait;
/**
* @param IDatabase|null $db
* @param string|false $centralDB
* @param WANObjectCache|null $cache
* @param bool $filterIsCentral
* @return FilterLookup
*/
private function getLookup(
?IDatabase $db = null,
$centralDB = false,
?WANObjectCache $cache = null,
bool $filterIsCentral = false
): FilterLookup {
$lb = $this->createMock( ILoadBalancer::class );
$lb->method( 'getConnection' )
->willReturn( $db ?? $this->createMock( IDatabase::class ) );
$lbFactory = $this->createMock( LBFactory::class );
$lbFactory->method( 'getMainLB' )->willReturnCallback(
static function ( $domain ) use ( $lb, $centralDB ) {
// Return null for sanity
return $domain === $centralDB ? $lb : null;
}
);
$centralDBManager = new CentralDBManager( $lbFactory, $centralDB, $filterIsCentral );
return new FilterLookup(
$lb,
// Cannot use mocks because final methods aren't mocked and they would error out
$cache ?? new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
$centralDBManager
);
}
/**
* Hacky helper to set up the database
* @param stdClass[] $filterRows abuse_filter or abuse_filter_history
* @param stdClass[] $actionRows
* @return IDatabase
*/
private function getDBWithMockRows( array $filterRows, array $actionRows = [] ): IDatabase {
$db = $this->createMock( IDatabase::class );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$db->method( 'selectRow' )->willReturnCallback( static function ( $table ) use ( $filterRows ) {
$tables = (array)$table;
return array_intersect( $tables, [ 'abuse_filter', 'abuse_filter_history' ] ) ? $filterRows[0] : false;
} );
$db->method( 'select' )->willReturnCallback(
static function ( $table, $_, $where ) use ( $filterRows, $actionRows ) {
$tables = (array)$table;
$ret = [];
if ( in_array( 'abuse_filter_action', $tables ) ) {
foreach ( $actionRows as $row ) {
if ( $row->afa_filter === $where['afa_filter'] ) {
$ret[] = $row;
}
}
} elseif ( array_intersect( $tables, [ 'abuse_filter', 'abuse_filter_history' ] ) ) {
$ret = $filterRows;
}
return new FakeResultWrapper( $ret );
}
);
return $db;
}
/**
* @param int $version
* @param stdClass $dbRow
* @param HistoryFilter $expected
* @dataProvider provideFilterVersions
*/
public function testGetFilterVersion( int $version, stdClass $dbRow, HistoryFilter $expected ) {
$db = $this->getDBWithMockRows( [ $dbRow ] );
$filterLookup = $this->getLookup( $db );
$this->assertEquals( $expected, $filterLookup->getFilterVersion( $version ) );
}
/**
* @return Generator
*/
public static function provideFilterVersions(): Generator {
$version = 163;
$filters = [
'no actions' => new HistoryFilter(
new Specs(
'false',
'X',
'Y',
[],
'default'
),
new Flags(
true,
true,
true,
true
),
[],
new LastEditInfo(
42,
'FilterManager',
'20180706142932'
),
1,
$version
),
'with actions' => new HistoryFilter(
new Specs(
'the_answer := 42; the_answer === 6*9',
'Some comments',
'My filter',
[ 'degroup', 'disallow' ],
'default'
),
new Flags(
true,
false,
true,
false
),
[ 'degroup' => [], 'disallow' => [] ],
new LastEditInfo(
42,
'FilterManager',
'20180706142932'
),
1,
$version
),
];
foreach ( $filters as $filter ) {
$flags = [];
foreach ( [ 'enabled', 'deleted', 'hidden', 'protected', 'global' ] as $flag ) {
$method = 'is' . ucfirst( $flag );
if ( $filter->$method() ) {
$flags[] = $flag;
}
}
$historyRow = (object)[
'afh_id' => $version,
'afh_filter' => $filter->getID(),
'afh_user' => $filter->getUserID(),
'afh_user_text' => $filter->getUserName(),
'afh_timestamp' => $filter->getTimestamp(),
'afh_pattern' => $filter->getRules(),
'afh_comments' => $filter->getComments(),
'afh_flags' => implode( ',', $flags ),
'afh_public_comments' => $filter->getName(),
'afh_actions' => serialize( array_fill_keys( $filter->getActionsNames(), [] ) ),
'afh_deleted' => 0,
'afh_changed_fields' => 'actions',
'afh_group' => $filter->getGroup()
];
yield [ $version, $historyRow, $filter ];
}
}
public function testGetFilterVersion_notfound() {
$db = $this->createMock( IDatabase::class );
$db->method( 'selectRow' )->willReturn( false );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$filterLookup = $this->getLookup( $db );
$this->expectException( FilterVersionNotFoundException::class );
$filterLookup->getFilterVersion( 42 );
}
public function testGetLastHistoryVersion() {
// Reuse this data provider for conveniency
[ , $historyRow, $filter ] = $this->provideFilterVersions()->current();
$db = $this->getDBWithMockRows( [ $historyRow ] );
$filterLookup = $this->getLookup( $db );
$this->assertEquals( $filter, $filterLookup->getLastHistoryVersion( $filter->getID() ) );
}
public function testGetLastHistoryVersion_notfound() {
$db = $this->createMock( IDatabase::class );
$db->method( 'selectRow' )->willReturn( false );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$filterLookup = $this->getLookup( $db );
$this->expectException( FilterNotFoundException::class );
$filterLookup->getLastHistoryVersion( 42 );
}
public function testGetClosestVersion() {
// Reuse this data provider for conveniency
[ , $historyRow, $filter ] = $this->provideFilterVersions()->current();
$db = $this->getDBWithMockRows( [ $historyRow ] );
$filterLookup = $this->getLookup( $db );
$this->assertEquals( $filter, $filterLookup->getClosestVersion( 1, 42, FilterLookup::DIR_NEXT ) );
}
public function testGetClosestVersion_notfound() {
$db = $this->createMock( IDatabase::class );
$db->method( 'selectRow' )->willReturn( false );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$filterLookup = $this->getLookup( $db );
$this->expectException( ClosestFilterVersionNotFoundException::class );
$filterLookup->getClosestVersion( 42, 42, FilterLookup::DIR_PREV );
}
public function testGetFirstFilterVersionID() {
$versionID = 1234;
$db = $this->createMock( IDatabase::class );
$db->method( 'selectField' )->willReturn( $versionID );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$filterLookup = $this->getLookup( $db );
$this->assertSame( $versionID, $filterLookup->getFirstFilterVersionID( 42 ) );
}
public function testGetFirstFilterVersionID_notfound() {
$db = $this->createMock( IDatabase::class );
$db->method( 'selectField' )->willReturn( false );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$filterLookup = $this->getLookup( $db );
$this->expectException( FilterNotFoundException::class );
$filterLookup->getFirstFilterVersionID( 42 );
}
public function testLocalCache() {
$row = $this->getRowsAndFilters()['no actions']['row'];
$db = $this->createMock( IDatabase::class );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$db->expects( $this->once() )->method( 'selectRow' )->willReturn( $row );
$filterLookup = $this->getLookup( $db );
// Warm-up cache
$filterLookup->getFilter( 42, false );
// This should fail the soft assertion of once()
$filterLookup->getFilter( 42, false );
}
public function testClearLocalCache() {
$row = $this->getRowsAndFilters()['no actions']['row'];
$db = $this->createMock( IDatabase::class );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$db->expects( $this->exactly( 2 ) )->method( 'selectRow' )->willReturn( $row );
$filterLookup = $this->getLookup( $db );
// Both calls should result in a query
$filterLookup->getFilter( 42, false );
$filterLookup->clearLocalCache();
$filterLookup->getFilter( 42, false );
}
/**
* Provider to account for central vs non-central filter DB
* @return array
*/
public static function provideIsCentral() {
return [
'central' => [ true ],
'not central' => [ false ]
];
}
/**
* @param bool $isCentral
* @dataProvider provideIsCentral
*/
public function testGlobalCache( bool $isCentral ) {
$row = $this->getRowsAndFilters()['no actions']['row'];
$db = $this->createMock( IDatabase::class );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
// Should be called twice: once for the filter, once for the actions
$db->expects( $this->exactly( 2 ) )->method( 'select' )
->willReturnOnConsecutiveCalls(
new FakeResultWrapper( [ $row ] ),
new FakeResultWrapper( [] )
);
$filterLookup = $this->getLookup( $db, 'foobar', null, $isCentral );
// WAN cache is only used for global filters
$global = true;
$group = 'foo';
// Warm-up cache: the following calls must not fail the soft assertion of once()
$filterLookup->getAllActiveFiltersInGroup( $group, $global );
// This is covered by the internal cache
$filterLookup->getAllActiveFiltersInGroup( $group, $global );
$filterLookup->clearLocalCache();
// This is covered by the network cache
$filterLookup->getAllActiveFiltersInGroup( $group, $global );
}
public function testFilterLookupClearNetworkCache() {
$row = $this->getRowsAndFilters()['no actions']['row'];
$db = $this->createMock( IDatabase::class );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
// 4 calls: row, actions, row, actions
$db->expects( $this->exactly( 4 ) )
->method( 'select' )
->willReturnOnConsecutiveCalls(
new FakeResultWrapper( [ $row ] ),
new FakeResultWrapper( [] ),
new FakeResultWrapper( [ $row ] ),
new FakeResultWrapper( [] )
);
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$filterLookup = $this->getLookup( $db, 'foobar', $cache );
// WAN cache is only used for global filters
$global = true;
$group = 'foo';
// Both calls should result in a query
$filterLookup->getAllActiveFiltersInGroup( $group, $global );
$filterLookup->clearLocalCache();
$filterLookup->purgeGroupWANCache( $group );
// Avoid cache hits due to $TINY_POSTIVE (use +10 out of an abundance of caution)
$fakeTime = microtime( true ) + 10;
$cache->setMockTime( $fakeTime );
$filterLookup->getAllActiveFiltersInGroup( $group, $global );
}
public function testValidConstructor() {
$this->assertInstanceOf(
FilterLookup::class,
new FilterLookup(
$this->createMock( ILoadBalancer::class ),
$this->createMock( WANObjectCache::class ),
$this->createMock( CentralDBManager::class )
)
);
}
/**
* @param stdClass $row
* @param stdClass[] $actionsRows
* @param ExistingFilter $expected
* @dataProvider getRowsAndFilters
*/
public function testGetFilter( stdClass $row, array $actionsRows, ExistingFilter $expected ) {
$db = $this->getDBWithMockRows( [ $row ], $actionsRows );
$filterLookup = $this->getLookup( $db );
$actual = $filterLookup->getFilter( $row->af_id, false );
// Trigger the lazy-load mechanism
$actual->getActions();
$this->assertEquals( $expected, $actual );
}
/**
* @param stdClass $row
* @param stdClass[] $actionsRows
* @param ExistingFilter $expected
* @dataProvider getRowsAndFilters
*/
public function testGetFilter_global( stdClass $row, array $actionsRows, ExistingFilter $expected ) {
$db = $this->getDBWithMockRows( [ $row ], $actionsRows );
$filterLookup = $this->getLookup( $db, 'central_wiki' );
$actual = $filterLookup->getFilter( $row->af_id, true );
// Trigger the lazy-load mechanism
$actual->getActions();
$this->assertEquals( $expected, $actual );
}
public function testGetFilter_notfound() {
$db = $this->createMock( IDatabase::class );
$db->method( 'newSelectQueryBuilder' )->willReturnCallback( static fn () => new SelectQueryBuilder( $db ) );
$db->method( 'selectRow' )->willReturn( false );
$filterLookup = $this->getLookup( $db );
$this->expectException( FilterNotFoundException::class );
$filterLookup->getFilter( 42, false );
}
public function testGetFilter_globaldisabled() {
$filterLookup = $this->getLookup();
$this->expectException( CentralDBNotAvailableException::class );
$filterLookup->getFilter( 42, true );
}
public function testGetAllActiveFiltersInGroup() {
$data = $this->getRowsAndFilters();
$db = $this->getDBWithMockRows(
array_column( $data, 'row' ),
array_merge( ...array_column( $data, 'actions' ) )
);
$filterLookup = $this->getLookup( $db, false, null );
$expected = [];
/** @var Filter $filter */
foreach ( array_column( $data, 'filter' ) as $filter ) {
$expected[$filter->getID()] = $filter;
}
$actual = $filterLookup->getAllActiveFiltersInGroup( '_', false );
foreach ( $actual as $filter ) {
// Trigger the lazy-load mechanism
$filter->getActions();
}
$this->assertEquals( $expected, $actual );
}
/**
* @param bool $isCentral
* @dataProvider provideIsCentral
*/
public function testGetAllActiveFiltersInGroup_global( bool $isCentral ) {
$data = $this->getRowsAndFilters();
$db = $this->getDBWithMockRows(
array_column( $data, 'row' ),
array_merge( ...array_column( $data, 'actions' ) )
);
$filterLookup = $this->getLookup( $db, 'some_db', null, $isCentral );
$expected = [];
/** @var Filter $filter */
foreach ( array_column( $data, 'filter' ) as $filter ) {
$key = 'global-' . $filter->getID();
$expected[$key] = $filter;
}
$actual = $filterLookup->getAllActiveFiltersInGroup( '_', true );
foreach ( $actual as $filter ) {
// Trigger the lazy-load mechanism
$filter->getActions();
}
$this->assertEquals( $expected, $actual );
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 19 2026, 19:06 (6 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e0/04/fd4d252facb00aa4abb82c84372c
Default Alt Text
FilterLookupTest.php (15 KB)

Event Timeline