wiki.techinc.nl/tests/phpunit/includes/CommentStoreTest.php
Brad Jorsch 5c228f5a18 Have CommentStore limit the maximum character length of comments
I92954c9 accidentally allowed comments, when CommentStore is enabled, to
be the full 65535 bytes available in the database field. This was never
intended.

There is not yet any consensus in T6714 or T6715 on just how long the
longer comments should be, for now we'll set 1000 because Tim says so.
Note this doesn't change the UI to actually allow more characters and
will only take effect once $wgCommentTableSchemaMigrationStage is raised
above MIGRATION_OLD.

Note this also doesn't make the limit configurable. That too can be done
later if needed, again along with whatever changes are necessary to the
UI to account for a variable limit.

Change-Id: I7e0c55619210ebab467436f0bb915271c0a7ccdc
2017-10-10 20:59:52 -04:00

647 lines
21 KiB
PHP

<?php
use Wikimedia\ScopedCallback;
use Wikimedia\TestingAccessWrapper;
/**
* @group Database
* @covers CommentStore
* @covers CommentStoreComment
*/
class CommentStoreTest extends MediaWikiLangTestCase {
protected $tablesUsed = [
'revision',
'revision_comment_temp',
'ipblocks',
'comment',
];
/**
* Create a store for a particular stage
* @param int $stage
* @param string $key
* @return CommentStore
*/
protected function makeStore( $stage, $key ) {
$store = new CommentStore( $key );
TestingAccessWrapper::newFromObject( $store )->stage = $stage;
return $store;
}
/**
* @dataProvider provideGetFields
* @param int $stage
* @param string $key
* @param array $expect
*/
public function testGetFields( $stage, $key, $expect ) {
$store = $this->makeStore( $stage, $key );
$result = $store->getFields();
$this->assertEquals( $expect, $result );
}
public static function provideGetFields() {
return [
'Simple table, old' => [
MIGRATION_OLD, 'ipb_reason',
[ 'ipb_reason_text' => 'ipb_reason', 'ipb_reason_data' => 'NULL', 'ipb_reason_cid' => 'NULL' ],
],
'Simple table, write-both' => [
MIGRATION_WRITE_BOTH, 'ipb_reason',
[ 'ipb_reason_old' => 'ipb_reason', 'ipb_reason_id' => 'ipb_reason_id' ],
],
'Simple table, write-new' => [
MIGRATION_WRITE_NEW, 'ipb_reason',
[ 'ipb_reason_old' => 'ipb_reason', 'ipb_reason_id' => 'ipb_reason_id' ],
],
'Simple table, new' => [
MIGRATION_NEW, 'ipb_reason',
[ 'ipb_reason_id' => 'ipb_reason_id' ],
],
'Revision, old' => [
MIGRATION_OLD, 'rev_comment',
[
'rev_comment_text' => 'rev_comment',
'rev_comment_data' => 'NULL',
'rev_comment_cid' => 'NULL',
],
],
'Revision, write-both' => [
MIGRATION_WRITE_BOTH, 'rev_comment',
[ 'rev_comment_old' => 'rev_comment', 'rev_comment_pk' => 'rev_id' ],
],
'Revision, write-new' => [
MIGRATION_WRITE_NEW, 'rev_comment',
[ 'rev_comment_old' => 'rev_comment', 'rev_comment_pk' => 'rev_id' ],
],
'Revision, new' => [
MIGRATION_NEW, 'rev_comment',
[ 'rev_comment_pk' => 'rev_id' ],
],
'Image, old' => [
MIGRATION_OLD, 'img_description',
[
'img_description_text' => 'img_description',
'img_description_data' => 'NULL',
'img_description_cid' => 'NULL',
],
],
'Image, write-both' => [
MIGRATION_WRITE_BOTH, 'img_description',
[ 'img_description_old' => 'img_description', 'img_description_pk' => 'img_name' ],
],
'Image, write-new' => [
MIGRATION_WRITE_NEW, 'img_description',
[ 'img_description_old' => 'img_description', 'img_description_pk' => 'img_name' ],
],
'Image, new' => [
MIGRATION_NEW, 'img_description',
[ 'img_description_pk' => 'img_name' ],
],
];
}
/**
* @dataProvider provideGetJoin
* @param int $stage
* @param string $key
* @param array $expect
*/
public function testGetJoin( $stage, $key, $expect ) {
$store = $this->makeStore( $stage, $key );
$result = $store->getJoin();
$this->assertEquals( $expect, $result );
}
public static function provideGetJoin() {
return [
'Simple table, old' => [
MIGRATION_OLD, 'ipb_reason', [
'tables' => [],
'fields' => [
'ipb_reason_text' => 'ipb_reason',
'ipb_reason_data' => 'NULL',
'ipb_reason_cid' => 'NULL',
],
'joins' => [],
],
],
'Simple table, write-both' => [
MIGRATION_WRITE_BOTH, 'ipb_reason', [
'tables' => [ 'comment_ipb_reason' => 'comment' ],
'fields' => [
'ipb_reason_text' => 'COALESCE( comment_ipb_reason.comment_text, ipb_reason )',
'ipb_reason_data' => 'comment_ipb_reason.comment_data',
'ipb_reason_cid' => 'comment_ipb_reason.comment_id',
],
'joins' => [
'comment_ipb_reason' => [ 'LEFT JOIN', 'comment_ipb_reason.comment_id = ipb_reason_id' ],
],
],
],
'Simple table, write-new' => [
MIGRATION_WRITE_NEW, 'ipb_reason', [
'tables' => [ 'comment_ipb_reason' => 'comment' ],
'fields' => [
'ipb_reason_text' => 'COALESCE( comment_ipb_reason.comment_text, ipb_reason )',
'ipb_reason_data' => 'comment_ipb_reason.comment_data',
'ipb_reason_cid' => 'comment_ipb_reason.comment_id',
],
'joins' => [
'comment_ipb_reason' => [ 'LEFT JOIN', 'comment_ipb_reason.comment_id = ipb_reason_id' ],
],
],
],
'Simple table, new' => [
MIGRATION_NEW, 'ipb_reason', [
'tables' => [ 'comment_ipb_reason' => 'comment' ],
'fields' => [
'ipb_reason_text' => 'comment_ipb_reason.comment_text',
'ipb_reason_data' => 'comment_ipb_reason.comment_data',
'ipb_reason_cid' => 'comment_ipb_reason.comment_id',
],
'joins' => [
'comment_ipb_reason' => [ 'JOIN', 'comment_ipb_reason.comment_id = ipb_reason_id' ],
],
],
],
'Revision, old' => [
MIGRATION_OLD, 'rev_comment', [
'tables' => [],
'fields' => [
'rev_comment_text' => 'rev_comment',
'rev_comment_data' => 'NULL',
'rev_comment_cid' => 'NULL',
],
'joins' => [],
],
],
'Revision, write-both' => [
MIGRATION_WRITE_BOTH, 'rev_comment', [
'tables' => [
'temp_rev_comment' => 'revision_comment_temp',
'comment_rev_comment' => 'comment',
],
'fields' => [
'rev_comment_text' => 'COALESCE( comment_rev_comment.comment_text, rev_comment )',
'rev_comment_data' => 'comment_rev_comment.comment_data',
'rev_comment_cid' => 'comment_rev_comment.comment_id',
],
'joins' => [
'temp_rev_comment' => [ 'LEFT JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'comment_rev_comment' => [ 'LEFT JOIN',
'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
],
],
],
'Revision, write-new' => [
MIGRATION_WRITE_NEW, 'rev_comment', [
'tables' => [
'temp_rev_comment' => 'revision_comment_temp',
'comment_rev_comment' => 'comment',
],
'fields' => [
'rev_comment_text' => 'COALESCE( comment_rev_comment.comment_text, rev_comment )',
'rev_comment_data' => 'comment_rev_comment.comment_data',
'rev_comment_cid' => 'comment_rev_comment.comment_id',
],
'joins' => [
'temp_rev_comment' => [ 'LEFT JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'comment_rev_comment' => [ 'LEFT JOIN',
'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
],
],
],
'Revision, new' => [
MIGRATION_NEW, 'rev_comment', [
'tables' => [
'temp_rev_comment' => 'revision_comment_temp',
'comment_rev_comment' => 'comment',
],
'fields' => [
'rev_comment_text' => 'comment_rev_comment.comment_text',
'rev_comment_data' => 'comment_rev_comment.comment_data',
'rev_comment_cid' => 'comment_rev_comment.comment_id',
],
'joins' => [
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'comment_rev_comment' => [ 'JOIN',
'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
],
],
],
'Image, old' => [
MIGRATION_OLD, 'img_description', [
'tables' => [],
'fields' => [
'img_description_text' => 'img_description',
'img_description_data' => 'NULL',
'img_description_cid' => 'NULL',
],
'joins' => [],
],
],
'Image, write-both' => [
MIGRATION_WRITE_BOTH, 'img_description', [
'tables' => [
'temp_img_description' => 'image_comment_temp',
'comment_img_description' => 'comment',
],
'fields' => [
'img_description_text' => 'COALESCE( comment_img_description.comment_text, img_description )',
'img_description_data' => 'comment_img_description.comment_data',
'img_description_cid' => 'comment_img_description.comment_id',
],
'joins' => [
'temp_img_description' => [ 'LEFT JOIN', 'temp_img_description.imgcomment_name = img_name' ],
'comment_img_description' => [ 'LEFT JOIN',
'comment_img_description.comment_id = temp_img_description.imgcomment_description_id' ],
],
],
],
'Image, write-new' => [
MIGRATION_WRITE_NEW, 'img_description', [
'tables' => [
'temp_img_description' => 'image_comment_temp',
'comment_img_description' => 'comment',
],
'fields' => [
'img_description_text' => 'COALESCE( comment_img_description.comment_text, img_description )',
'img_description_data' => 'comment_img_description.comment_data',
'img_description_cid' => 'comment_img_description.comment_id',
],
'joins' => [
'temp_img_description' => [ 'LEFT JOIN', 'temp_img_description.imgcomment_name = img_name' ],
'comment_img_description' => [ 'LEFT JOIN',
'comment_img_description.comment_id = temp_img_description.imgcomment_description_id' ],
],
],
],
'Image, new' => [
MIGRATION_NEW, 'img_description', [
'tables' => [
'temp_img_description' => 'image_comment_temp',
'comment_img_description' => 'comment',
],
'fields' => [
'img_description_text' => 'comment_img_description.comment_text',
'img_description_data' => 'comment_img_description.comment_data',
'img_description_cid' => 'comment_img_description.comment_id',
],
'joins' => [
'temp_img_description' => [ 'JOIN', 'temp_img_description.imgcomment_name = img_name' ],
'comment_img_description' => [ 'JOIN',
'comment_img_description.comment_id = temp_img_description.imgcomment_description_id' ],
],
],
],
];
}
private function assertComment( $expect, $actual, $from ) {
$this->assertSame( $expect['text'], $actual->text, "text $from" );
$this->assertInstanceOf( get_class( $expect['message'] ), $actual->message,
"message class $from" );
$this->assertSame( $expect['message']->getKeysToTry(), $actual->message->getKeysToTry(),
"message keys $from" );
$this->assertEquals( $expect['message']->text(), $actual->message->text(),
"message rendering $from" );
$this->assertEquals( $expect['data'], $actual->data, "data $from" );
}
/**
* @dataProvider provideInsertRoundTrip
* @param string $table
* @param string $key
* @param string $pk
* @param string $extraFields
* @param string|Message $comment
* @param array|null $data
* @param array $expect
*/
public function testInsertRoundTrip( $table, $key, $pk, $extraFields, $comment, $data, $expect ) {
$expectOld = [
'text' => $expect['text'],
'message' => new RawMessage( '$1', [ $expect['text'] ] ),
'data' => null,
];
$stages = [
MIGRATION_OLD => [ MIGRATION_OLD, MIGRATION_WRITE_NEW ],
MIGRATION_WRITE_BOTH => [ MIGRATION_OLD, MIGRATION_NEW ],
MIGRATION_WRITE_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
MIGRATION_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
];
foreach ( $stages as $writeStage => $readRange ) {
if ( $key === 'ipb_reason' ) {
$extraFields['ipb_address'] = __CLASS__ . "#$writeStage";
}
$wstore = $this->makeStore( $writeStage, $key );
$usesTemp = $key === 'rev_comment';
if ( $usesTemp ) {
list( $fields, $callback ) = $wstore->insertWithTempTable( $this->db, $comment, $data );
} else {
$fields = $wstore->insert( $this->db, $comment, $data );
}
if ( $writeStage <= MIGRATION_WRITE_BOTH ) {
$this->assertSame( $expect['text'], $fields[$key], "old field, stage=$writeStage" );
} else {
$this->assertArrayNotHasKey( $key, $fields, "old field, stage=$writeStage" );
}
if ( $writeStage >= MIGRATION_WRITE_BOTH && !$usesTemp ) {
$this->assertArrayHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
} else {
$this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
}
$this->db->insert( $table, $extraFields + $fields, __METHOD__ );
$id = $this->db->insertId();
if ( $usesTemp ) {
$callback( $id );
}
for ( $readStage = $readRange[0]; $readStage <= $readRange[1]; $readStage++ ) {
$rstore = $this->makeStore( $readStage, $key );
$fieldRow = $this->db->selectRow(
$table,
$rstore->getFields(),
[ $pk => $id ],
__METHOD__
);
$queryInfo = $rstore->getJoin();
$joinRow = $this->db->selectRow(
[ $table ] + $queryInfo['tables'],
$queryInfo['fields'],
[ $pk => $id ],
__METHOD__,
[],
$queryInfo['joins']
);
$this->assertComment(
$writeStage === MIGRATION_OLD || $readStage === MIGRATION_OLD ? $expectOld : $expect,
$rstore->getCommentLegacy( $this->db, $fieldRow ),
"w=$writeStage, r=$readStage, from getFields()"
);
$this->assertComment(
$writeStage === MIGRATION_OLD || $readStage === MIGRATION_OLD ? $expectOld : $expect,
$rstore->getComment( $joinRow ),
"w=$writeStage, r=$readStage, from getJoin()"
);
}
}
}
public static function provideInsertRoundTrip() {
$db = wfGetDB( DB_REPLICA ); // for timestamps
$msgComment = new Message( 'parentheses', [ 'message comment' ] );
$textCommentMsg = new RawMessage( '$1', [ 'text comment' ] );
$nestedMsgComment = new Message( [ 'parentheses', 'rawmessage' ], [ new Message( 'mainpage' ) ] );
$ipbfields = [
'ipb_range_start' => '',
'ipb_range_end' => '',
'ipb_by' => 0,
'ipb_timestamp' => $db->timestamp(),
'ipb_expiry' => $db->getInfinity(),
];
$revfields = [
'rev_page' => 42,
'rev_text_id' => 42,
'rev_len' => 0,
'rev_user' => 0,
'rev_user_text' => '',
'rev_timestamp' => $db->timestamp(),
];
$comStoreComment = new CommentStoreComment(
null, 'comment store comment', null, [ 'foo' => 'bar' ]
);
return [
'Simple table, text comment' => [
'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', null, [
'text' => 'text comment',
'message' => $textCommentMsg,
'data' => null,
]
],
'Simple table, text comment with data' => [
'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', [ 'message' => 42 ], [
'text' => 'text comment',
'message' => $textCommentMsg,
'data' => [ 'message' => 42 ],
]
],
'Simple table, message comment' => [
'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, null, [
'text' => '(message comment)',
'message' => $msgComment,
'data' => null,
]
],
'Simple table, message comment with data' => [
'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, [ 'message' => 42 ], [
'text' => '(message comment)',
'message' => $msgComment,
'data' => [ 'message' => 42 ],
]
],
'Simple table, nested message comment' => [
'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $nestedMsgComment, null, [
'text' => '(Main Page)',
'message' => $nestedMsgComment,
'data' => null,
]
],
'Simple table, CommentStoreComment' => [
'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, clone $comStoreComment, [ 'baz' => 'baz' ], [
'text' => 'comment store comment',
'message' => $comStoreComment->message,
'data' => [ 'foo' => 'bar' ],
]
],
'Revision, text comment' => [
'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', null, [
'text' => 'text comment',
'message' => $textCommentMsg,
'data' => null,
]
],
'Revision, text comment with data' => [
'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', [ 'message' => 42 ], [
'text' => 'text comment',
'message' => $textCommentMsg,
'data' => [ 'message' => 42 ],
]
],
'Revision, message comment' => [
'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, null, [
'text' => '(message comment)',
'message' => $msgComment,
'data' => null,
]
],
'Revision, message comment with data' => [
'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, [ 'message' => 42 ], [
'text' => '(message comment)',
'message' => $msgComment,
'data' => [ 'message' => 42 ],
]
],
'Revision, nested message comment' => [
'revision', 'rev_comment', 'rev_id', $revfields, $nestedMsgComment, null, [
'text' => '(Main Page)',
'message' => $nestedMsgComment,
'data' => null,
]
],
'Revision, CommentStoreComment' => [
'revision', 'rev_comment', 'rev_id', $revfields, clone $comStoreComment, [ 'baz' => 'baz' ], [
'text' => 'comment store comment',
'message' => $comStoreComment->message,
'data' => [ 'foo' => 'bar' ],
]
],
];
}
public function testGetCommentErrors() {
MediaWiki\suppressWarnings();
$reset = new ScopedCallback( 'MediaWiki\restoreWarnings' );
$store = $this->makeStore( MIGRATION_OLD, 'dummy' );
$res = $store->getComment( [ 'dummy' => 'comment' ] );
$this->assertSame( '', $res->text );
$res = $store->getComment( [ 'dummy' => 'comment' ], true );
$this->assertSame( 'comment', $res->text );
$store = $this->makeStore( MIGRATION_NEW, 'dummy' );
try {
$store->getComment( [ 'dummy' => 'comment' ] );
$this->fail( 'Expected exception not thrown' );
} catch ( InvalidArgumentException $ex ) {
$this->assertSame( '$row does not contain fields needed for comment dummy', $ex->getMessage() );
}
$res = $store->getComment( [ 'dummy' => 'comment' ], true );
$this->assertSame( 'comment', $res->text );
try {
$store->getComment( [ 'dummy_id' => 1 ] );
$this->fail( 'Expected exception not thrown' );
} catch ( InvalidArgumentException $ex ) {
$this->assertSame(
'$row does not contain fields needed for comment dummy and getComment(), '
. 'but does have fields for getCommentLegacy()',
$ex->getMessage()
);
}
$store = $this->makeStore( MIGRATION_NEW, 'rev_comment' );
try {
$store->getComment( [ 'rev_comment' => 'comment' ] );
$this->fail( 'Expected exception not thrown' );
} catch ( InvalidArgumentException $ex ) {
$this->assertSame(
'$row does not contain fields needed for comment rev_comment', $ex->getMessage()
);
}
$res = $store->getComment( [ 'rev_comment' => 'comment' ], true );
$this->assertSame( 'comment', $res->text );
try {
$store->getComment( [ 'rev_comment_pk' => 1 ] );
$this->fail( 'Expected exception not thrown' );
} catch ( InvalidArgumentException $ex ) {
$this->assertSame(
'$row does not contain fields needed for comment rev_comment and getComment(), '
. 'but does have fields for getCommentLegacy()',
$ex->getMessage()
);
}
}
public static function provideStages() {
return [
'MIGRATION_OLD' => [ MIGRATION_OLD ],
'MIGRATION_WRITE_BOTH' => [ MIGRATION_WRITE_BOTH ],
'MIGRATION_WRITE_NEW' => [ MIGRATION_WRITE_NEW ],
'MIGRATION_NEW' => [ MIGRATION_NEW ],
];
}
/**
* @dataProvider provideStages
* @param int $stage
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Must use insertWithTempTable() for rev_comment
*/
public function testInsertWrong( $stage ) {
$store = $this->makeStore( $stage, 'rev_comment' );
$store->insert( $this->db, 'foo' );
}
/**
* @dataProvider provideStages
* @param int $stage
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Must use insert() for ipb_reason
*/
public function testInsertWithTempTableWrong( $stage ) {
$store = $this->makeStore( $stage, 'ipb_reason' );
$store->insertWithTempTable( $this->db, 'foo' );
}
/**
* @dataProvider provideStages
* @param int $stage
*/
public function testInsertWithTempTableDeprecated( $stage ) {
$wrap = TestingAccessWrapper::newFromClass( CommentStore::class );
$wrap->formerTempTables += [ 'ipb_reason' => '1.30' ];
$this->hideDeprecated( 'CommentStore::insertWithTempTable for ipb_reason' );
$store = $this->makeStore( $stage, 'ipb_reason' );
list( $fields, $callback ) = $store->insertWithTempTable( $this->db, 'foo' );
$this->assertTrue( is_callable( $callback ) );
}
public function testInsertTruncation() {
$comment = str_repeat( '💣', 16400 );
$truncated1 = str_repeat( '💣', 63 ) . '...';
$truncated2 = str_repeat( '💣', CommentStore::COMMENT_CHARACTER_LIMIT - 3 ) . '...';
$store = $this->makeStore( MIGRATION_WRITE_BOTH, 'ipb_reason' );
$fields = $store->insert( $this->db, $comment );
$this->assertSame( $truncated1, $fields['ipb_reason'] );
$stored = $this->db->selectField(
'comment', 'comment_text', [ 'comment_id' => $fields['ipb_reason_id'] ], __METHOD__
);
$this->assertSame( $truncated2, $stored );
}
/**
* @expectedException OverflowException
* @expectedExceptionMessage Comment data is too long (65611 bytes, maximum is 65535)
*/
public function testInsertTooMuchData() {
$store = $this->makeStore( MIGRATION_WRITE_BOTH, 'ipb_reason' );
$store->insert( $this->db, 'foo', [
'long' => str_repeat( '💣', 16400 )
] );
}
public function testConstructor() {
$this->assertInstanceOf( CommentStore::class, CommentStore::newKey( 'dummy' ) );
}
}