Merge "RevisionStoreDbTestBase, remove redundant needsDB override"
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / RevisionRecordTests.php
1 <?php
2
3 // phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClassTrait
4
5 namespace MediaWiki\Tests\Storage;
6
7 use CommentStoreComment;
8 use LogicException;
9 use MediaWiki\Storage\RevisionRecord;
10 use MediaWiki\Storage\RevisionSlots;
11 use MediaWiki\Storage\RevisionStoreRecord;
12 use MediaWiki\Storage\SlotRecord;
13 use MediaWiki\Storage\SuppressedDataException;
14 use MediaWiki\User\UserIdentityValue;
15 use TextContent;
16 use Title;
17
18 /**
19 * @covers \MediaWiki\Storage\RevisionRecord
20 *
21 * @note Expects to be used in classes that extend MediaWikiTestCase.
22 */
23 trait RevisionRecordTests {
24
25 /**
26 * @param array $rowOverrides
27 *
28 * @return RevisionRecord
29 */
30 protected abstract function newRevision( array $rowOverrides = [] );
31
32 private function provideAudienceCheckData( $field ) {
33 yield 'field accessible for oversighter (ALL)' => [
34 RevisionRecord::SUPPRESSED_ALL,
35 [ 'oversight' ],
36 true,
37 false
38 ];
39
40 yield 'field accessible for oversighter' => [
41 RevisionRecord::DELETED_RESTRICTED | $field,
42 [ 'oversight' ],
43 true,
44 false
45 ];
46
47 yield 'field not accessible for sysops (ALL)' => [
48 RevisionRecord::SUPPRESSED_ALL,
49 [ 'sysop' ],
50 false,
51 false
52 ];
53
54 yield 'field not accessible for sysops' => [
55 RevisionRecord::DELETED_RESTRICTED | $field,
56 [ 'sysop' ],
57 false,
58 false
59 ];
60
61 yield 'field accessible for sysops' => [
62 $field,
63 [ 'sysop' ],
64 true,
65 false
66 ];
67
68 yield 'field suppressed for logged in users' => [
69 $field,
70 [ 'user' ],
71 false,
72 false
73 ];
74
75 yield 'unrelated field suppressed' => [
76 $field === RevisionRecord::DELETED_COMMENT
77 ? RevisionRecord::DELETED_USER
78 : RevisionRecord::DELETED_COMMENT,
79 [ 'user' ],
80 true,
81 true
82 ];
83
84 yield 'nothing suppressed' => [
85 0,
86 [ 'user' ],
87 true,
88 true
89 ];
90 }
91
92 public function testSerialization_fails() {
93 $this->setExpectedException( LogicException::class );
94 $rev = $this->newRevision();
95 serialize( $rev );
96 }
97
98 public function provideGetComment_audience() {
99 return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
100 }
101
102 private function forceStandardPermissions() {
103 $this->setMwGlobals(
104 'wgGroupPermissions',
105 [
106 'user' => [
107 'viewsuppressed' => false,
108 'suppressrevision' => false,
109 'deletedtext' => false,
110 'deletedhistory' => false,
111 ],
112 'sysop' => [
113 'viewsuppressed' => false,
114 'suppressrevision' => false,
115 'deletedtext' => true,
116 'deletedhistory' => true,
117 ],
118 'oversight' => [
119 'deletedtext' => true,
120 'deletedhistory' => true,
121 'viewsuppressed' => true,
122 'suppressrevision' => true,
123 ],
124 ]
125 );
126 }
127
128 /**
129 * @dataProvider provideGetComment_audience
130 */
131 public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
132 $this->forceStandardPermissions();
133
134 $user = $this->getTestUser( $groups )->getUser();
135 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
136
137 $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
138
139 $this->assertSame(
140 $publicCan,
141 $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
142 'public can'
143 );
144 $this->assertSame(
145 $userCan,
146 $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
147 'user can'
148 );
149 }
150
151 public function provideGetUser_audience() {
152 return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
153 }
154
155 /**
156 * @dataProvider provideGetUser_audience
157 */
158 public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
159 $this->forceStandardPermissions();
160
161 $user = $this->getTestUser( $groups )->getUser();
162 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
163
164 $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
165
166 $this->assertSame(
167 $publicCan,
168 $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
169 'public can'
170 );
171 $this->assertSame(
172 $userCan,
173 $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
174 'user can'
175 );
176 }
177
178 public function provideGetSlot_audience() {
179 return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
180 }
181
182 /**
183 * @dataProvider provideGetSlot_audience
184 */
185 public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
186 $this->forceStandardPermissions();
187
188 $user = $this->getTestUser( $groups )->getUser();
189 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
190
191 // NOTE: slot meta-data is never suppressed, just the content is!
192 $this->assertTrue( $rev->hasSlot( 'main' ), 'hasSlot is never suppressed' );
193 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw meta' );
194 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public meta' );
195
196 $this->assertNotNull(
197 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ),
198 'user can'
199 );
200
201 try {
202 $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC )->getContent();
203 $exception = null;
204 } catch ( SuppressedDataException $ex ) {
205 $exception = $ex;
206 }
207
208 $this->assertSame(
209 $publicCan,
210 $exception === null,
211 'public can'
212 );
213
214 try {
215 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user )->getContent();
216 $exception = null;
217 } catch ( SuppressedDataException $ex ) {
218 $exception = $ex;
219 }
220
221 $this->assertSame(
222 $userCan,
223 $exception === null,
224 'user can'
225 );
226 }
227
228 /**
229 * @dataProvider provideGetSlot_audience
230 */
231 public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
232 $this->forceStandardPermissions();
233
234 $user = $this->getTestUser( $groups )->getUser();
235 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
236
237 $this->assertNotNull( $rev->getContent( 'main', RevisionRecord::RAW ), 'raw can' );
238
239 $this->assertSame(
240 $publicCan,
241 $rev->getContent( 'main', RevisionRecord::FOR_PUBLIC ) !== null,
242 'public can'
243 );
244 $this->assertSame(
245 $userCan,
246 $rev->getContent( 'main', RevisionRecord::FOR_THIS_USER, $user ) !== null,
247 'user can'
248 );
249 }
250
251 public function testGetSlot() {
252 $rev = $this->newRevision();
253
254 $slot = $rev->getSlot( 'main' );
255 $this->assertNotNull( $slot, 'getSlot()' );
256 $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
257 }
258
259 public function testHasSlot() {
260 $rev = $this->newRevision();
261
262 $this->assertTrue( $rev->hasSlot( 'main' ) );
263 $this->assertFalse( $rev->hasSlot( 'xyz' ) );
264 }
265
266 public function testGetContent() {
267 $rev = $this->newRevision();
268
269 $content = $rev->getSlot( 'main' );
270 $this->assertNotNull( $content, 'getContent()' );
271 $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
272 }
273
274 public function provideUserCanBitfield() {
275 yield [ 0, 0, [], null, true ];
276 // Bitfields match, user has no permissions
277 yield [
278 RevisionRecord::DELETED_TEXT,
279 RevisionRecord::DELETED_TEXT,
280 [],
281 null,
282 false
283 ];
284 yield [
285 RevisionRecord::DELETED_COMMENT,
286 RevisionRecord::DELETED_COMMENT,
287 [],
288 null,
289 false,
290 ];
291 yield [
292 RevisionRecord::DELETED_USER,
293 RevisionRecord::DELETED_USER,
294 [],
295 null,
296 false
297 ];
298 yield [
299 RevisionRecord::DELETED_RESTRICTED,
300 RevisionRecord::DELETED_RESTRICTED,
301 [],
302 null,
303 false,
304 ];
305 // Bitfields match, user (admin) does have permissions
306 yield [
307 RevisionRecord::DELETED_TEXT,
308 RevisionRecord::DELETED_TEXT,
309 [ 'sysop' ],
310 null,
311 true,
312 ];
313 yield [
314 RevisionRecord::DELETED_COMMENT,
315 RevisionRecord::DELETED_COMMENT,
316 [ 'sysop' ],
317 null,
318 true,
319 ];
320 yield [
321 RevisionRecord::DELETED_USER,
322 RevisionRecord::DELETED_USER,
323 [ 'sysop' ],
324 null,
325 true,
326 ];
327 // Bitfields match, user (admin) does not have permissions
328 yield [
329 RevisionRecord::DELETED_RESTRICTED,
330 RevisionRecord::DELETED_RESTRICTED,
331 [ 'sysop' ],
332 null,
333 false,
334 ];
335 // Bitfields match, user (oversight) does have permissions
336 yield [
337 RevisionRecord::DELETED_RESTRICTED,
338 RevisionRecord::DELETED_RESTRICTED,
339 [ 'oversight' ],
340 null,
341 true,
342 ];
343 // Check permissions using the title
344 yield [
345 RevisionRecord::DELETED_TEXT,
346 RevisionRecord::DELETED_TEXT,
347 [ 'sysop' ],
348 __METHOD__,
349 true,
350 ];
351 yield [
352 RevisionRecord::DELETED_TEXT,
353 RevisionRecord::DELETED_TEXT,
354 [],
355 __METHOD__,
356 false,
357 ];
358 }
359
360 /**
361 * @dataProvider provideUserCanBitfield
362 * @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
363 */
364 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
365 if ( is_string( $title ) ) {
366 // NOTE: Data providers cannot instantiate Title objects! See T202641.
367 $title = Title::newFromText( $title );
368 }
369
370 $this->forceStandardPermissions();
371
372 $user = $this->getTestUser( $userGroups )->getUser();
373
374 $this->assertSame(
375 $expected,
376 RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
377 );
378 }
379
380 public function provideHasSameContent() {
381 // Create some slots with content
382 $mainA = SlotRecord::newUnsaved( 'main', new TextContent( 'A' ) );
383 $mainB = SlotRecord::newUnsaved( 'main', new TextContent( 'B' ) );
384 $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
385 $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
386
387 $initialRecordSpec = [ [ $mainA ], 12 ];
388
389 return [
390 'same record object' => [
391 true,
392 $initialRecordSpec,
393 $initialRecordSpec,
394 ],
395 'same record content, different object' => [
396 true,
397 [ [ $mainA ], 12 ],
398 [ [ $mainA ], 13 ],
399 ],
400 'same record content, aux slot, different object' => [
401 true,
402 [ [ $auxA ], 12 ],
403 [ [ $auxB ], 13 ],
404 ],
405 'different content' => [
406 false,
407 [ [ $mainA ], 12 ],
408 [ [ $mainB ], 13 ],
409 ],
410 'different content and number of slots' => [
411 false,
412 [ [ $mainA ], 12 ],
413 [ [ $mainA, $mainB ], 13 ],
414 ],
415 ];
416 }
417
418 /**
419 * @note Do not call directly from a data provider! Data providers cannot instantiate
420 * Title objects! See T202641.
421 *
422 * @param SlotRecord[] $slots
423 * @param int $revId
424 * @return RevisionStoreRecord
425 */
426 private function makeHasSameContentTestRecord( array $slots, $revId ) {
427 $title = Title::newFromText( 'provideHasSameContent' );
428 $title->resetArticleID( 19 );
429 $slots = new RevisionSlots( $slots );
430
431 return new RevisionStoreRecord(
432 $title,
433 new UserIdentityValue( 11, __METHOD__, 0 ),
434 CommentStoreComment::newUnsavedComment( __METHOD__ ),
435 (object)[
436 'rev_id' => strval( $revId ),
437 'rev_page' => strval( $title->getArticleID() ),
438 'rev_timestamp' => '20200101000000',
439 'rev_deleted' => 0,
440 'rev_minor_edit' => 0,
441 'rev_parent_id' => '5',
442 'rev_len' => $slots->computeSize(),
443 'rev_sha1' => $slots->computeSha1(),
444 'page_latest' => '18',
445 ],
446 $slots
447 );
448 }
449
450 /**
451 * @dataProvider provideHasSameContent
452 * @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
453 * @group Database
454 */
455 public function testHasSameContent(
456 $expected,
457 $recordSpec1,
458 $recordSpec2
459 ) {
460 $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
461 $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
462
463 $this->assertSame(
464 $expected,
465 $record1->hasSameContent( $record2 )
466 );
467 }
468
469 public function provideIsDeleted() {
470 yield 'no deletion' => [
471 0,
472 [
473 RevisionRecord::DELETED_TEXT => false,
474 RevisionRecord::DELETED_COMMENT => false,
475 RevisionRecord::DELETED_USER => false,
476 RevisionRecord::DELETED_RESTRICTED => false,
477 ]
478 ];
479 yield 'text deleted' => [
480 RevisionRecord::DELETED_TEXT,
481 [
482 RevisionRecord::DELETED_TEXT => true,
483 RevisionRecord::DELETED_COMMENT => false,
484 RevisionRecord::DELETED_USER => false,
485 RevisionRecord::DELETED_RESTRICTED => false,
486 ]
487 ];
488 yield 'text and comment deleted' => [
489 RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
490 [
491 RevisionRecord::DELETED_TEXT => true,
492 RevisionRecord::DELETED_COMMENT => true,
493 RevisionRecord::DELETED_USER => false,
494 RevisionRecord::DELETED_RESTRICTED => false,
495 ]
496 ];
497 yield 'all 4 deleted' => [
498 RevisionRecord::DELETED_TEXT +
499 RevisionRecord::DELETED_COMMENT +
500 RevisionRecord::DELETED_RESTRICTED +
501 RevisionRecord::DELETED_USER,
502 [
503 RevisionRecord::DELETED_TEXT => true,
504 RevisionRecord::DELETED_COMMENT => true,
505 RevisionRecord::DELETED_USER => true,
506 RevisionRecord::DELETED_RESTRICTED => true,
507 ]
508 ];
509 }
510
511 /**
512 * @dataProvider provideIsDeleted
513 * @covers \MediaWiki\Storage\RevisionRecord::isDeleted
514 */
515 public function testIsDeleted( $revDeleted, $assertionMap ) {
516 $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
517 foreach ( $assertionMap as $deletionLevel => $expected ) {
518 $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
519 }
520 }
521
522 public function testIsReadyForInsertion() {
523 $rev = $this->newRevision();
524 $this->assertTrue( $rev->isReadyForInsertion() );
525 }
526
527 }