3 namespace MediaWiki\Tests\Revision
;
5 use CommentStoreComment
;
7 use MediaWiki\MediaWikiServices
;
8 use MediaWiki\Revision\MutableRevisionRecord
;
9 use MediaWiki\Revision\RevisionRecord
;
10 use MediaWiki\Revision\SlotRecord
;
11 use MediaWiki\Storage\BlobStore
;
12 use MediaWiki\Storage\SqlBlobStore
;
17 use Wikimedia\TestingAccessWrapper
;
21 * Tests RevisionStore against the post-migration MCR DB schema.
23 * @covers \MediaWiki\Revision\RevisionStore
25 * @group RevisionStore
30 class McrRevisionStoreDbTest
extends RevisionStoreDbTestBase
{
32 use McrSchemaOverride
;
34 protected function assertRevisionExistsInDatabase( RevisionRecord
$rev ) {
35 $numberOfSlots = count( $rev->getSlotRoles() );
37 // new schema is written
41 [ 'slot_revision_id' => $rev->getId() ],
42 [ [ (string)$numberOfSlots ] ]
45 $store = MediaWikiServices
::getInstance()->getRevisionStore();
46 $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
52 'slot_revision_id' => $rev->getId(),
54 [ [ (string)$numberOfSlots ] ],
59 parent
::assertRevisionExistsInDatabase( $rev );
63 * @param SlotRecord $a
64 * @param SlotRecord $b
66 protected function assertSameSlotContent( SlotRecord
$a, SlotRecord
$b ) {
67 parent
::assertSameSlotContent( $a, $b );
69 // Assert that the same content ID has been used
70 $this->assertSame( $a->getContentId(), $b->getContentId() );
73 public function provideInsertRevisionOn_successes() {
74 foreach ( parent
::provideInsertRevisionOn_successes() as $case ) {
78 yield
'Multi-slot revision insertion' => [
81 'main' => new WikitextContent( 'Chicken' ),
82 'aux' => new TextContent( 'Egg' ),
85 'comment' => $this->getRandomCommentStoreComment(),
86 'timestamp' => '20171117010101',
92 public function provideNewNullRevision() {
93 foreach ( parent
::provideNewNullRevision() as $case ) {
98 Title
::newFromText( 'UTPage_notAutoCreated' ),
101 'main' => new WikitextContent( 'Chicken' ),
102 'aux' => new WikitextContent( 'Omelet' ),
105 CommentStoreComment
::newUnsavedComment( __METHOD__
. ' comment multi' ),
109 public function provideNewMutableRevisionFromArray() {
110 foreach ( parent
::provideNewMutableRevisionFromArray() as $case ) {
114 yield
'Basic array, multiple roles' => [
118 'timestamp' => '20171017114835',
119 'user_text' => '111.0.1.2',
121 'minor_edit' => false,
125 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
126 'comment' => 'Goat Comment!',
128 'main' => new WikitextContent( 'Söme Cöntent' ),
129 'aux' => new TextContent( 'Öther Cöntent' ),
135 public function testGetQueryInfo_NoSlotDataJoin() {
136 $store = MediaWikiServices
::getInstance()->getRevisionStore();
137 $queryInfo = $store->getQueryInfo();
139 // with the new schema enabled, query info should not join the main slot info
140 $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
141 $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
145 * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
146 * @covers \MediaWiki\Revision\RevisionStore::insertSlotRowOn
147 * @covers \MediaWiki\Revision\RevisionStore::insertContentRowOn
149 public function testInsertRevisionOn_T202032() {
150 // This test only makes sense for MySQL
151 if ( $this->db
->getType() !== 'mysql' ) {
152 $this->assertTrue( true );
156 // NOTE: must be done before checking MAX(rev_id)
157 $page = $this->getTestPage();
159 $maxRevId = $this->db
->selectField( 'revision', 'MAX(rev_id)' );
161 // Construct a slot row that will conflict with the insertion of the next revision ID,
162 // to emulate the failure mode described in T202032. Nothing will ever read this row,
163 // we just need it to trigger a primary key conflict.
164 $this->db
->insert( 'slots', [
165 'slot_revision_id' => $maxRevId +
1,
167 'slot_content_id' => 0,
171 $rev = new MutableRevisionRecord( $page->getTitle() );
172 $rev->setTimestamp( '20180101000000' );
173 $rev->setComment( CommentStoreComment
::newUnsavedComment( 'test' ) );
174 $rev->setUser( $this->getTestUser()->getUser() );
175 $rev->setContent( 'main', new WikitextContent( 'Text' ) );
176 $rev->setPageId( $page->getId() );
178 $store = MediaWikiServices
::getInstance()->getRevisionStore();
179 $return = $store->insertRevisionOn( $rev, $this->db
);
181 $this->assertSame( $maxRevId +
2, $return->getId() );
183 // is the new revision correct?
184 $this->assertRevisionCompleteness( $return );
185 $this->assertRevisionRecordsEqual( $rev, $return );
187 // can we find it directly in the database?
188 $this->assertRevisionExistsInDatabase( $return );
190 // can we load it from the store?
191 $loaded = $store->getRevisionById( $return->getId() );
192 $this->assertRevisionCompleteness( $loaded );
193 $this->assertRevisionRecordsEqual( $return, $loaded );
197 * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given
202 protected function getSlotRevisionConditions( $revId ) {
203 return [ 'slot_revision_id' => $revId ];
207 * @covers \MediaWiki\Revision\RevisionStore::getContentBlobsForBatch
208 * @throws \MWException
210 public function testGetContentBlobsForBatch_error() {
211 $page1 = $this->getTestPage();
212 $text = __METHOD__
. 'b-ä';
213 $editStatus = $this->editPage( $page1->getTitle()->getPrefixedDBkey(), $text . '1' );
214 $this->assertTrue( $editStatus->isGood(), 'Sanity: must create revision 1' );
215 /** @var Revision $rev1 */
216 $rev1 = $editStatus->getValue()['revision'];
218 $contentAddress = $rev1->getRevisionRecord()->getSlot( SlotRecord
::MAIN
)->getAddress();
219 $blobStatus = StatusValue
::newGood( [] );
220 $blobStatus->warning( 'internalerror', 'oops!' );
222 $mockBlobStore = $this->getMock( BlobStore
::class );
223 $mockBlobStore->method( 'getBlobBatch' )
224 ->willReturn( $blobStatus );
226 $revStore = MediaWikiServices
::getInstance()
227 ->getRevisionStoreFactory()
228 ->getRevisionStore();
229 $wrappedRevStore = TestingAccessWrapper
::newFromObject( $revStore );
230 $wrappedRevStore->blobStore
= $mockBlobStore;
232 $result = $revStore->getContentBlobsForBatch( [ $rev1->getId() ] );
233 $this->assertTrue( $result->isOK() );
234 $this->assertFalse( $result->isGood() );
235 $this->assertNotEmpty( $result->getErrors() );
237 $records = $result->getValue();
238 $this->assertArrayHasKey( $rev1->getId(), $records );
240 $mainRow = $records[$rev1->getId()][SlotRecord
::MAIN
];
241 $this->assertNull( $mainRow->blob_data
);
245 'message' => 'internalerror',
252 'message' => 'internalerror',
254 "Couldn't find blob data for rev " . $rev1->getId()
257 ], $result->getErrors() );
261 * @covers \MediaWiki\Revision\RevisionStore::getContentBlobsForBatch
263 public function testGetContentBlobsForBatchUsesGetBlobBatch() {
264 $page1 = $this->getTestPage();
265 $text = __METHOD__
. 'b-ä';
266 $editStatus = $this->editPage( $page1->getTitle()->getPrefixedDBkey(), $text . '1' );
267 $this->assertTrue( $editStatus->isGood(), 'Sanity: must create revision 1' );
268 /** @var Revision $rev1 */
269 $rev1 = $editStatus->getValue()['revision'];
271 $contentAddress = $rev1->getRevisionRecord()->getSlot( SlotRecord
::MAIN
)->getAddress();
272 $mockBlobStore = $this->getMockBuilder( SqlBlobStore
::class )
273 ->disableOriginalConstructor()
276 ->expects( $this->once() )
277 ->method( 'getBlobBatch' )
278 ->with( [ $contentAddress ], $this->anything() )
279 ->willReturn( StatusValue
::newGood( [
280 $contentAddress => 'Content_From_Mock'
283 ->expects( $this->never() )
284 ->method( 'getBlob' );
286 $revStore = MediaWikiServices
::getInstance()
287 ->getRevisionStoreFactory()
288 ->getRevisionStore();
289 $wrappedRevStore = TestingAccessWrapper
::newFromObject( $revStore );
290 $wrappedRevStore->blobStore
= $mockBlobStore;
292 $result = $revStore->getContentBlobsForBatch(
296 $this->assertTrue( $result->isGood() );
297 $this->assertSame( 'Content_From_Mock',
298 $result->getValue()[$rev1->getId()][SlotRecord
::MAIN
]->blob_data
);
302 * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
303 * @throws \MWException
305 public function testNewRevisionsFromBatch_error() {
306 $page = $this->getTestPage();
307 $text = __METHOD__
. 'b-ä';
308 /** @var Revision $rev1 */
309 $rev1 = $page->doEditContent(
310 new WikitextContent( $text . '1' ),
314 $this->getTestUser()->getUser()
315 )->value
['revision'];
316 $invalidRow = $this->revisionToRow( $rev1 );
317 $invalidRow->rev_id
= 100500;
318 $result = MediaWikiServices
::getInstance()->getRevisionStore()
319 ->newRevisionsFromBatch(
320 [ $this->revisionToRow( $rev1 ), $invalidRow ],
322 'slots' => [ SlotRecord
::MAIN
],
326 $this->assertFalse( $result->isGood() );
327 $this->assertNotEmpty( $result->getErrors() );
328 $records = $result->getValue();
329 $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
330 $this->assertSame( $text . '1',
331 $records[$rev1->getId()]->getContent( SlotRecord
::MAIN
)->serialize() );
332 $this->assertEquals( $page->getTitle()->getDBkey(),
333 $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
334 $this->assertNull( $records[$invalidRow->rev_id
] );
335 $this->assertSame( [ [
337 'message' => 'internalerror',
339 "Couldn't find slots for rev 100500"
341 ] ], $result->getErrors() );
345 * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
347 public function testNewRevisionFromBatchUsesGetBlobBatch() {
348 $page1 = $this->getTestPage();
349 $text = __METHOD__
. 'b-ä';
350 $editStatus = $this->editPage( $page1->getTitle()->getPrefixedDBkey(), $text . '1' );
351 $this->assertTrue( $editStatus->isGood(), 'Sanity: must create revision 1' );
352 /** @var Revision $rev1 */
353 $rev1 = $editStatus->getValue()['revision'];
355 $contentAddress = $rev1->getRevisionRecord()->getSlot( SlotRecord
::MAIN
)->getAddress();
356 $mockBlobStore = $this->getMockBuilder( SqlBlobStore
::class )
357 ->disableOriginalConstructor()
360 ->expects( $this->once() )
361 ->method( 'getBlobBatch' )
362 ->with( [ $contentAddress ], $this->anything() )
363 ->willReturn( StatusValue
::newGood( [
364 $contentAddress => 'Content_From_Mock'
367 ->expects( $this->never() )
368 ->method( 'getBlob' );
370 $revStore = MediaWikiServices
::getInstance()
371 ->getRevisionStoreFactory()
372 ->getRevisionStore();
373 $wrappedRevStore = TestingAccessWrapper
::newFromObject( $revStore );
374 $wrappedRevStore->blobStore
= $mockBlobStore;
376 $result = $revStore->newRevisionsFromBatch(
377 [ $this->revisionToRow( $rev1 ) ],
379 'slots' => [ SlotRecord
::MAIN
],
383 $this->assertTrue( $result->isGood() );
384 $this->assertSame( 'Content_From_Mock',
385 ContentHandler
::getContentText( $result->getValue()[$rev1->getId()]
386 ->getContent( SlotRecord
::MAIN
) ) );