1<?php 2 3namespace MediaWiki\Tests\Rest\Handler; 4 5use ApiUsageException; 6use FormatJson; 7use HashConfig; 8use MediaWiki\Content\IContentHandlerFactory; 9use MediaWiki\Rest\Handler\UpdateHandler; 10use MediaWiki\Rest\HttpException; 11use MediaWiki\Rest\LocalizedHttpException; 12use MediaWiki\Rest\RequestData; 13use MediaWiki\Revision\RevisionLookup; 14use MediaWiki\Storage\MutableRevisionRecord; 15use MediaWiki\Storage\SlotRecord; 16use MediaWikiTitleCodec; 17use MockTitleTrait; 18use PHPUnit\Framework\MockObject\MockObject; 19use Status; 20use Title; 21use Wikimedia\Message\MessageValue; 22use Wikimedia\Message\ParamType; 23use Wikimedia\Message\ScalarParam; 24use WikitextContent; 25use WikitextContentHandler; 26 27/** 28 * @covers \MediaWiki\Rest\Handler\UpdateHandler 29 */ 30class UpdateHandlerTest extends \MediaWikiLangTestCase { 31 32 use ActionModuleBasedHandlerTestTrait; 33 use MockTitleTrait; 34 35 private function newHandler( $resultData, $throwException = null, $csrfSafe = false ) { 36 $config = new HashConfig( [ 37 'RightsUrl' => 'https://creativecommons.org/licenses/by-sa/4.0/', 38 'RightsText' => 'CC-BY-SA 4.0' 39 ] ); 40 41 /** @var IContentHandlerFactory|MockObject $contentHandlerFactory */ 42 $contentHandlerFactory = 43 $this->createNoOpMock( 44 IContentHandlerFactory::class, 45 [ 'isDefinedModel', 'getContentHandler' ] 46 ); 47 48 $contentHandlerFactory 49 ->method( 'isDefinedModel' ) 50 ->willReturnMap( [ 51 [ CONTENT_MODEL_WIKITEXT, true ], 52 ] ); 53 54 $contentHandlerFactory 55 ->method( 'getContentHandler' ) 56 ->willReturn( new WikitextContentHandler() ); 57 58 /** @var MockObject|MediaWikiTitleCodec $titleCodec */ 59 $titleCodec = $this->getMockBuilder( MediaWikiTitleCodec::class ) 60 ->disableOriginalConstructor() 61 ->onlyMethods( [ 'formatTitle', 'splitTitleString' ] ) 62 ->getMock(); 63 64 $titleCodec->method( 'formatTitle' ) 65 ->willReturnCallback( static function ( $namespace, $text ) { 66 return "ns:$namespace:" . ucfirst( $text ); 67 } ); 68 $titleCodec->method( 'splitTitleString' ) 69 ->willReturnCallback( static function ( $text ) { 70 return [ 71 'interwiki' => '', 72 'fragment' => '', 73 'namespace' => 0, 74 'dbkey' => str_replace( ' ', '_', $text ) 75 ]; 76 } ); 77 78 /** @var RevisionLookup|MockObject $revisionLookup */ 79 $revisionLookup = $this->createNoOpMock( 80 RevisionLookup::class, 81 [ 'getRevisionById', 'getRevisionByTitle' ] 82 ); 83 $revisionLookup->method( 'getRevisionById' ) 84 ->willReturnCallback( function ( $id ) { 85 $title = $this->makeMockTitle( __CLASS__ ); 86 $rev = new MutableRevisionRecord( $title ); 87 $rev->setId( $id ); 88 $rev->setContent( SlotRecord::MAIN, new WikitextContent( "Content of revision $id" ) ); 89 $rev->setTimestamp( '2020-01-01T01:02:03Z' ); 90 return $rev; 91 } ); 92 $revisionLookup->method( 'getRevisionByTitle' ) 93 ->willReturnCallback( static function ( $title ) { 94 $rev = new MutableRevisionRecord( Title::castFromLinkTarget( $title ) ); 95 $rev->setId( 1234 ); 96 $rev->setContent( SlotRecord::MAIN, new WikitextContent( "Current content of $title" ) ); 97 $rev->setTimestamp( '2020-01-01T01:02:03Z' ); 98 return $rev; 99 } ); 100 101 $handler = new UpdateHandler( 102 $config, 103 $contentHandlerFactory, 104 $titleCodec, 105 $titleCodec, 106 $revisionLookup 107 ); 108 109 $apiMain = $this->getApiMain( $csrfSafe ); 110 $dummyModule = $this->getDummyApiModule( $apiMain, 'edit', $resultData, $throwException ); 111 112 $handler->setApiMain( $apiMain ); 113 $handler->overrideActionModule( 114 'edit', 115 'action', 116 $dummyModule 117 ); 118 119 return $handler; 120 } 121 122 public function provideExecute() { 123 yield "create with token" => [ 124 [ // Request data received by UpdateHandler 125 'method' => 'PUT', 126 'pathParams' => [ 'title' => 'Foo' ], 127 'headers' => [ 128 'Content-Type' => 'application/json', 129 ], 130 'bodyContents' => json_encode( [ 131 'token' => 'TOKEN', 132 'source' => 'Lorem Ipsum', 133 'comment' => 'Testing' 134 ] ), 135 ], 136 [ // Fake request expected to be passed into ApiEditPage 137 'title' => 'Foo', 138 'text' => 'Lorem Ipsum', 139 'summary' => 'Testing', 140 'createonly' => '1', 141 'token' => 'TOKEN', 142 ], 143 [ // Mock response returned by ApiEditPage 144 "edit" => [ 145 "new" => true, 146 "result" => "Success", 147 "pageid" => 94542, 148 "title" => "Foo", 149 "contentmodel" => "wikitext", 150 "oldrevid" => 0, 151 "newrevid" => 371707, 152 "newtimestamp" => "2018-12-18T16:59:42Z", 153 ] 154 ], 155 [ // Response expected to be generated by UpdateHandler 156 'id' => 94542, 157 'title' => 'ns:0:Foo', 158 'key' => 'ns:0:Foo', 159 'content_model' => 'wikitext', 160 'latest' => [ 161 'id' => 371707, 162 'timestamp' => "2018-12-18T16:59:42Z" 163 ], 164 'license' => [ 165 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', 166 'title' => 'CC-BY-SA 4.0' 167 ], 168 'source' => 'Content of revision 371707' 169 ], 170 false 171 ]; 172 173 yield "create with model" => [ 174 [ // Request data received by UpdateHandler 175 'method' => 'POST', 176 'pathParams' => [ 'title' => 'Foo' ], 177 'headers' => [ 178 'Content-Type' => 'application/json', 179 ], 180 'bodyContents' => json_encode( [ 181 'token' => 'TOKEN', 182 'source' => 'Lorem Ipsum', 183 'comment' => 'Testing', 184 'content_model' => CONTENT_MODEL_WIKITEXT, 185 ] ), 186 ], 187 [ // Fake request expected to be passed into ApiEditPage 188 'title' => 'Foo', 189 'text' => 'Lorem Ipsum', 190 'summary' => 'Testing', 191 'contentmodel' => 'wikitext', 192 'createonly' => '1', 193 'token' => 'TOKEN', 194 ], 195 [ // Mock response returned by ApiEditPage 196 "edit" => [ 197 "new" => true, 198 "result" => "Success", 199 "pageid" => 94542, 200 "title" => "Foo", 201 "contentmodel" => "wikitext", 202 "oldrevid" => 0, 203 "newrevid" => 371707, 204 "newtimestamp" => "2018-12-18T16:59:42Z", 205 ] 206 ], 207 [ // Response expected to be generated by UpdateHandler 208 'id' => 94542, 209 'title' => 'ns:0:Foo', 210 'key' => 'ns:0:Foo', 211 'content_model' => 'wikitext', 212 'latest' => [ 213 'id' => 371707, 214 'timestamp' => "2018-12-18T16:59:42Z" 215 ], 216 'license' => [ 217 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', 218 'title' => 'CC-BY-SA 4.0' 219 ], 220 'source' => 'Content of revision 371707' 221 ], 222 false 223 ]; 224 225 yield "update with token" => [ 226 [ // Request data received by UpdateHandler 227 'method' => 'PUT', 228 'pathParams' => [ 'title' => 'foo bar' ], 229 'headers' => [ 230 'Content-Type' => 'application/json', 231 ], 232 'bodyContents' => json_encode( [ 233 'token' => 'TOKEN', 234 'source' => 'Lorem Ipsum', 235 'comment' => 'Testing', 236 'latest' => [ 'id' => 789123 ], 237 ] ), 238 ], 239 [ // Fake request expected to be passed into ApiEditPage 240 'title' => 'foo bar', 241 'text' => 'Lorem Ipsum', 242 'summary' => 'Testing', 243 'nocreate' => '1', 244 'baserevid' => '789123', 245 'token' => 'TOKEN', 246 ], 247 [ // Mock response returned by ApiEditPage 248 "edit" => [ 249 "result" => "Success", 250 "pageid" => 94542, 251 "title" => "Foo_bar", 252 "contentmodel" => "wikitext", 253 "oldrevid" => 371705, 254 "newrevid" => 371707, 255 "newtimestamp" => "2018-12-18T16:59:42Z", 256 ] 257 ], 258 [ // Response expected to be generated by UpdateHandler 259 'id' => 94542, 260 'content_model' => 'wikitext', 261 'title' => 'ns:0:Foo bar', 262 'key' => 'ns:0:Foo_bar', 263 'latest' => [ 264 'id' => 371707, 265 'timestamp' => "2018-12-18T16:59:42Z" 266 ], 267 'license' => [ 268 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', 269 'title' => 'CC-BY-SA 4.0' 270 ], 271 'source' => 'Content of revision 371707' 272 ], 273 false 274 ]; 275 276 yield "update with model" => [ 277 [ // Request data received by UpdateHandler 278 'method' => 'POST', 279 'pathParams' => [ 'title' => 'foo bar' ], 280 'headers' => [ 281 'Content-Type' => 'application/json', 282 ], 283 'bodyContents' => json_encode( [ 284 'source' => 'Lorem Ipsum', 285 'comment' => 'Testing', 286 'content_model' => CONTENT_MODEL_WIKITEXT, 287 'latest' => [ 'id' => 789123 ], 288 ] ), 289 ], 290 [ // Fake request expected to be passed into ApiEditPage 291 'title' => 'foo bar', 292 'text' => 'Lorem Ipsum', 293 'summary' => 'Testing', 294 'contentmodel' => 'wikitext', 295 'nocreate' => '1', 296 'baserevid' => '789123', 297 'token' => '+\\', 298 ], 299 [ // Mock response returned by ApiEditPage 300 "edit" => [ 301 "result" => "Success", 302 "pageid" => 94542, 303 "title" => "Foo_bar", 304 "contentmodel" => "wikitext", 305 "oldrevid" => 371705, 306 "newrevid" => 371707, 307 "newtimestamp" => "2018-12-18T16:59:42Z", 308 ] 309 ], 310 [ // Response expected to be generated by UpdateHandler 311 'id' => 94542, 312 'content_model' => 'wikitext', 313 'title' => 'ns:0:Foo bar', 314 'key' => 'ns:0:Foo_bar', 315 'latest' => [ 316 'id' => 371707, 317 'timestamp' => "2018-12-18T16:59:42Z" 318 ], 319 'license' => [ 320 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', 321 'title' => 'CC-BY-SA 4.0' 322 ], 323 'source' => 'Content of revision 371707' 324 ], 325 true 326 ]; 327 328 yield "update without token" => [ 329 [ // Request data received by UpdateHandler 330 'method' => 'PUT', 331 'pathParams' => [ 'title' => 'Foo' ], 332 'headers' => [ 333 'Content-Type' => 'application/json', 334 ], 335 'bodyContents' => json_encode( [ 336 'source' => 'Lorem Ipsum', 337 'comment' => 'Testing', 338 'content_model' => CONTENT_MODEL_WIKITEXT, 339 'latest' => [ 'id' => 789123 ], 340 ] ), 341 ], 342 [ // Fake request expected to be passed into ApiEditPage 343 'title' => 'Foo', 344 'text' => 'Lorem Ipsum', 345 'summary' => 'Testing', 346 'contentmodel' => 'wikitext', 347 'nocreate' => '1', 348 'baserevid' => '789123', 349 'token' => '+\\', // use known-good token for current user (anon) 350 ], 351 [ // Mock response returned by ApiEditPage 352 "edit" => [ 353 "result" => "Success", 354 "pageid" => 94542, 355 "title" => "Foo", 356 "contentmodel" => "wikitext", 357 "oldrevid" => 371705, 358 "newrevid" => 371707, 359 "newtimestamp" => "2018-12-18T16:59:42Z", 360 ] 361 ], 362 [ // Response expected to be generated by UpdateHandler 363 'id' => 94542, 364 'content_model' => 'wikitext', 365 'latest' => [ 366 'id' => 371707, 367 'timestamp' => "2018-12-18T16:59:42Z" 368 ], 369 'license' => [ 370 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', 371 'title' => 'CC-BY-SA 4.0' 372 ], 373 ], 374 true 375 ]; 376 377 yield "null-edit (unchanged)" => [ 378 [ // Request data received by UpdateHandler 379 'method' => 'PUT', 380 'pathParams' => [ 'title' => 'Foo' ], 381 'headers' => [ 382 'Content-Type' => 'application/json', 383 ], 384 'bodyContents' => json_encode( [ 385 'source' => 'Lorem Ipsum', 386 'comment' => 'Testing', 387 'content_model' => CONTENT_MODEL_WIKITEXT, 388 'latest' => [ 'id' => 789123 ], 389 ] ), 390 ], 391 [ // Fake request expected to be passed into ApiEditPage 392 'title' => 'Foo', 393 'text' => 'Lorem Ipsum', 394 'summary' => 'Testing', 395 'contentmodel' => 'wikitext', 396 'nocreate' => '1', 397 'baserevid' => '789123', 398 'token' => '+\\', // use known-good token for current user (anon) 399 ], 400 [ // Mock response returned by ApiEditPage 401 "edit" => [ 402 "result" => "Success", 403 "pageid" => 94542, 404 "title" => "Foo", 405 "contentmodel" => "wikitext", 406 "nochange" => "", // null-edit! 407 ] 408 ], 409 [ // Response expected to be generated by UpdateHandler 410 'id' => 94542, 411 'content_model' => 'wikitext', 412 'latest' => [ 413 'id' => 1234, // ID of current rev, as defined in newHandler() 414 'timestamp' => '2020-01-01T01:02:03Z' // see fake RevisionStore in newHandler() 415 ], 416 'license' => [ 417 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', 418 'title' => 'CC-BY-SA 4.0' 419 ], 420 ], 421 true 422 ]; 423 } 424 425 /** 426 * @dataProvider provideExecute 427 */ 428 public function testExecute( 429 $requestData, 430 $expectedActionParams, 431 $actionResult, 432 $expectedResponse, 433 $csrfSafe 434 ) { 435 $request = new RequestData( $requestData ); 436 437 $handler = $this->newHandler( $actionResult, null, $csrfSafe ); 438 439 $responseData = $this->executeHandlerAndGetBodyData( $handler, $request ); 440 441 // Check parameters passed to ApiEditPage by UpdateHandler based on $requestData 442 foreach ( $expectedActionParams as $key => $value ) { 443 $this->assertSame( 444 $value, 445 $handler->getApiMain()->getVal( $key ), 446 "ApiEditPage param: $key" 447 ); 448 } 449 450 // Check response that UpdateHandler created after receiving $actionResult from ApiEditPage 451 foreach ( $expectedResponse as $key => $value ) { 452 $this->assertArrayHasKey( $key, $responseData ); 453 $this->assertSame( 454 $value, 455 $responseData[ $key ], 456 "UpdateHandler response field: $key" 457 ); 458 } 459 } 460 461 public function provideBodyValidation() { 462 yield "missing source field" => [ 463 [ // Request data received by UpdateHandler 464 'method' => 'PUT', 465 'pathParams' => [ 'title' => 'Foo' ], 466 'headers' => [ 467 'Content-Type' => 'application/json', 468 ], 469 'bodyContents' => json_encode( [ 470 'token' => 'TOKEN', 471 'comment' => 'Testing', 472 'content_model' => CONTENT_MODEL_WIKITEXT, 473 ] ), 474 ], 475 new MessageValue( 'rest-missing-body-field', [ 'source' ] ), 476 ]; 477 yield "missing comment field" => [ 478 [ // Request data received by UpdateHandler 479 'method' => 'PUT', 480 'pathParams' => [ 'title' => 'Foo' ], 481 'headers' => [ 482 'Content-Type' => 'application/json', 483 ], 484 'bodyContents' => json_encode( [ 485 'token' => 'TOKEN', 486 'source' => 'Lorem Ipsum', 487 'content_model' => CONTENT_MODEL_WIKITEXT, 488 ] ), 489 ], 490 new MessageValue( 'rest-missing-body-field', [ 'comment' ] ), 491 ]; 492 } 493 494 /** 495 * @dataProvider provideBodyValidation 496 */ 497 public function testBodyValidation( array $requestData, MessageValue $expectedMessage ) { 498 $request = new RequestData( $requestData ); 499 500 $handler = $this->newHandler( [] ); 501 502 $exception = $this->executeHandlerAndGetHttpException( $handler, $request ); 503 504 $this->assertSame( 400, $exception->getCode(), 'HTTP status' ); 505 $this->assertInstanceOf( LocalizedHttpException::class, $exception ); 506 507 /** @var LocalizedHttpException $exception */ 508 $this->assertEquals( $expectedMessage, $exception->getMessageValue() ); 509 } 510 511 public function testBodyValidation_extraneousToken() { 512 $requestData = [ 513 'method' => 'PUT', 514 'pathParams' => [ 'title' => 'Foo' ], 515 'headers' => [ 516 'Content-Type' => 'application/json', 517 ], 518 'bodyContents' => json_encode( [ 519 'token' => 'TOKEN', 520 'comment' => 'Testing', 521 'source' => 'Lorem Ipsum', 522 'content_model' => 'wikitext' 523 ] ), 524 ]; 525 526 $request = new RequestData( $requestData ); 527 528 $handler = $this->newHandler( [], null, true ); 529 530 $exception = $this->executeHandlerAndGetHttpException( $handler, $request ); 531 532 $this->assertSame( 400, $exception->getCode(), 'HTTP status' ); 533 $this->assertInstanceOf( LocalizedHttpException::class, $exception ); 534 535 $expectedMessage = new MessageValue( 'rest-extraneous-csrf-token' ); 536 $this->assertEquals( $expectedMessage, $exception->getMessageValue() ); 537 } 538 539 public function provideHeaderValidation() { 540 yield "bad content type" => [ 541 [ // Request data received by UpdateHandler 542 'method' => 'PUT', 543 'pathParams' => [ 'title' => 'Foo' ], 544 'headers' => [ 545 'Content-Type' => 'text/plain', 546 ], 547 'bodyContents' => json_encode( [ 548 'token' => 'TOKEN', 549 'source' => 'Lorem Ipsum', 550 'comment' => 'Testing', 551 'content_model' => CONTENT_MODEL_WIKITEXT, 552 ] ), 553 ], 554 415 555 ]; 556 } 557 558 /** 559 * @dataProvider provideHeaderValidation 560 */ 561 public function testHeaderValidation( array $requestData, $expectedStatus ) { 562 $request = new RequestData( $requestData ); 563 564 $handler = $this->newHandler( [] ); 565 566 $exception = $this->executeHandlerAndGetHttpException( $handler, $request ); 567 568 $this->assertSame( $expectedStatus, $exception->getCode(), 'HTTP status' ); 569 } 570 571 public function provideErrorMapping() { 572 yield "missingtitle" => [ 573 new ApiUsageException( null, Status::newFatal( 'apierror-missingtitle' ) ), 574 new LocalizedHttpException( new MessageValue( 'apierror-missingtitle' ), 404 ), 575 ]; 576 yield "protectedpage" => [ 577 new ApiUsageException( null, Status::newFatal( 'apierror-protectedpage' ) ), 578 new LocalizedHttpException( new MessageValue( 'apierror-protectedpage' ), 403 ), 579 ]; 580 yield "articleexists" => [ 581 new ApiUsageException( null, Status::newFatal( 'apierror-articleexists' ) ), 582 new LocalizedHttpException( 583 new MessageValue( 'rest-update-cannot-create-page', [ 'Foo' ] ), 584 409 585 ), 586 ]; 587 yield "editconflict" => [ 588 new ApiUsageException( null, Status::newFatal( 'apierror-editconflict' ) ), 589 new LocalizedHttpException( new MessageValue( 'apierror-editconflict' ), 409 ), 590 ]; 591 yield "ratelimited" => [ 592 new ApiUsageException( null, Status::newFatal( 'apierror-ratelimited' ) ), 593 new LocalizedHttpException( new MessageValue( 'apierror-ratelimited' ), 429 ), 594 ]; 595 yield "badtoken" => [ 596 new ApiUsageException( 597 null, 598 Status::newFatal( 'apierror-badtoken', [ 'plaintext' => 'BAD' ] ) 599 ), 600 new LocalizedHttpException( 601 new MessageValue( 602 'apierror-badtoken', 603 [ new ScalarParam( ParamType::PLAINTEXT, 'BAD' ) ] 604 ), 403 605 ), 606 ]; 607 608 // Unmapped errors should be passed through with a status 400. 609 yield "no-direct-editing" => [ 610 new ApiUsageException( null, Status::newFatal( 'apierror-no-direct-editing' ) ), 611 new LocalizedHttpException( new MessageValue( 'apierror-no-direct-editing' ), 400 ), 612 ]; 613 yield "badformat" => [ 614 new ApiUsageException( null, Status::newFatal( 'apierror-badformat' ) ), 615 new LocalizedHttpException( new MessageValue( 'apierror-badformat' ), 400 ), 616 ]; 617 yield "emptypage" => [ 618 new ApiUsageException( null, Status::newFatal( 'apierror-emptypage' ) ), 619 new LocalizedHttpException( new MessageValue( 'apierror-emptypage' ), 400 ), 620 ]; 621 } 622 623 /** 624 * @dataProvider provideErrorMapping 625 */ 626 public function testErrorMapping( 627 ApiUsageException $apiUsageException, 628 HttpException $expectedHttpException 629 ) { 630 $requestData = [ // Request data received by UpdateHandler 631 'method' => 'POST', 632 'pathParams' => [ 'title' => 'Foo' ], 633 'headers' => [ 634 'Content-Type' => 'application/json', 635 ], 636 'bodyContents' => json_encode( [ 637 'source' => 'Lorem Ipsum', 638 'comment' => 'Testing', 639 'content_model' => CONTENT_MODEL_WIKITEXT, 640 ] ), 641 ]; 642 $request = new RequestData( $requestData ); 643 644 $handler = $this->newHandler( [], $apiUsageException ); 645 646 $exception = $this->executeHandlerAndGetHttpException( $handler, $request ); 647 648 $this->assertSame( $expectedHttpException->getMessage(), $exception->getMessage() ); 649 $this->assertSame( $expectedHttpException->getCode(), $exception->getCode(), 'HTTP status' ); 650 651 $errorData = $exception->getErrorData(); 652 if ( $expectedHttpException->getErrorData() ) { 653 foreach ( $expectedHttpException->getErrorData() as $key => $value ) { 654 $this->assertSame( $value, $errorData[$key], 'Error data key $key' ); 655 } 656 } 657 658 if ( $expectedHttpException instanceof LocalizedHttpException ) { 659 /** @var LocalizedHttpException $exception */ 660 $this->assertEquals( 661 $expectedHttpException->getMessageValue(), 662 $exception->getMessageValue() 663 ); 664 } 665 } 666 667 public function testConflictOutput() { 668 $requestData = [ // Request data received by UpdateHandler 669 'method' => 'POST', 670 'pathParams' => [ 'title' => 'Foo' ], 671 'headers' => [ 672 'Content-Type' => 'application/json', 673 ], 674 'bodyContents' => json_encode( [ 675 'latest' => [ 676 'id' => 17, 677 ], 678 'source' => 'Lorem Ipsum', 679 'comment' => 'Testing' 680 ] ), 681 ]; 682 $request = new RequestData( $requestData ); 683 684 $apiUsageException = new ApiUsageException( null, Status::newFatal( 'apierror-editconflict' ) ); 685 $handler = $this->newHandler( [], $apiUsageException ); 686 $handler->setJsonDiffFunction( [ $this, 'fakeJsonDiff' ] ); 687 688 $exception = $this->executeHandlerAndGetHttpException( $handler, $request ); 689 690 $this->assertSame( 409, $exception->getCode(), 'HTTP status' ); 691 692 $expectedData = [ 693 'local' => [ 694 'from' => 'Content of revision 17', 695 'to' => 'Lorem Ipsum', 696 ], 697 'remote' => [ 698 'from' => 'Content of revision 17', 699 'to' => 'Current content of 0:Foo', 700 ], 701 'base' => 17, 702 'current' => 1234 703 ]; 704 705 $errorData = $exception->getErrorData(); 706 foreach ( $expectedData as $key => $value ) { 707 $this->assertSame( $value, $errorData[$key], "Error data key $key" ); 708 } 709 } 710 711 public function fakeJsonDiff( $fromText, $toText ) { 712 return FormatJson::encode( [ 713 'from' => $fromText, 714 'to' => $toText 715 ] ); 716 } 717 718} 719