[MCR] Break Revision into RevisionRecord and RevisionStore
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / RevisionStoreRecordTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use CommentStoreComment;
6 use InvalidArgumentException;
7 use LogicException;
8 use MediaWiki\Storage\RevisionRecord;
9 use MediaWiki\Storage\RevisionSlots;
10 use MediaWiki\Storage\RevisionStoreRecord;
11 use MediaWiki\Storage\SlotRecord;
12 use MediaWiki\Storage\SuppressedDataException;
13 use MediaWiki\User\UserIdentity;
14 use MediaWiki\User\UserIdentityValue;
15 use MediaWikiTestCase;
16 use TextContent;
17 use Title;
18
19 /**
20 * @covers MediaWiki\Storage\RevisionStoreRecord
21 */
22 class RevisionStoreRecordTest extends MediaWikiTestCase {
23
24 /**
25 * @param array $overrides
26 * @return RevisionStoreRecord
27 */
28 public function newRevision( array $overrides = [] ) {
29 $title = Title::newFromText( 'Dummy' );
30 $title->resetArticleID( 17 );
31
32 $user = new UserIdentityValue( 11, 'Tester' );
33 $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
34
35 $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
36 $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
37 $slots = new RevisionSlots( [ $main, $aux ] );
38
39 $row = [
40 'rev_id' => '7',
41 'rev_page' => strval( $title->getArticleID() ),
42 'rev_timestamp' => '20200101000000',
43 'rev_deleted' => 0,
44 'rev_minor_edit' => 0,
45 'rev_parent_id' => '5',
46 'rev_len' => $slots->computeSize(),
47 'rev_sha1' => $slots->computeSha1(),
48 'page_latest' => '18',
49 ];
50
51 $row = array_merge( $row, $overrides );
52
53 return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
54 }
55
56 public function provideConstructor() {
57 $title = Title::newFromText( 'Dummy' );
58 $title->resetArticleID( 17 );
59
60 $user = new UserIdentityValue( 11, 'Tester' );
61 $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
62
63 $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
64 $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
65 $slots = new RevisionSlots( [ $main, $aux ] );
66
67 $protoRow = [
68 'rev_id' => '7',
69 'rev_page' => strval( $title->getArticleID() ),
70 'rev_timestamp' => '20200101000000',
71 'rev_deleted' => 0,
72 'rev_minor_edit' => 0,
73 'rev_parent_id' => '5',
74 'rev_len' => $slots->computeSize(),
75 'rev_sha1' => $slots->computeSha1(),
76 'page_latest' => '18',
77 ];
78
79 $row = $protoRow;
80 yield 'all info' => [
81 $title,
82 $user,
83 $comment,
84 (object)$row,
85 $slots,
86 'acmewiki'
87 ];
88
89 $row = $protoRow;
90 $row['rev_minor_edit'] = '1';
91 $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER );
92
93 yield 'minor deleted' => [
94 $title,
95 $user,
96 $comment,
97 (object)$row,
98 $slots
99 ];
100
101 $row = $protoRow;
102 $row['page_latest'] = $row['rev_id'];
103
104 yield 'latest' => [
105 $title,
106 $user,
107 $comment,
108 (object)$row,
109 $slots
110 ];
111
112 $row = $protoRow;
113 unset( $row['rev_parent'] );
114
115 yield 'no parent' => [
116 $title,
117 $user,
118 $comment,
119 (object)$row,
120 $slots
121 ];
122
123 $row = $protoRow;
124 unset( $row['rev_len'] );
125 unset( $row['rev_sha1'] );
126
127 yield 'no length, no hash' => [
128 $title,
129 $user,
130 $comment,
131 (object)$row,
132 $slots
133 ];
134
135 $row = $protoRow;
136 yield 'no length, no hash' => [
137 Title::newFromText( 'DummyDoesNotExist' ),
138 $user,
139 $comment,
140 (object)$row,
141 $slots
142 ];
143 }
144
145 /**
146 * @dataProvider provideConstructor
147 *
148 * @param Title $title
149 * @param UserIdentity $user
150 * @param CommentStoreComment $comment
151 * @param object $row
152 * @param RevisionSlots $slots
153 * @param bool $wikiId
154 */
155 public function testConstructorAndGetters(
156 Title $title,
157 UserIdentity $user,
158 CommentStoreComment $comment,
159 $row,
160 RevisionSlots $slots,
161 $wikiId = false
162 ) {
163 $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
164
165 $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
166 $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
167 $this->assertSame( $comment, $rec->getComment(), 'getComment' );
168
169 $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
170 $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
171
172 $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' );
173 $this->assertSame( (int)$row->rev_page, $rec->getPageId(), 'getId' );
174 $this->assertSame( $row->rev_timestamp, $rec->getTimestamp(), 'getTimestamp' );
175 $this->assertSame( (int)$row->rev_deleted, $rec->getVisibility(), 'getVisibility' );
176 $this->assertSame( (bool)$row->rev_minor_edit, $rec->isMinor(), 'getIsMinor' );
177
178 if ( isset( $row->rev_parent_id ) ) {
179 $this->assertSame( (int)$row->rev_parent_id, $rec->getParentId(), 'getParentId' );
180 } else {
181 $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
182 }
183
184 if ( isset( $row->rev_len ) ) {
185 $this->assertSame( (int)$row->rev_len, $rec->getSize(), 'getSize' );
186 } else {
187 $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
188 }
189
190 if ( isset( $row->rev_sha1 ) ) {
191 $this->assertSame( $row->rev_sha1, $rec->getSha1(), 'getSha1' );
192 } else {
193 $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
194 }
195
196 if ( isset( $row->page_latest ) ) {
197 $this->assertSame(
198 (int)$row->rev_id === (int)$row->page_latest,
199 $rec->isCurrent(),
200 'isCurrent'
201 );
202 } else {
203 $this->assertSame(
204 false,
205 $rec->isCurrent(),
206 'isCurrent'
207 );
208 }
209 }
210
211 public function provideConstructorFailure() {
212 $title = Title::newFromText( 'Dummy' );
213 $title->resetArticleID( 17 );
214
215 $user = new UserIdentityValue( 11, 'Tester' );
216
217 $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
218
219 $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
220 $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
221 $slots = new RevisionSlots( [ $main, $aux ] );
222
223 $protoRow = [
224 'rev_id' => '7',
225 'rev_page' => strval( $title->getArticleID() ),
226 'rev_timestamp' => '20200101000000',
227 'rev_deleted' => 0,
228 'rev_minor_edit' => 0,
229 'rev_parent_id' => '5',
230 'rev_len' => $slots->computeSize(),
231 'rev_sha1' => $slots->computeSha1(),
232 'page_latest' => '18',
233 ];
234
235 yield 'not a row' => [
236 $title,
237 $user,
238 $comment,
239 'not a row',
240 $slots,
241 'acmewiki'
242 ];
243
244 $row = $protoRow;
245 $row['rev_timestamp'] = 'kittens';
246
247 yield 'bad timestamp' => [
248 $title,
249 $user,
250 $comment,
251 (object)$row,
252 $slots
253 ];
254
255 $row = $protoRow;
256 $row['rev_page'] = 99;
257
258 yield 'page ID mismatch' => [
259 $title,
260 $user,
261 $comment,
262 (object)$row,
263 $slots
264 ];
265
266 $row = $protoRow;
267
268 yield 'bad wiki' => [
269 $title,
270 $user,
271 $comment,
272 (object)$row,
273 $slots,
274 12345
275 ];
276 }
277
278 /**
279 * @dataProvider provideConstructorFailure
280 *
281 * @param Title $title
282 * @param UserIdentity $user
283 * @param CommentStoreComment $comment
284 * @param object $row
285 * @param RevisionSlots $slots
286 * @param bool $wikiId
287 */
288 public function testConstructorFailure(
289 Title $title,
290 UserIdentity $user,
291 CommentStoreComment $comment,
292 $row,
293 RevisionSlots $slots,
294 $wikiId = false
295 ) {
296 $this->setExpectedException( InvalidArgumentException::class );
297 new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
298 }
299
300 private function provideAudienceCheckData( $field ) {
301 yield 'field accessible for oversighter (ALL)' => [
302 Revisionrecord::SUPPRESSED_ALL,
303 [ 'oversight' ],
304 true,
305 false
306 ];
307
308 yield 'field accessible for oversighter' => [
309 Revisionrecord::DELETED_RESTRICTED | $field,
310 [ 'oversight' ],
311 true,
312 false
313 ];
314
315 yield 'field not accessible for sysops (ALL)' => [
316 Revisionrecord::SUPPRESSED_ALL,
317 [ 'sysop' ],
318 false,
319 false
320 ];
321
322 yield 'field not accessible for sysops' => [
323 Revisionrecord::DELETED_RESTRICTED | $field,
324 [ 'sysop' ],
325 false,
326 false
327 ];
328
329 yield 'field accessible for sysops' => [
330 $field,
331 [ 'sysop' ],
332 true,
333 false
334 ];
335
336 yield 'field suppressed for logged in users' => [
337 $field,
338 [ 'user' ],
339 false,
340 false
341 ];
342
343 yield 'unrelated field suppressed' => [
344 $field === Revisionrecord::DELETED_COMMENT
345 ? Revisionrecord::DELETED_USER
346 : Revisionrecord::DELETED_COMMENT,
347 [ 'user' ],
348 true,
349 true
350 ];
351
352 yield 'nothing suppressed' => [
353 0,
354 [ 'user' ],
355 true,
356 true
357 ];
358 }
359
360 public function testSerialization_fails() {
361 $this->setExpectedException( LogicException::class );
362 $rev = $this->newRevision();
363 serialize( $rev );
364 }
365
366 public function provideGetComment_audience() {
367 return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
368 }
369
370 private function forceStandardPermissions() {
371 $this->setMwGlobals(
372 'wgGroupPermissions',
373 [
374 'user' => [
375 'viewsuppressed' => false,
376 'suppressrevision' => false,
377 'deletedtext' => false,
378 'deletedhistory' => false,
379 ],
380 'sysop' => [
381 'viewsuppressed' => false,
382 'suppressrevision' => false,
383 'deletedtext' => true,
384 'deletedhistory' => true,
385 ],
386 'oversight' => [
387 'deletedtext' => true,
388 'deletedhistory' => true,
389 'viewsuppressed' => true,
390 'suppressrevision' => true,
391 ],
392 ]
393 );
394 }
395
396 /**
397 * @dataProvider provideGetComment_audience
398 */
399 public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
400 $this->forceStandardPermissions();
401
402 $user = $this->getTestUser( $groups )->getUser();
403 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
404
405 $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
406
407 $this->assertSame(
408 $publicCan,
409 $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
410 'public can'
411 );
412 $this->assertSame(
413 $userCan,
414 $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
415 'user can'
416 );
417 }
418
419 public function provideGetUser_audience() {
420 return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
421 }
422
423 /**
424 * @dataProvider provideGetUser_audience
425 */
426 public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
427 $this->forceStandardPermissions();
428
429 $user = $this->getTestUser( $groups )->getUser();
430 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
431
432 $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
433
434 $this->assertSame(
435 $publicCan,
436 $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
437 'public can'
438 );
439 $this->assertSame(
440 $userCan,
441 $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
442 'user can'
443 );
444 }
445
446 public function provideGetSlot_audience() {
447 return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
448 }
449
450 /**
451 * @dataProvider provideGetSlot_audience
452 */
453 public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
454 $this->forceStandardPermissions();
455
456 $user = $this->getTestUser( $groups )->getUser();
457 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
458
459 // NOTE: slot meta-data is never suppressed, just the content is!
460 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw can' );
461 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public can' );
462
463 $this->assertNotNull(
464 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ),
465 'user can'
466 );
467
468 try {
469 $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC )->getContent();
470 $exception = null;
471 } catch ( SuppressedDataException $ex ) {
472 $exception = $ex;
473 }
474
475 $this->assertSame(
476 $publicCan,
477 $exception === null,
478 'public can'
479 );
480
481 try {
482 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user )->getContent();
483 $exception = null;
484 } catch ( SuppressedDataException $ex ) {
485 $exception = $ex;
486 }
487
488 $this->assertSame(
489 $userCan,
490 $exception === null,
491 'user can'
492 );
493 }
494
495 public function provideGetSlot_audience_latest() {
496 return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
497 }
498
499 /**
500 * @dataProvider provideGetSlot_audience_latest
501 */
502 public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
503 $this->forceStandardPermissions();
504
505 $user = $this->getTestUser( $groups )->getUser();
506 $rev = $this->newRevision(
507 [
508 'rev_deleted' => $visibility,
509 'rev_id' => 11,
510 'page_latest' => 11, // revision is current
511 ]
512 );
513
514 // sanity check
515 $this->assertTrue( $rev->isCurrent(), 'isCurrent()' );
516
517 // NOTE: slot meta-data is never suppressed, just the content is!
518 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw can' );
519 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public can' );
520
521 $this->assertNotNull(
522 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ),
523 'user can'
524 );
525
526 // NOTE: the content of the current revision is never suppressed!
527 // Check that getContent() doesn't throw SuppressedDataException
528 $rev->getSlot( 'main', RevisionRecord::RAW )->getContent();
529 $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC )->getContent();
530 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user )->getContent();
531 }
532
533 /**
534 * @dataProvider provideGetSlot_audience
535 */
536 public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
537 $this->forceStandardPermissions();
538
539 $user = $this->getTestUser( $groups )->getUser();
540 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
541
542 $this->assertNotNull( $rev->getContent( 'main', RevisionRecord::RAW ), 'raw can' );
543
544 $this->assertSame(
545 $publicCan,
546 $rev->getContent( 'main', RevisionRecord::FOR_PUBLIC ) !== null,
547 'public can'
548 );
549 $this->assertSame(
550 $userCan,
551 $rev->getContent( 'main', RevisionRecord::FOR_THIS_USER, $user ) !== null,
552 'user can'
553 );
554 }
555
556 public function testGetSlot() {
557 $rev = $this->newRevision();
558
559 $slot = $rev->getSlot( 'main' );
560 $this->assertNotNull( $slot, 'getSlot()' );
561 $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
562 }
563
564 public function testGetContent() {
565 $rev = $this->newRevision();
566
567 $content = $rev->getSlot( 'main' );
568 $this->assertNotNull( $content, 'getContent()' );
569 $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
570 }
571
572 public function provideUserCanBitfield() {
573 yield [ 0, 0, [], null, true ];
574 // Bitfields match, user has no permissions
575 yield [
576 RevisionRecord::DELETED_TEXT,
577 RevisionRecord::DELETED_TEXT,
578 [],
579 null,
580 false
581 ];
582 yield [
583 RevisionRecord::DELETED_COMMENT,
584 RevisionRecord::DELETED_COMMENT,
585 [],
586 null,
587 false,
588 ];
589 yield [
590 RevisionRecord::DELETED_USER,
591 RevisionRecord::DELETED_USER,
592 [],
593 null,
594 false
595 ];
596 yield [
597 RevisionRecord::DELETED_RESTRICTED,
598 RevisionRecord::DELETED_RESTRICTED,
599 [],
600 null,
601 false,
602 ];
603 // Bitfields match, user (admin) does have permissions
604 yield [
605 RevisionRecord::DELETED_TEXT,
606 RevisionRecord::DELETED_TEXT,
607 [ 'sysop' ],
608 null,
609 true,
610 ];
611 yield [
612 RevisionRecord::DELETED_COMMENT,
613 RevisionRecord::DELETED_COMMENT,
614 [ 'sysop' ],
615 null,
616 true,
617 ];
618 yield [
619 RevisionRecord::DELETED_USER,
620 RevisionRecord::DELETED_USER,
621 [ 'sysop' ],
622 null,
623 true,
624 ];
625 // Bitfields match, user (admin) does not have permissions
626 yield [
627 RevisionRecord::DELETED_RESTRICTED,
628 RevisionRecord::DELETED_RESTRICTED,
629 [ 'sysop' ],
630 null,
631 false,
632 ];
633 // Bitfields match, user (oversight) does have permissions
634 yield [
635 RevisionRecord::DELETED_RESTRICTED,
636 RevisionRecord::DELETED_RESTRICTED,
637 [ 'oversight' ],
638 null,
639 true,
640 ];
641 // Check permissions using the title
642 yield [
643 RevisionRecord::DELETED_TEXT,
644 RevisionRecord::DELETED_TEXT,
645 [ 'sysop' ],
646 Title::newFromText( __METHOD__ ),
647 true,
648 ];
649 yield [
650 RevisionRecord::DELETED_TEXT,
651 RevisionRecord::DELETED_TEXT,
652 [],
653 Title::newFromText( __METHOD__ ),
654 false,
655 ];
656 }
657
658 /**
659 * @dataProvider provideUserCanBitfield
660 * @covers RevisionRecord::userCanBitfield
661 */
662 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
663 $this->forceStandardPermissions();
664
665 $user = $this->getTestUser( $userGroups )->getUser();
666
667 $this->assertSame(
668 $expected,
669 RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
670 );
671 }
672
673 public function testHasSameContent() {
674 // TBD
675 }
676
677 public function testIsDeleted() {
678 // TBD
679 }
680
681 public function testUserCan() {
682 // TBD
683 }
684
685 }