Merge "Exclude redirects from Special:Fewestrevisions"
[lhc/web/wiklou.git] / tests / phpunit / unit / includes / Revision / SlotRecordTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use InvalidArgumentException;
6 use LogicException;
7 use MediaWiki\Revision\IncompleteRevisionException;
8 use MediaWiki\Revision\SlotRecord;
9 use MediaWiki\Revision\SuppressedDataException;
10 use MediaWikiUnitTestCase;
11 use WikitextContent;
12
13 /**
14 * @covers \MediaWiki\Revision\SlotRecord
15 */
16 class SlotRecordTest extends MediaWikiUnitTestCase {
17
18 private function makeRow( $data = [] ) {
19 $data = $data + [
20 'slot_id' => 1234,
21 'slot_content_id' => 33,
22 'content_size' => '5',
23 'content_sha1' => 'someHash',
24 'content_address' => 'tt:456',
25 'model_name' => CONTENT_MODEL_WIKITEXT,
26 'format_name' => CONTENT_FORMAT_WIKITEXT,
27 'slot_revision_id' => '2',
28 'slot_origin' => '1',
29 'role_name' => 'myRole',
30 ];
31 return (object)$data;
32 }
33
34 public function testCompleteConstruction() {
35 $row = $this->makeRow();
36 $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
37
38 $this->assertTrue( $record->hasAddress() );
39 $this->assertTrue( $record->hasContentId() );
40 $this->assertTrue( $record->hasRevision() );
41 $this->assertTrue( $record->isInherited() );
42 $this->assertSame( 'A', $record->getContent()->getText() );
43 $this->assertSame( 5, $record->getSize() );
44 $this->assertSame( 'someHash', $record->getSha1() );
45 $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
46 $this->assertSame( 2, $record->getRevision() );
47 $this->assertSame( 1, $record->getOrigin() );
48 $this->assertSame( 'tt:456', $record->getAddress() );
49 $this->assertSame( 33, $record->getContentId() );
50 $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
51 $this->assertSame( 'myRole', $record->getRole() );
52 }
53
54 public function testConstructionDeferred() {
55 $row = $this->makeRow( [
56 'content_size' => null, // to be computed
57 'content_sha1' => null, // to be computed
58 'format_name' => function () {
59 return CONTENT_FORMAT_WIKITEXT;
60 },
61 'slot_revision_id' => '2',
62 'slot_origin' => '2',
63 'slot_content_id' => function () {
64 return null;
65 },
66 ] );
67
68 $content = function () {
69 return new WikitextContent( 'A' );
70 };
71
72 $record = new SlotRecord( $row, $content );
73
74 $this->assertTrue( $record->hasAddress() );
75 $this->assertTrue( $record->hasRevision() );
76 $this->assertFalse( $record->hasContentId() );
77 $this->assertFalse( $record->isInherited() );
78 $this->assertSame( 'A', $record->getContent()->getText() );
79 $this->assertSame( 1, $record->getSize() );
80 $this->assertNotEmpty( $record->getSha1() );
81 $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
82 $this->assertSame( 2, $record->getRevision() );
83 $this->assertSame( 2, $record->getRevision() );
84 $this->assertSame( 'tt:456', $record->getAddress() );
85 $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
86 $this->assertSame( 'myRole', $record->getRole() );
87 }
88
89 public function testNewUnsaved() {
90 $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
91
92 $this->assertFalse( $record->hasAddress() );
93 $this->assertFalse( $record->hasContentId() );
94 $this->assertFalse( $record->hasRevision() );
95 $this->assertFalse( $record->isInherited() );
96 $this->assertFalse( $record->hasOrigin() );
97 $this->assertSame( 'A', $record->getContent()->getText() );
98 $this->assertSame( 1, $record->getSize() );
99 $this->assertNotEmpty( $record->getSha1() );
100 $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
101 $this->assertSame( 'myRole', $record->getRole() );
102 }
103
104 public function provideInvalidConstruction() {
105 yield 'both null' => [ null, null ];
106 yield 'null row' => [ null, new WikitextContent( 'A' ) ];
107 yield 'array row' => [ [], new WikitextContent( 'A' ) ];
108 yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
109 yield 'null content' => [ (object)[], null ];
110 }
111
112 /**
113 * @dataProvider provideInvalidConstruction
114 */
115 public function testInvalidConstruction( $row, $content ) {
116 $this->setExpectedException( InvalidArgumentException::class );
117 new SlotRecord( $row, $content );
118 }
119
120 public function testGetContentId_fails() {
121 $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
122 $this->setExpectedException( IncompleteRevisionException::class );
123
124 $record->getContentId();
125 }
126
127 public function testGetAddress_fails() {
128 $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
129 $this->setExpectedException( IncompleteRevisionException::class );
130
131 $record->getAddress();
132 }
133
134 public function provideIncomplete() {
135 $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
136 yield 'unsaved' => [ $unsaved ];
137
138 $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
139 $inherited = SlotRecord::newInherited( $parent );
140 yield 'inherited' => [ $inherited ];
141 }
142
143 /**
144 * @dataProvider provideIncomplete
145 */
146 public function testGetRevision_fails( SlotRecord $record ) {
147 $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
148 $this->setExpectedException( IncompleteRevisionException::class );
149
150 $record->getRevision();
151 }
152
153 /**
154 * @dataProvider provideIncomplete
155 */
156 public function testGetOrigin_fails( SlotRecord $record ) {
157 $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
158 $this->setExpectedException( IncompleteRevisionException::class );
159
160 $record->getOrigin();
161 }
162
163 public function provideHashStability() {
164 yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
165 yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
166 }
167
168 /**
169 * @dataProvider provideHashStability
170 */
171 public function testHashStability( $text, $hash ) {
172 // Changing the output of the hash function will break things horribly!
173
174 $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
175
176 $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
177 $this->assertSame( $hash, $record->getSha1() );
178 }
179
180 public function testHashComputed() {
181 $row = $this->makeRow();
182 $row->content_sha1 = '';
183
184 $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
185 $this->assertNotEmpty( $rec->getSha1() );
186 }
187
188 public function testNewWithSuppressedContent() {
189 $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
190 $output = SlotRecord::newWithSuppressedContent( $input );
191
192 $this->setExpectedException( SuppressedDataException::class );
193 $output->getContent();
194 }
195
196 public function testNewInherited() {
197 $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
198 $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
199
200 // This would happen while doing an edit, before saving revision meta-data.
201 $inherited = SlotRecord::newInherited( $parent );
202
203 $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
204 $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
205 $this->assertSame( $parent->getContent(), $inherited->getContent() );
206 $this->assertTrue( $inherited->isInherited() );
207 $this->assertTrue( $inherited->hasOrigin() );
208 $this->assertFalse( $inherited->hasRevision() );
209
210 // make sure we didn't mess with the internal state of $parent
211 $this->assertFalse( $parent->isInherited() );
212 $this->assertSame( 7, $parent->getRevision() );
213
214 // This would happen while doing an edit, after saving the revision meta-data
215 // and content meta-data.
216 $saved = SlotRecord::newSaved(
217 10,
218 $inherited->getContentId(),
219 $inherited->getAddress(),
220 $inherited
221 );
222 $this->assertSame( $parent->getContentId(), $saved->getContentId() );
223 $this->assertSame( $parent->getAddress(), $saved->getAddress() );
224 $this->assertSame( $parent->getContent(), $saved->getContent() );
225 $this->assertTrue( $saved->isInherited() );
226 $this->assertTrue( $saved->hasRevision() );
227 $this->assertSame( 10, $saved->getRevision() );
228
229 // make sure we didn't mess with the internal state of $parent or $inherited
230 $this->assertSame( 7, $parent->getRevision() );
231 $this->assertFalse( $inherited->hasRevision() );
232 }
233
234 public function testNewSaved() {
235 // This would happen while doing an edit, before saving revision meta-data.
236 $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
237
238 // This would happen while doing an edit, after saving the revision meta-data
239 // and content meta-data.
240 $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
241 $this->assertFalse( $saved->isInherited() );
242 $this->assertTrue( $saved->hasOrigin() );
243 $this->assertTrue( $saved->hasRevision() );
244 $this->assertTrue( $saved->hasAddress() );
245 $this->assertTrue( $saved->hasContentId() );
246 $this->assertSame( 'theNewAddress', $saved->getAddress() );
247 $this->assertSame( 20, $saved->getContentId() );
248 $this->assertSame( 'A', $saved->getContent()->getText() );
249 $this->assertSame( 10, $saved->getRevision() );
250 $this->assertSame( 10, $saved->getOrigin() );
251
252 // make sure we didn't mess with the internal state of $unsaved
253 $this->assertFalse( $unsaved->hasAddress() );
254 $this->assertFalse( $unsaved->hasContentId() );
255 $this->assertFalse( $unsaved->hasRevision() );
256 }
257
258 public function provideNewSaved_LogicException() {
259 $freshRow = $this->makeRow( [
260 'content_id' => 10,
261 'content_address' => 'address:1',
262 'slot_origin' => 1,
263 'slot_revision_id' => 1,
264 ] );
265
266 $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
267 yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
268 yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
269 yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
270
271 $inheritedRow = $this->makeRow( [
272 'content_id' => null,
273 'content_address' => null,
274 'slot_origin' => 0,
275 'slot_revision_id' => 1,
276 ] );
277
278 $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
279 yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
280 }
281
282 /**
283 * @dataProvider provideNewSaved_LogicException
284 */
285 public function testNewSaved_LogicException(
286 $revisionId,
287 $contentId,
288 $contentAddress,
289 SlotRecord $protoSlot
290 ) {
291 $this->setExpectedException( LogicException::class );
292 SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
293 }
294
295 public function provideNewSaved_InvalidArgumentException() {
296 $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
297
298 yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
299 yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
300 yield 'bad content address' => [ 7, 5, 77, $unsaved ];
301 }
302
303 /**
304 * @dataProvider provideNewSaved_InvalidArgumentException
305 */
306 public function testNewSaved_InvalidArgumentException(
307 $revisionId,
308 $contentId,
309 $contentAddress,
310 SlotRecord $protoSlot
311 ) {
312 $this->setExpectedException( InvalidArgumentException::class );
313 SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
314 }
315
316 public function provideHasSameContent() {
317 $fail = function () {
318 self::fail( 'There should be no need to actually load the content.' );
319 };
320
321 $a100a1 = new SlotRecord(
322 $this->makeRow(
323 [
324 'model_name' => 'A',
325 'content_size' => 100,
326 'content_sha1' => 'hash-a',
327 'content_address' => 'xxx:a1',
328 ]
329 ),
330 $fail
331 );
332 $a100a1b = new SlotRecord(
333 $this->makeRow(
334 [
335 'model_name' => 'A',
336 'content_size' => 100,
337 'content_sha1' => 'hash-a',
338 'content_address' => 'xxx:a1',
339 ]
340 ),
341 $fail
342 );
343 $a100null = new SlotRecord(
344 $this->makeRow(
345 [
346 'model_name' => 'A',
347 'content_size' => 100,
348 'content_sha1' => 'hash-a',
349 'content_address' => null,
350 ]
351 ),
352 $fail
353 );
354 $a100a2 = new SlotRecord(
355 $this->makeRow(
356 [
357 'model_name' => 'A',
358 'content_size' => 100,
359 'content_sha1' => 'hash-a',
360 'content_address' => 'xxx:a2',
361 ]
362 ),
363 $fail
364 );
365 $b100a1 = new SlotRecord(
366 $this->makeRow(
367 [
368 'model_name' => 'B',
369 'content_size' => 100,
370 'content_sha1' => 'hash-a',
371 'content_address' => 'xxx:a1',
372 ]
373 ),
374 $fail
375 );
376 $a200a1 = new SlotRecord(
377 $this->makeRow(
378 [
379 'model_name' => 'A',
380 'content_size' => 200,
381 'content_sha1' => 'hash-a',
382 'content_address' => 'xxx:a2',
383 ]
384 ),
385 $fail
386 );
387 $a100x1 = new SlotRecord(
388 $this->makeRow(
389 [
390 'model_name' => 'A',
391 'content_size' => 100,
392 'content_sha1' => 'hash-x',
393 'content_address' => 'xxx:x1',
394 ]
395 ),
396 $fail
397 );
398
399 yield 'same instance' => [ $a100a1, $a100a1, true ];
400 yield 'no address' => [ $a100a1, $a100null, true ];
401 yield 'same address' => [ $a100a1, $a100a1b, true ];
402 yield 'different address' => [ $a100a1, $a100a2, true ];
403 yield 'different model' => [ $a100a1, $b100a1, false ];
404 yield 'different size' => [ $a100a1, $a200a1, false ];
405 yield 'different hash' => [ $a100a1, $a100x1, false ];
406 }
407
408 /**
409 * @dataProvider provideHasSameContent
410 */
411 public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
412 $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
413 $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
414 }
415
416 }