1<?php 2 3namespace MediaWiki\Tests\Storage; 4 5use MediaWiki\Revision\MutableRevisionRecord; 6use MediaWiki\Revision\RevisionRecord; 7use MediaWiki\Revision\RevisionStore; 8use MediaWiki\Storage\EditResult; 9use MediaWiki\Storage\EditResultBuilder; 10use MediaWiki\Storage\PageUpdateException; 11use MediaWikiUnitTestCase; 12use Title; 13 14/** 15 * @covers \MediaWiki\Storage\EditResultBuilder 16 * @covers \MediaWiki\Storage\EditResult 17 */ 18class EditResultBuilderTest extends MediaWikiUnitTestCase { 19 20 /** 21 * @covers \MediaWiki\Storage\EditResultBuilder::buildEditResult 22 */ 23 public function testBuilderThrowsExceptionOnMissingRevision() { 24 $erb = $this->getNewEditResultBuilder(); 25 26 $this->expectException( PageUpdateException::class ); 27 $erb->buildEditResult(); 28 } 29 30 /** 31 * @covers \MediaWiki\Storage\EditResultBuilder 32 */ 33 public function testIsNewUnset() { 34 $erb = $this->getNewEditResultBuilder(); 35 $erb->setRevisionRecord( $this->getDummyRevision() ); 36 $er = $erb->buildEditResult(); 37 38 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 39 } 40 41 public function provideSetIsNew() { 42 return [ 43 'not a new page' => [ false ], 44 'a new page' => [ true ] 45 ]; 46 } 47 48 /** 49 * @dataProvider provideSetIsNew 50 * @covers \MediaWiki\Storage\EditResultBuilder 51 * @param bool $isNew 52 */ 53 public function testSetIsNew( bool $isNew ) { 54 $erb = $this->getNewEditResultBuilder(); 55 $erb->setIsNew( $isNew ); 56 $erb->setRevisionRecord( $this->getDummyRevision() ); 57 $er = $erb->buildEditResult(); 58 59 $this->assertSame( $isNew, $er->isNew(), 'EditResult::isNew()' ); 60 } 61 62 /** 63 * Tests a normal edit to the page 64 * @covers \MediaWiki\Storage\EditResult 65 * @covers \MediaWiki\Storage\EditResultBuilder 66 */ 67 public function testEditNotARevert() { 68 $erb = $this->getNewEditResultBuilder(); 69 $erb->setRevisionRecord( $this->getDummyRevision() ); 70 $er = $erb->buildEditResult(); 71 72 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 73 $this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' ); 74 $this->assertFalse( $er->getOriginalRevisionId(), 75 'EditResult::getOriginalRevisionId()' ); 76 $this->assertFalse( $er->isRevert(), 'EditResult::isRevert()' ); 77 $this->assertFalse( $er->isExactRevert(), 'EditResult::isExactRevert()' ); 78 $this->assertNull( $er->getRevertMethod(), 'EditResult::getRevertMethod()' ); 79 $this->assertNull( $er->getOldestRevertedRevisionId(), 80 'EditResult::getOldestRevertedRevisionId()' ); 81 $this->assertNull( $er->getNewestRevertedRevisionId(), 82 'EditResult::getNewestRevertedRevisionId()' ); 83 $this->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' ); 84 $this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' ); 85 } 86 87 /** 88 * Tests the case when the new edit doesn't actually change anything on the page, 89 * i.e. is a null edit. 90 * @covers \MediaWiki\Storage\EditResult 91 * @covers \MediaWiki\Storage\EditResultBuilder 92 */ 93 public function testNullEdit() { 94 $originalRevision = $this->getExistingRevision(); 95 $erb = $this->getNewEditResultBuilder( $originalRevision ); 96 $newRevision = MutableRevisionRecord::newFromParentRevision( $originalRevision ); 97 98 $erb->setOriginalRevisionId( $originalRevision->getId() ); 99 $erb->setRevisionRecord( $newRevision ); 100 $er = $erb->buildEditResult(); 101 102 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 103 $this->assertTrue( $er->isNullEdit(), 'EditResult::isNullEdit()' ); 104 $this->assertSame( $originalRevision->getId(), $er->getOriginalRevisionId(), 105 'EditResult::getOriginalRevisionId()' ); 106 $this->assertFalse( $er->isRevert(), 'EditResult::isRevert()' ); 107 $this->assertFalse( $er->isExactRevert(), 'EditResult::isExactRevert()' ); 108 $this->assertNull( $er->getRevertMethod(), 'EditResult::getRevertMethod()' ); 109 $this->assertNull( $er->getOldestRevertedRevisionId(), 110 'EditResult::getOldestRevertedRevisionId()' ); 111 $this->assertNull( $er->getNewestRevertedRevisionId(), 112 'EditResult::getNewestRevertedRevisionId()' ); 113 $this->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' ); 114 $this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' ); 115 } 116 117 public function provideEnabledSoftwareTagsForRollback() : array { 118 return [ 119 "all change tags enabled" => [ 120 $this->getSoftwareTags(), 121 [ "mw-rollback" ] 122 ], 123 "no change tags enabled" => [ 124 [], 125 [] 126 ] 127 ]; 128 } 129 130 /** 131 * Test the case where the edit restored the page exactly to a previous state. 132 * 133 * @covers \MediaWiki\Storage\EditResult 134 * @covers \MediaWiki\Storage\EditResultBuilder 135 * @dataProvider provideEnabledSoftwareTagsForRollback 136 * 137 * @param string[] $changeTags 138 * @param string[] $expectedRevertTags 139 */ 140 public function testRollback( array $changeTags, array $expectedRevertTags ) { 141 $originalRevision = $this->getExistingRevision(); 142 $erb = $this->getNewEditResultBuilder( $originalRevision, $changeTags ); 143 $newRevision = MutableRevisionRecord::newFromParentRevision( $originalRevision ); 144 // We change the parent id to something different, so it's not treated as a null edit 145 $newRevision->setParentId( 125 ); 146 147 $erb->setOriginalRevisionId( $originalRevision->getId() ); 148 $erb->setRevisionRecord( $newRevision ); 149 // We are bluffing here, those revision ids don't exist. 150 // EditResult is as dumb as possible, it doesn't check that. 151 $erb->markAsRevert( EditResult::REVERT_ROLLBACK, 123, 125 ); 152 $er = $erb->buildEditResult(); 153 154 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 155 $this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' ); 156 $this->assertSame( $originalRevision->getId(), $er->getOriginalRevisionId(), 157 'EditResult::getOriginalRevisionId()' ); 158 $this->assertTrue( $er->isRevert(), 'EditResult::isRevert()' ); 159 $this->assertTrue( $er->isExactRevert(), 'EditResult::isExactRevert()' ); 160 $this->assertSame( EditResult::REVERT_ROLLBACK, $er->getRevertMethod(), 161 'EditResult::getRevertMethod()' ); 162 $this->assertSame( 123, $er->getOldestRevertedRevisionId(), 163 'EditResult::getOldestRevertedRevisionId()' ); 164 $this->assertSame( 125, $er->getNewestRevertedRevisionId(), 165 'EditResult::getNewestRevertedRevisionId()' ); 166 $this->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' ); 167 $this->assertArrayEquals( $expectedRevertTags, $er->getRevertTags(), 168 'EditResult::getRevertTags' ); 169 } 170 171 public function provideEnabledSoftwareTagsForUndo() : array { 172 return [ 173 "all change tags enabled" => [ 174 $this->getSoftwareTags(), 175 [ "mw-undo" ] 176 ], 177 "no change tags enabled" => [ 178 [], 179 [] 180 ] 181 ]; 182 } 183 184 /** 185 * Test the case where the edit was an undo 186 * 187 * @covers \MediaWiki\Storage\EditResult 188 * @covers \MediaWiki\Storage\EditResultBuilder 189 * @dataProvider provideEnabledSoftwareTagsForUndo 190 * 191 * @param string[] $changeTags 192 * @param string[] $expectedRevertTags 193 */ 194 public function testUndo( array $changeTags, array $expectedRevertTags ) { 195 $originalRevision = $this->getExistingRevision(); 196 $erb = $this->getNewEditResultBuilder( $originalRevision, $changeTags ); 197 $newRevision = MutableRevisionRecord::newFromParentRevision( $originalRevision ); 198 // We change the parent id to something different, so it's not treated as a null edit 199 $newRevision->setParentId( 124 ); 200 201 $erb->setOriginalRevisionId( $originalRevision->getId() ); 202 $erb->setRevisionRecord( $newRevision ); 203 $erb->markAsRevert( EditResult::REVERT_UNDO, 124 ); 204 $er = $erb->buildEditResult(); 205 206 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 207 $this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' ); 208 $this->assertSame( $originalRevision->getId(), $er->getOriginalRevisionId(), 209 'EditResult::getOriginalRevisionId()' ); 210 $this->assertTrue( $er->isRevert(), 'EditResult::isRevert()' ); 211 $this->assertTrue( $er->isExactRevert(), 'EditResult::isExactRevert()' ); 212 $this->assertSame( EditResult::REVERT_UNDO, $er->getRevertMethod(), 213 'EditResult::getRevertMethod()' ); 214 $this->assertSame( 124, $er->getOldestRevertedRevisionId(), 215 'EditResult::getOldestRevertedRevisionId()' ); 216 $this->assertSame( 124, $er->getNewestRevertedRevisionId(), 217 'EditResult::getNewestRevertedRevisionId()' ); 218 $this->assertSame( 124, $er->getUndidRevId(), 'EditResult::getUndidRevId' ); 219 $this->assertArrayEquals( $expectedRevertTags, $er->getRevertTags(), 220 'EditResult::getRevertTags' ); 221 } 222 223 /** 224 * Test the case where setRevert() is called, but nothing was really reverted 225 * 226 * @covers \MediaWiki\Storage\EditResult 227 * @covers \MediaWiki\Storage\EditResultBuilder 228 */ 229 public function testIgnoreEmptyRevert() { 230 $erb = $this->getNewEditResultBuilder(); 231 $newRevision = $this->getDummyRevision(); 232 233 $erb->setRevisionRecord( $newRevision ); 234 $erb->markAsRevert( EditResult::REVERT_UNDO, 0 ); 235 $er = $erb->buildEditResult(); 236 237 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 238 $this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' ); 239 $this->assertFalse( $er->getOriginalRevisionId(), 'EditResult::getOriginalRevisionId()' ); 240 $this->assertFalse( $er->isRevert(), 'EditResult::isRevert()' ); 241 $this->assertFalse( $er->isExactRevert(), 'EditResult::isExactRevert()' ); 242 $this->assertNull( $er->getRevertMethod(), 'EditResult::getRevertMethod()' ); 243 $this->assertNull( $er->getOldestRevertedRevisionId(), 244 'EditResult::getOldestRevertedRevisionId()' ); 245 $this->assertNull( $er->getNewestRevertedRevisionId(), 246 'EditResult::getNewestRevertedRevisionId()' ); 247 $this->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' ); 248 $this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' ); 249 } 250 251 /** 252 * Test the case where setRevert() is properly called, but the original revision was not set 253 * 254 * @covers \MediaWiki\Storage\EditResult 255 * @covers \MediaWiki\Storage\EditResultBuilder 256 */ 257 public function testRevertWithoutOriginalRevision() { 258 $erb = $this->getNewEditResultBuilder( 259 null, 260 $this->getSoftwareTags() 261 ); 262 $newRevision = $this->getDummyRevision(); 263 264 $erb->setRevisionRecord( $newRevision ); 265 $erb->markAsRevert( EditResult::REVERT_UNDO, 123 ); 266 $er = $erb->buildEditResult(); 267 268 $this->assertFalse( $er->isNew(), 'EditResult::isNew()' ); 269 $this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' ); 270 $this->assertFalse( $er->getOriginalRevisionId(), 'EditResult::getOriginalRevisionId()' ); 271 $this->assertTrue( $er->isRevert(), 'EditResult::isRevert()' ); 272 $this->assertFalse( $er->isExactRevert(), 'EditResult::isExactRevert()' ); 273 $this->assertSame( EditResult::REVERT_UNDO, $er->getRevertMethod(), 274 'EditResult::getRevertMethod()' ); 275 $this->assertSame( 123, $er->getOldestRevertedRevisionId(), 276 'EditResult::getOldestRevertedRevisionId()' ); 277 $this->assertSame( 123, $er->getNewestRevertedRevisionId(), 278 'EditResult::getNewestRevertedRevisionId()' ); 279 $this->assertSame( 123, $er->getUndidRevId(), 'EditResult::getUndidRevId' ); 280 $this->assertArrayEquals( [ 'mw-undo' ], $er->getRevertTags(), 281 'EditResult::getRevertTags' ); 282 } 283 284 /** 285 * Returns an empty RevisionRecord 286 * 287 * @return MutableRevisionRecord 288 */ 289 private function getDummyRevision() : MutableRevisionRecord { 290 return new MutableRevisionRecord( 291 $this->createMock( Title::class ) 292 ); 293 } 294 295 /** 296 * Returns a RevisionRecord that pretends to have an ID and a page ID. 297 * 298 * @return MutableRevisionRecord 299 */ 300 private function getExistingRevision() : MutableRevisionRecord { 301 $revisionRecord = $this->getDummyRevision(); 302 $revisionRecord->setId( 5 ); 303 $revisionRecord->setPageId( 5 ); 304 return $revisionRecord; 305 } 306 307 /** 308 * Convenience function for creating a new EditResultBuilder object. 309 * 310 * @param RevisionRecord|null $originalRevisionRecord RevisionRecord that should be returned 311 * by RevisionStore::getRevisionById. 312 * @param string[] $changeTags 313 * 314 * @return EditResultBuilder 315 */ 316 private function getNewEditResultBuilder( 317 ?RevisionRecord $originalRevisionRecord = null, 318 array $changeTags = [] 319 ) { 320 $store = $this->createMock( RevisionStore::class ); 321 $store->method( 'getRevisionById' ) 322 ->willReturn( $originalRevisionRecord ); 323 324 return new EditResultBuilder( 325 $store, 326 $changeTags 327 ); 328 } 329 330 /** 331 * Meant to reproduce the values provided by ChangeTags::getSoftwareTags. 332 * 333 * @return string[] 334 */ 335 private function getSoftwareTags() : array { 336 return [ 337 "mw-contentmodelchange", 338 "mw-new-redirect", 339 "mw-removed-redirect", 340 "mw-changed-redirect-target", 341 "mw-blank", 342 "mw-replace", 343 "mw-rollback", 344 "mw-undo" 345 ]; 346 } 347} 348