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