mediawiki-extensions-AbuseF.../tests/phpunit/UpdateVarDumpsTest.php
Daimona Eaytoy 55ba083b13 Introduce a KeywordsManager service
This will decouple a bit the huge and chaotic tangle of AF classes. Some
boilerplate code for AbuseFilter services is also added with this patch.

Note that this requires injecting a KeywordsManager in
AbuseFilterVariableHolder, or unit tests would fail. This is still
incomplete, and the Manager is only injected in tests, because
VariableHolder still has to be refactored.

The test for the UpdateVarDumps script had to be updated, because
serializing VHs in there was a bad choice. As pointed out in a comment,
the test is likely going to break again once we remove the BC code, but
I hope that we'll be able to remove the test at that point.

Change-Id: I12a656a310adb8c5f75cab63f6db9e121e109717
2020-09-28 23:03:52 +00:00

484 lines
15 KiB
PHP

<?php
use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\TestingAccessWrapper;
/**
* @group Database
* @coversDefaultClass UpdateVarDumps
* @property TestingAccessWrapper|UpdateVarDumps $maintenance
*
* NOTE: This test is likely going to break once we remove the BC code after T213006 is done.
* Since maintaining the test would be expensive, and the script was single-use anyway, this
* test can be just removed (and the script marked as codeCoverageIgnore) as soon as it starts breaking.
*/
class UpdateVarDumpsTest extends MaintenanceBaseTestCase {
private const TIMESTAMP = '20000102030405';
private static $aflRow = [
// 'afl_id'
'afl_filter' => 1,
'afl_global' => 0,
'afl_filter_id' => 1,
'afl_user' => 1,
'afl_user_text' => 'Foo',
'afl_ip' => '127.0.0.1',
'afl_action' => 'edit',
'afl_actions' => '',
// 'afl_var_dump'
// 'afl_timestamp' added in __construct
'afl_namespace' => 1,
'afl_title' => 'Foobar',
'afl_wiki' => null,
'afl_deleted' => 0,
'afl_patrolled_by' => 1,
'afl_rev_id' => 123
];
private const TEXT_ROW = [
// 'old_id'
'old_flags' => ''
// 'old_text'
];
private const VARS = [
'action' => 'edit',
'page_id' => 12,
'user_blocked' => true,
'accountname' => null,
'user_groups' => [ 'x', 'y' ]
];
/**
* A serialized AbuseFilterVariableHolder object holding self::VARS. Don't try serializing
* the object in tests, because that's going to break too easily.
*/
private const SERIALIZED_VH = 'O:25:"AbuseFilterVariableHolder":3:{s:5:"mVars";a:5:{s:6:"action";O:7:"AFPData":' .
'2:{s:4:"type";s:6:"string";s:4:"data";s:4:"edit";}s:7:"page_id";O:7:"AFPData":2:{s:4:"type";s:3:"int";s:4:' .
'"data";i:12;}s:12:"user_blocked";O:7:"AFPData":2:{s:4:"type";s:4:"bool";s:4:"data";b:1;}s:11:"accountname";' .
'O:7:"AFPData":2:{s:4:"type";s:4:"null";s:4:"data";N;}s:11:"user_groups";O:7:"AFPData":2:{s:4:"type";s:5:' .
'"array";s:4:"data";a:2:{i:0;O:7:"AFPData":2:{s:4:"type";s:6:"string";s:4:"data";s:1:"x";}i:1;O:7:"AFPData"' .
':2:{s:4:"type";s:6:"string";s:4:"data";s:1:"y";}}}}s:9:"forFilter";b:0;s:12:"mVarsVersion";i:2;}';
/**
* @inheritDoc
*/
protected $tablesUsed = [ 'abuse_filter_log', 'text' ];
/**
* @inheritDoc
*/
public function __construct( $name = null, array $data = [], $dataName = '' ) {
parent::__construct( $name, $data, $dataName );
self::$aflRow['afl_timestamp'] = wfGetDB( DB_REPLICA )->timestamp( self::TIMESTAMP );
}
/**
* @inheritDoc
*/
public function setUp(): void {
parent::setUp();
$this->maintenance->dbr = $this->maintenance->dbw = $this->db;
// This isn't really necessary
$this->maintenance->allRowsCount = 50;
}
/**
* @inheritDoc
*/
public function getMaintenanceClass() {
return UpdateVarDumps::class;
}
/**
* Shorthand to select all rows on a table (either abuse_filter_log or text), ordering
* by pkey ASC
* @param string $table
* @return IResultWrapper
*/
private function selectAllAscending( string $table ) : IResultWrapper {
$key = $table === 'abuse_filter_log' ? 'afl_id' : 'old_id';
return $this->db->select(
$table,
'*',
'',
wfGetCaller(),
[ 'ORDER_BY' => "$key ASC" ]
);
}
/**
* @covers ::doDBUpdates
*/
public function testOnEmptyDB() {
$this->expectOutputRegex( '/the abuse_filter_log table is empty/' );
$this->maintenance->execute();
}
/**
* @covers ::fixMissingDumps
* @covers ::doFixMissingDumps
*/
public function testFixMissingDumps() {
$expected = $this->insertMissingDumps();
$this->maintenance->fixMissingDumps();
$rows = $this->selectAllAscending( 'abuse_filter_log' );
$actual = [];
foreach ( $rows as $row ) {
$actual[] = [ 'afl_id' => (int)$row->afl_id, 'afl_var_dump' => $row->afl_var_dump ];
}
$this->assertSame( $expected, $actual );
}
/**
* @return array Expected content of abuse_filter_log after the cleanup
*/
private function insertMissingDumps() : array {
$insertRows = [
'Wrong duplicate 1' => [ 'afl_id' => 1, 'afl_var_dump' => '' ] + self::$aflRow,
'Good duplicate 1' => [ 'afl_id' => 2, 'afl_var_dump' => 'stored-text:12345' ] + self::$aflRow,
'Wrong duplicate 2' => [ 'afl_id' => 3, 'afl_var_dump' => '' ] + self::$aflRow,
'Good duplicate 2' => [ 'afl_id' => 4, 'afl_var_dump' => 'stored-text:12345' ] + self::$aflRow,
'Wrong duplicate, 3' => [ 'afl_id' => 5, 'afl_var_dump' => '' ] + self::$aflRow,
'Extraneous row' => [ 'afl_id' => 6, 'afl_var_dump' => 'stored-text:444' ] + self::$aflRow,
'Good duplicate 3' => [ 'afl_id' => 7, 'afl_var_dump' => 'stored-text:12345' ] + self::$aflRow,
];
$this->db->insert( 'abuse_filter_log', array_values( $insertRows ), __METHOD__ );
$expected = [
[ 'afl_id' => 2, 'afl_var_dump' => 'stored-text:12345' ],
[ 'afl_id' => 4, 'afl_var_dump' => 'stored-text:12345' ],
[ 'afl_id' => 6, 'afl_var_dump' => 'stored-text:444' ],
[ 'afl_id' => 7, 'afl_var_dump' => 'stored-text:12345' ],
];
return $expected;
}
/**
* @covers ::fixMissingDumps
* @covers ::doFixMissingDumps
*/
public function testFixMissingDumpsToRebuild() {
$expected = $this->insertMissingDumpsToRebuild();
$this->maintenance->fixMissingDumps();
$aflRows = $this->selectAllAscending( 'abuse_filter_log' );
$aflActual = [];
foreach ( $aflRows as $aflRow ) {
$aflActual[] = [ 'afl_id' => (int)$aflRow->afl_id, 'afl_var_dump' => $aflRow->afl_var_dump ];
}
$this->assertSame( $expected['abuse_filter_log'], $aflActual );
$textRows = $this->selectAllAscending( 'text' );
$textActual = [];
foreach ( $textRows as $textRow ) {
$textActual[] = [ 'old_id' => (int)$textRow->old_id, 'old_text' => $textRow->old_text ];
}
$this->assertSame( $expected['text'], $textActual );
}
/**
* @return array Expected content of abuse_filter_log after the cleanup
*/
private function insertMissingDumpsToRebuild() : array {
$baseVars = [
'timestamp' => wfTimestamp( TS_UNIX, self::TIMESTAMP ),
];
$insertRows = [
'Edit' => [ 'afl_id' => 1, 'afl_var_dump' => '' ] + self::$aflRow,
// afl_action added below in order to keep the same order of rows
'Createaccount' => [ 'afl_id' => 2, 'afl_var_dump' => '' ] + self::$aflRow,
'Move' => [ 'afl_id' => 3, 'afl_var_dump' => '' ] + self::$aflRow,
];
$insertRows['Createaccount']['afl_action'] = 'createaccount';
$insertRows['Move']['afl_action'] = 'move';
$this->db->insert( 'abuse_filter_log', array_values( $insertRows ), __METHOD__ );
$title = Title::makeTitle( self::$aflRow['afl_namespace'], self::$aflRow['afl_title'] );
$expected = [
'abuse_filter_log' => [
[ 'afl_id' => 1, 'afl_var_dump' => 'tt:1' ],
[ 'afl_id' => 2, 'afl_var_dump' => 'tt:2' ],
[ 'afl_id' => 3, 'afl_var_dump' => 'tt:3' ],
],
'text' => [
[
'old_id' => 1,
'old_text' => FormatJson::encode( $baseVars + [
'action' => 'edit',
'user_name' => self::$aflRow['afl_user_text'],
'page_title' => self::$aflRow['afl_title'],
'page_prefixedtitle' => $title->getPrefixedText()
] )
],
[
'old_id' => 2,
'old_text' => FormatJson::encode( $baseVars + [
'action' => 'createaccount',
'accountname' => self::$aflRow['afl_user_text']
] )
],
[
'old_id' => 3,
'old_text' => FormatJson::encode( $baseVars + [
'action' => 'move',
'user_name' => self::$aflRow['afl_user_text'],
'moved_from_title' => self::$aflRow['afl_title'],
'moved_from_prefixedtitle' => $title->getPrefixedText()
] )
],
]
];
return $expected;
}
/**
* @covers ::moveToText
* @covers ::doMoveToText
*/
public function testMoveToText() {
$expected = $this->insertMoveToText();
$this->maintenance->moveToText();
$aflRows = $this->selectAllAscending( 'abuse_filter_log' );
$aflActual = [];
foreach ( $aflRows as $row ) {
$aflActual[] = [ 'afl_id' => (int)$row->afl_id, 'afl_var_dump' => $row->afl_var_dump ];
}
$this->assertSame( $expected['abuse_filter_log'], $aflActual );
$textRows = $this->selectAllAscending( 'text' );
$textActual = [];
foreach ( $textRows as $row ) {
$textActual[] = [ 'old_id' => (int)$row->old_id, 'old_text' => $row->old_text ];
}
$this->assertSame( $expected['text'], $textActual );
}
/**
* @return array Expected contents of abuse_filter_log and text tables
*/
private function insertMoveToText() : array {
$serializedArr = serialize( self::VARS );
$truncatedArr = substr( $serializedArr, 0, -5 );
$expectedTruncated = FormatJson::encode( array_diff_key( self::VARS, [ 'user_groups' => 1 ] ) );
$insertRows = [
'Truncated arr' => [ 'afl_id' => 1, 'afl_var_dump' => $truncatedArr ] + self::$aflRow,
'Serialized array' => [ 'afl_id' => 2, 'afl_var_dump' => $serializedArr ] + self::$aflRow,
'Serialized VariableHolder' =>
[ 'afl_id' => 3, 'afl_var_dump' => self::SERIALIZED_VH ] + self::$aflRow,
];
$this->db->insert( 'abuse_filter_log', array_values( $insertRows ), __METHOD__ );
$expected = [
'abuse_filter_log' => [
[ 'afl_id' => 1, 'afl_var_dump' => 'tt:1' ],
[ 'afl_id' => 2, 'afl_var_dump' => 'tt:2' ],
[ 'afl_id' => 3, 'afl_var_dump' => 'tt:3' ],
],
'text' => [
[ 'old_id' => 1, 'old_text' => $expectedTruncated ],
[ 'old_id' => 2, 'old_text' => FormatJson::encode( self::VARS ) ],
[ 'old_id' => 3, 'old_text' => FormatJson::encode( self::VARS ) ],
]
];
return $expected;
}
/**
* @return TestingAccessWrapper|UpdateVarDumps
*/
private function getMaintenanceWithoutExit() {
// We first need to mock UpdateVarDumps, because fatalError kills PHP.
$maint = $this->getMockBuilder( UpdateVarDumps::class )
->setMethods( [ 'fatalError' ] )
->getMock();
$maint->method( 'fatalError' )->willThrowException( new LogicException() );
// Then use an access wrapper to call private methods.
$wrapper = TestingAccessWrapper::newFromObject( $maint );
$wrapper->allRowsCount = 50;
$wrapper->dbr = $wrapper->dbw = $this->db;
return $wrapper;
}
/**
* @param array $row
* @dataProvider provideMoveToTextUnexpectedTypes
* @covers ::doMoveToText
*/
public function testMoveToTextUnexpectedTypes( array $row ) {
$this->db->insert( 'abuse_filter_log', $row, __METHOD__ );
$maint = $this->getMaintenanceWithoutExit();
$this->expectException( LogicException::class );
$maint->moveToText();
}
/**
* @return array
*/
public function provideMoveToTextUnexpectedTypes() {
return [
'Truncated obj' => [
[ 'afl_id' => 1, 'afl_var_dump' => substr( self::SERIALIZED_VH, 0, -5 ) ] + self::$aflRow
],
'Wrong type' => [
[ 'afl_id' => 3, 'afl_var_dump' => serialize( 'foo bar baz' ) ] + self::$aflRow
]
];
}
/**
* @param string $str
* @param array $expected
* @covers UpdateVarDumps::restoreTruncatedDump
* @dataProvider provideTruncatedDump
*/
public function testRestoreTruncatedDump( string $str, array $expected ) {
$this->assertSame( $expected, $this->maintenance->restoreTruncatedDump( $str ) );
}
/**
* @return array
*/
public function provideTruncatedDump() {
$serialized = serialize( self::VARS );
$varsWithoutKeys = function ( ...$keys ) {
return array_diff_key( self::VARS, array_fill_keys( $keys, 1 ) );
};
return [
[ substr( $serialized, 0, -1 ), $varsWithoutKeys( 'user_groups' ) ],
[ substr( $serialized, 0, -7 ), $varsWithoutKeys( 'user_groups' ) ],
[ substr( $serialized, 0, -16 ), $varsWithoutKeys( 'user_groups' ) ],
[ substr( $serialized, 0, -32 ), $varsWithoutKeys( 'user_groups' ) ],
[ substr( $serialized, 0, -46 ), $varsWithoutKeys( 'user_groups' ) ],
[ substr( $serialized, 0, -56 ), $varsWithoutKeys( 'user_groups', 'accountname' ) ],
[
substr( $serialized, 0, -72 ),
$varsWithoutKeys( 'user_groups', 'accountname', 'user_blocked' )
],
[
substr( $serialized, 0, -96 ),
$varsWithoutKeys( 'user_groups', 'accountname', 'user_blocked', 'page_id' )
],
[ substr( $serialized, 0, 17 ), [] ],
[ substr( $serialized, 0, 10 ), [] ],
[ substr( $serialized, 0, 5 ), [] ],
];
}
/**
* @covers ::updateText
* @covers ::doUpdateText
*/
public function testUpdateText() {
$expected = $this->insertUpdateText();
$this->maintenance->updateText();
$rows = $this->selectAllAscending( 'text' );
$actual = [];
foreach ( $rows as $row ) {
$actual[] = [
'old_id' => (int)$row->old_id,
'old_flags' => $row->old_flags,
'old_text' => $row->old_text
];
}
$this->assertSame( $expected, $actual );
}
/**
* @return array Expected content of the text table
*/
private function insertUpdateText() {
$serializedArr = serialize( self::VARS );
$jsonArr = FormatJson::encode( self::VARS );
$textRows = [
'Serialized VH' => [ 'old_text' => self::SERIALIZED_VH ] + self::TEXT_ROW,
'Serialized array' =>
[ 'old_text' => $serializedArr, 'old_flags' => 'nativeDataArray' ] + self::TEXT_ROW,
'JSON array' => [ 'old_text' => $jsonArr, 'old_flags' => 'utf-8' ] + self::TEXT_ROW,
];
$this->db->insert( 'text', array_values( $textRows ), __METHOD__ );
$pointerRows = [
[ 'afl_var_dump' => 'stored-text:1' ] + self::$aflRow,
[ 'afl_var_dump' => 'stored-text:2' ] + self::$aflRow,
[ 'afl_var_dump' => 'stored-text:3' ] + self::$aflRow,
];
$this->db->insert( 'abuse_filter_log', $pointerRows, __METHOD__ );
return [
[ 'old_id' => 1, 'old_flags' => 'utf-8', 'old_text' => $jsonArr ],
[ 'old_id' => 2, 'old_flags' => 'utf-8', 'old_text' => $jsonArr ],
[ 'old_id' => 3, 'old_flags' => 'utf-8', 'old_text' => $jsonArr ],
];
}
/**
* @covers ::doUpdateText
*/
public function testUpdateTextWrongFlags() {
$jsonArr = FormatJson::encode( self::VARS );
$textRow = [ 'old_id' => 1, 'old_flags' => 'nativeDataArray,utf-8', 'old_text' => $jsonArr ];
$this->db->insert( 'text', $textRow, __METHOD__ );
$pointerRow = [ 'afl_var_dump' => 'stored-text:1' ] + self::$aflRow;
$this->db->insert( 'abuse_filter_log', $pointerRow, __METHOD__ );
$maint = $this->getMaintenanceWithoutExit();
$this->expectException( LogicException::class );
$maint->updateText();
}
/**
* @covers ::updateAflVarDump
*/
public function testUpdateAflVarDump() {
$this->insertAflVarDump();
$this->maintenance->updateAflVarDump();
$vals = $this->db->selectFieldValues( 'abuse_filter_log', 'afl_var_dump' );
$this->assertSame( [ 'tt:123' ], array_unique( $vals ) );
}
private function insertAflVarDump() {
$rows = [
'Old prefix' => [ 'afl_var_dump' => 'stored-text:123' ] + self::$aflRow,
'New prefix' => [ 'afl_var_dump' => 'tt:123' ] + self::$aflRow
];
$this->db->insert( 'abuse_filter_log', array_values( $rows ), __METHOD__ );
}
/**
* @param array $old
* @param array $expected
* @covers UpdateVarDumps::updateVariables
* @dataProvider provideUpdateVariables
*/
public function testUpdateVariables( array $old, array $expected ) {
$this->assertSame( $expected, $this->maintenance->updateVariables( $old ) );
}
/**
* @return array
*/
public function provideUpdateVariables() {
return [
'Fine' => [ self::VARS, self::VARS ],
'Meta-variable' => [ [ 'action' => 'edit', 'context' => 'foo' ], [ 'action' => 'edit' ] ],
'Uppercase' => [ [ 'USER_GROUPS' => [ 'bot' ] ], [ 'user_groups' => [ 'bot' ] ] ],
'Deprecated' => [
[ 'article_text' => 'foo', 'moved_to_prefixedtext' => 'bar' ],
[ 'page_title' => 'foo', 'moved_to_prefixedtitle' => 'bar' ]
],
'Mixed' => [
[ 'ARTICLE_ARTICLEID' => 1, 'logged_local_ids' => [ 1, 2, 3 ], 'OLD_HTML' => '' ],
[ 'page_id' => 1, 'old_html' => '' ]
]
];
}
}