Merge "tests: Fix broken assertion in ApiQueryAllPagesTest"
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / McrRevisionStoreDbTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use CommentStoreComment;
6 use ContentHandler;
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;
13 use Revision;
14 use StatusValue;
15 use TextContent;
16 use Title;
17 use Wikimedia\TestingAccessWrapper;
18 use WikitextContent;
19
20 /**
21 * Tests RevisionStore against the post-migration MCR DB schema.
22 *
23 * @covers \MediaWiki\Revision\RevisionStore
24 *
25 * @group RevisionStore
26 * @group Storage
27 * @group Database
28 * @group medium
29 */
30 class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
31
32 use McrSchemaOverride;
33
34 protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
35 $numberOfSlots = count( $rev->getSlotRoles() );
36
37 // new schema is written
38 $this->assertSelect(
39 'slots',
40 [ 'count(*)' ],
41 [ 'slot_revision_id' => $rev->getId() ],
42 [ [ (string)$numberOfSlots ] ]
43 );
44
45 $store = MediaWikiServices::getInstance()->getRevisionStore();
46 $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
47
48 $this->assertSelect(
49 $revQuery['tables'],
50 [ 'count(*)' ],
51 [
52 'slot_revision_id' => $rev->getId(),
53 ],
54 [ [ (string)$numberOfSlots ] ],
55 [],
56 $revQuery['joins']
57 );
58
59 parent::assertRevisionExistsInDatabase( $rev );
60 }
61
62 /**
63 * @param SlotRecord $a
64 * @param SlotRecord $b
65 */
66 protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
67 parent::assertSameSlotContent( $a, $b );
68
69 // Assert that the same content ID has been used
70 $this->assertSame( $a->getContentId(), $b->getContentId() );
71 }
72
73 public function provideInsertRevisionOn_successes() {
74 foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
75 yield $case;
76 }
77
78 yield 'Multi-slot revision insertion' => [
79 [
80 'content' => [
81 'main' => new WikitextContent( 'Chicken' ),
82 'aux' => new TextContent( 'Egg' ),
83 ],
84 'page' => true,
85 'comment' => $this->getRandomCommentStoreComment(),
86 'timestamp' => '20171117010101',
87 'user' => true,
88 ],
89 ];
90 }
91
92 public function provideNewNullRevision() {
93 foreach ( parent::provideNewNullRevision() as $case ) {
94 yield $case;
95 }
96
97 yield [
98 Title::newFromText( 'UTPage_notAutoCreated' ),
99 [
100 'content' => [
101 'main' => new WikitextContent( 'Chicken' ),
102 'aux' => new WikitextContent( 'Omelet' ),
103 ],
104 ],
105 CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
106 ];
107 }
108
109 public function provideNewMutableRevisionFromArray() {
110 foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
111 yield $case;
112 }
113
114 yield 'Basic array, multiple roles' => [
115 [
116 'id' => 2,
117 'page' => 1,
118 'timestamp' => '20171017114835',
119 'user_text' => '111.0.1.2',
120 'user' => 0,
121 'minor_edit' => false,
122 'deleted' => 0,
123 'len' => 29,
124 'parent_id' => 1,
125 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
126 'comment' => 'Goat Comment!',
127 'content' => [
128 'main' => new WikitextContent( 'Söme Cöntent' ),
129 'aux' => new TextContent( 'Öther Cöntent' ),
130 ]
131 ]
132 ];
133 }
134
135 public function testGetQueryInfo_NoSlotDataJoin() {
136 $store = MediaWikiServices::getInstance()->getRevisionStore();
137 $queryInfo = $store->getQueryInfo();
138
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'] ) );
142 }
143
144 /**
145 * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
146 * @covers \MediaWiki\Revision\RevisionStore::insertSlotRowOn
147 * @covers \MediaWiki\Revision\RevisionStore::insertContentRowOn
148 */
149 public function testInsertRevisionOn_T202032() {
150 // This test only makes sense for MySQL
151 if ( $this->db->getType() !== 'mysql' ) {
152 $this->assertTrue( true );
153 return;
154 }
155
156 // NOTE: must be done before checking MAX(rev_id)
157 $page = $this->getTestPage();
158
159 $maxRevId = $this->db->selectField( 'revision', 'MAX(rev_id)' );
160
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,
166 'slot_role_id' => 1,
167 'slot_content_id' => 0,
168 'slot_origin' => 0
169 ], __METHOD__ );
170
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() );
177
178 $store = MediaWikiServices::getInstance()->getRevisionStore();
179 $return = $store->insertRevisionOn( $rev, $this->db );
180
181 $this->assertSame( $maxRevId + 2, $return->getId() );
182
183 // is the new revision correct?
184 $this->assertRevisionCompleteness( $return );
185 $this->assertRevisionRecordsEqual( $rev, $return );
186
187 // can we find it directly in the database?
188 $this->assertRevisionExistsInDatabase( $return );
189
190 // can we load it from the store?
191 $loaded = $store->getRevisionById( $return->getId() );
192 $this->assertRevisionCompleteness( $loaded );
193 $this->assertRevisionRecordsEqual( $return, $loaded );
194 }
195
196 /**
197 * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given
198 * revision.
199 *
200 * @return array
201 */
202 protected function getSlotRevisionConditions( $revId ) {
203 return [ 'slot_revision_id' => $revId ];
204 }
205
206 /**
207 * @covers \MediaWiki\Revision\RevisionStore::getContentBlobsForBatch
208 * @throws \MWException
209 */
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'];
217
218 $contentAddress = $rev1->getRevisionRecord()->getSlot( SlotRecord::MAIN )->getAddress();
219 $blobStatus = StatusValue::newGood( [] );
220 $blobStatus->warning( 'internalerror', 'oops!' );
221
222 $mockBlobStore = $this->getMock( BlobStore::class );
223 $mockBlobStore->method( 'getBlobBatch' )
224 ->willReturn( $blobStatus );
225
226 $revStore = MediaWikiServices::getInstance()
227 ->getRevisionStoreFactory()
228 ->getRevisionStore();
229 $wrappedRevStore = TestingAccessWrapper::newFromObject( $revStore );
230 $wrappedRevStore->blobStore = $mockBlobStore;
231
232 $result = $revStore->getContentBlobsForBatch( [ $rev1->getId() ] );
233 $this->assertTrue( $result->isOK() );
234 $this->assertFalse( $result->isGood() );
235 $this->assertNotEmpty( $result->getErrors() );
236
237 $records = $result->getValue();
238 $this->assertArrayHasKey( $rev1->getId(), $records );
239
240 $mainRow = $records[$rev1->getId()][SlotRecord::MAIN];
241 $this->assertNull( $mainRow->blob_data );
242 $this->assertSame( [
243 [
244 'type' => 'warning',
245 'message' => 'internalerror',
246 'params' => [
247 "oops!"
248 ]
249 ],
250 [
251 'type' => 'warning',
252 'message' => 'internalerror',
253 'params' => [
254 "Couldn't find blob data for rev " . $rev1->getId()
255 ]
256 ]
257 ], $result->getErrors() );
258 }
259
260 /**
261 * @covers \MediaWiki\Revision\RevisionStore::getContentBlobsForBatch
262 */
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'];
270
271 $contentAddress = $rev1->getRevisionRecord()->getSlot( SlotRecord::MAIN )->getAddress();
272 $mockBlobStore = $this->getMockBuilder( SqlBlobStore::class )
273 ->disableOriginalConstructor()
274 ->getMock();
275 $mockBlobStore
276 ->expects( $this->once() )
277 ->method( 'getBlobBatch' )
278 ->with( [ $contentAddress ], $this->anything() )
279 ->willReturn( StatusValue::newGood( [
280 $contentAddress => 'Content_From_Mock'
281 ] ) );
282 $mockBlobStore
283 ->expects( $this->never() )
284 ->method( 'getBlob' );
285
286 $revStore = MediaWikiServices::getInstance()
287 ->getRevisionStoreFactory()
288 ->getRevisionStore();
289 $wrappedRevStore = TestingAccessWrapper::newFromObject( $revStore );
290 $wrappedRevStore->blobStore = $mockBlobStore;
291
292 $result = $revStore->getContentBlobsForBatch(
293 [ $rev1->getId() ],
294 [ SlotRecord::MAIN ]
295 );
296 $this->assertTrue( $result->isGood() );
297 $this->assertSame( 'Content_From_Mock',
298 $result->getValue()[$rev1->getId()][SlotRecord::MAIN]->blob_data );
299 }
300
301 /**
302 * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
303 * @throws \MWException
304 */
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' ),
311 __METHOD__ . 'b',
312 0,
313 false,
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 ],
321 [
322 'slots' => [ SlotRecord::MAIN ],
323 'content' => true
324 ]
325 );
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( [ [
336 'type' => 'warning',
337 'message' => 'internalerror',
338 'params' => [
339 "Couldn't find slots for rev 100500"
340 ]
341 ] ], $result->getErrors() );
342 }
343
344 /**
345 * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
346 */
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'];
354
355 $contentAddress = $rev1->getRevisionRecord()->getSlot( SlotRecord::MAIN )->getAddress();
356 $mockBlobStore = $this->getMockBuilder( SqlBlobStore::class )
357 ->disableOriginalConstructor()
358 ->getMock();
359 $mockBlobStore
360 ->expects( $this->once() )
361 ->method( 'getBlobBatch' )
362 ->with( [ $contentAddress ], $this->anything() )
363 ->willReturn( StatusValue::newGood( [
364 $contentAddress => 'Content_From_Mock'
365 ] ) );
366 $mockBlobStore
367 ->expects( $this->never() )
368 ->method( 'getBlob' );
369
370 $revStore = MediaWikiServices::getInstance()
371 ->getRevisionStoreFactory()
372 ->getRevisionStore();
373 $wrappedRevStore = TestingAccessWrapper::newFromObject( $revStore );
374 $wrappedRevStore->blobStore = $mockBlobStore;
375
376 $result = $revStore->newRevisionsFromBatch(
377 [ $this->revisionToRow( $rev1 ) ],
378 [
379 'slots' => [ SlotRecord::MAIN ],
380 'content' => true
381 ]
382 );
383 $this->assertTrue( $result->isGood() );
384 $this->assertSame( 'Content_From_Mock',
385 ContentHandler::getContentText( $result->getValue()[$rev1->getId()]
386 ->getContent( SlotRecord::MAIN ) ) );
387 }
388 }