From: Aryeh Gregor Date: Tue, 27 Mar 2018 17:12:48 +0000 (+0300) Subject: Improve test coverage for ApiParse X-Git-Tag: 1.31.0-rc.0~106^2 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=84db63aa6f0482f12ddf2b207944e942aaaf4d4c Improve test coverage for ApiParse Also removed a sketchy-looking usage of ?: with a string in ApiParse.php. In this case I think it was fine, because it would only cause a bug if a page's display title was '0' but its actual title was not '0', which is only possible if $wgRestrictDisplayTitle is false, which is broken by design anyway and I don't think is worth testing. But ?: used for something that should be interpreted as a string is generally not a good idea. One bug fixed: an error message that used an undefined variable. Depends-On: Id0e6184aff8f9d7e8f32558e1de14faa0168cc1d Change-Id: I0904bff0f9d80892d0db2ebb590c24fb862f2418 --- diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index cbd62a97df..099d278f0c 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -243,12 +243,6 @@ class ApiParse extends ApiBase { if ( $params['onlypst'] ) { // Build a result and bail out $result_array = []; - if ( $this->contentIsDeleted ) { - $result_array['textdeleted'] = true; - } - if ( $this->contentIsSuppressed ) { - $result_array['textsuppressed'] = true; - } $result_array['text'] = $this->pstContent->serialize( $format ); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; if ( isset( $prop['wikitext'] ) ) { @@ -400,8 +394,8 @@ class ApiParse extends ApiBase { } if ( isset( $prop['displaytitle'] ) ) { - $result_array['displaytitle'] = $p_result->getDisplayTitle() ?: - $titleObj->getPrefixedText(); + $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false + ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText(); } if ( isset( $prop['headitems'] ) ) { @@ -490,12 +484,7 @@ class ApiParse extends ApiBase { } $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS ); - $dom = $wgParser->preprocessToDom( $this->content->getNativeData() ); - if ( is_callable( [ $dom, 'saveXML' ] ) ) { - $xml = $dom->saveXML(); - } else { - $xml = $dom->__toString(); - } + $xml = $wgParser->preprocessToDom( $this->content->getNativeData() )->__toString(); $result_array['parsetree'] = $xml; $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree'; } @@ -578,7 +567,7 @@ class ApiParse extends ApiBase { } else { $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() ); if ( !$this->content ) { - $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] ); + $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] ); } } $this->contentIsDeleted = $isDeleted; @@ -602,7 +591,7 @@ class ApiParse extends ApiBase { $pout = $page->getParserOutput( $popts, $revId, $suppressCache ); } if ( !$pout ) { - $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); + $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); // @codeCoverageIgnore } return $pout; diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php index e236437722..a04271f602 100644 --- a/tests/phpunit/includes/api/ApiParseTest.php +++ b/tests/phpunit/includes/api/ApiParseTest.php @@ -13,48 +13,136 @@ class ApiParseTest extends ApiTestCase { protected static $revIds = []; public function addDBDataOnce() { - $user = static::getTestSysop()->getUser(); $title = Title::newFromText( __CLASS__ ); - $page = WikiPage::factory( $title ); - $status = $page->doEditContent( - ContentHandler::makeContent( 'Test for revdel', $title, CONTENT_MODEL_WIKITEXT ), - __METHOD__ . ' Test for revdel', 0, false, $user - ); - if ( !$status->isOK() ) { - $this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) ); - } + $status = $this->editPage( __CLASS__, 'Test for revdel' ); self::$pageId = $status->value['revision']->getPage(); self::$revIds['revdel'] = $status->value['revision']->getId(); - $status = $page->doEditContent( - ContentHandler::makeContent( 'Test for oldid', $title, CONTENT_MODEL_WIKITEXT ), - __METHOD__ . ' Test for oldid', 0, false, $user - ); - if ( !$status->isOK() ) { - $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); - } + $status = $this->editPage( __CLASS__, 'Test for suppressed' ); + self::$revIds['suppressed'] = $status->value['revision']->getId(); + + $status = $this->editPage( __CLASS__, 'Test for oldid' ); self::$revIds['oldid'] = $status->value['revision']->getId(); - $status = $page->doEditContent( - ContentHandler::makeContent( 'Test for latest', $title, CONTENT_MODEL_WIKITEXT ), - __METHOD__ . ' Test for latest', 0, false, $user + $status = $this->editPage( __CLASS__, 'Test for latest' ); + self::$revIds['latest'] = $status->value['revision']->getId(); + + $this->revisionDelete( self::$revIds['revdel'] ); + $this->revisionDelete( + self::$revIds['suppressed'], + [ Revision::DELETED_TEXT => 1, Revision::DELETED_RESTRICTED => 1 ] ); - if ( !$status->isOK() ) { - $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); + + Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason + } + + /** + * Assert that the given result of calling $this->doApiRequest() with + * action=parse resulted in $html, accounting for the boilerplate that the + * parser adds around the parsed page. Also asserts that warnings match + * the provided $warning. + * + * @param string $html Expected HTML + * @param array $res Returned from doApiRequest() + * @param string|null $warnings Exact value of expected warnings, null for + * no warnings + */ + protected function assertParsedTo( $expected, array $res, $warnings = null ) { + $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertSame' ] ); + } + + /** + * Same as above, but asserts that the HTML matches a regexp instead of a + * literal string match. + * + * @param string $html Expected HTML + * @param array $res Returned from doApiRequest() + * @param string|null $warnings Exact value of expected warnings, null for + * no warnings + */ + protected function assertParsedToRegExp( $expected, array $res, $warnings = null ) { + $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertRegExp' ] ); + } + + private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) { + $html = $res[0]['parse']['text']; + + $expectedStart = '
'; + $this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) ); + + $html = substr( $html, strlen( $expectedStart ) ); + + if ( $res[1]->getBool( 'disablelimitreport' ) ) { + $expectedEnd = "
"; + $this->assertSame( $expectedEnd, substr( $html, -strlen( $expectedEnd ) ) ); + + $html = substr( $html, 0, strlen( $html ) - strlen( $expectedEnd ) ); + } else { + $expectedEnd = '#\n)\n' . + ')\n' . + '(\n)\n)?$#s'; + $this->assertRegExp( $expectedEnd, $html ); + + $html = preg_replace( $expectedEnd, '', $html ); } - self::$revIds['latest'] = $status->value['revision']->getId(); - RevisionDeleter::createList( - 'revision', RequestContext::getMain(), $title, [ self::$revIds['revdel'] ] - )->setVisibility( [ - 'value' => [ - Revision::DELETED_TEXT => 1, + call_user_func( $callback, $expected, $html ); + + if ( $warnings === null ) { + $this->assertCount( 1, $res[0] ); + } else { + $this->assertCount( 2, $res[0] ); + // This deliberately fails if there are extra warnings + $this->assertSame( [ 'parse' => [ 'warnings' => $warnings ] ], $res[0]['warnings'] ); + } + } + + /** + * Set up an interwiki entry for testing. + */ + protected function setupInterwiki() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( + 'interwiki', + [ + 'iw_prefix' => 'madeuplanguage', + 'iw_url' => "https://example.com/wiki/$1", + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => false, ], - 'comment' => 'Test for revdel', - ] ); + __METHOD__, + 'IGNORE' + ); - Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason + $this->setMwGlobals( 'wgExtraInterlanguageLinkPrefixes', [ 'madeuplanguage' ] ); + $this->tablesUsed[] = 'interwiki'; + } + + /** + * Set up a skin for testing. + * + * @todo Should this code be in MediaWikiTestCase or something? + */ + protected function setupSkin() { + $factory = new SkinFactory(); + $factory->register( 'testing', 'Testing', function () { + $skin = $this->getMockBuilder( SkinFallback::class ) + ->setMethods( [ 'getDefaultModules', 'setupSkinUserCss' ] ) + ->getMock(); + $skin->expects( $this->once() )->method( 'getDefaultModules' ) + ->willReturn( [ + 'core' => [ 'foo', 'bar' ], + 'content' => [ 'baz' ] + ] ); + $skin->expects( $this->once() )->method( 'setupSkinUserCss' ) + ->will( $this->returnCallback( function ( OutputPage $out ) { + $out->addModuleStyles( 'foo.styles' ); + } ) ); + return $skin; + } ); + $this->setService( 'SkinFactory', $factory ); } public function testParseByName() { @@ -62,14 +150,14 @@ class ApiParseTest extends ApiTestCase { 'action' => 'parse', 'page' => __CLASS__, ] ); - $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + $this->assertParsedTo( "

Test for latest\n

", $res ); $res = $this->doApiRequest( [ 'action' => 'parse', 'page' => __CLASS__, 'disablelimitreport' => 1, ] ); - $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + $this->assertParsedTo( "

Test for latest\n

", $res ); } public function testParseById() { @@ -77,7 +165,7 @@ class ApiParseTest extends ApiTestCase { 'action' => 'parse', 'pageid' => self::$pageId, ] ); - $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + $this->assertParsedTo( "

Test for latest\n

", $res ); } public function testParseByOldId() { @@ -85,36 +173,46 @@ class ApiParseTest extends ApiTestCase { 'action' => 'parse', 'oldid' => self::$revIds['oldid'], ] ); - $this->assertContains( 'Test for oldid', $res[0]['parse']['text'] ); + $this->assertParsedTo( "

Test for oldid\n

", $res ); $this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] ); $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] ); } - public function testParseRevDel() { - $user = static::getTestUser()->getUser(); - $sysop = static::getTestSysop()->getUser(); - - try { - $this->doApiRequest( [ - 'action' => 'parse', - 'oldid' => self::$revIds['revdel'], - ], null, null, $user ); - $this->fail( "API did not return an error as expected" ); - } catch ( ApiUsageException $ex ) { - $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'permissiondenied' ), - "API failed with error 'permissiondenied'" ); - } - + public function testRevDel() { $res = $this->doApiRequest( [ 'action' => 'parse', 'oldid' => self::$revIds['revdel'], - ], null, null, $sysop ); - $this->assertContains( 'Test for revdel', $res[0]['parse']['text'] ); + ] ); + + $this->assertParsedTo( "

Test for revdel\n

", $res ); $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] ); $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] ); } - public function testParseNonexistentPage() { + public function testRevDelNoPermission() { + $this->setExpectedException( ApiUsageException::class, + "You don't have permission to view deleted revision text." ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => self::$revIds['revdel'], + ], null, null, static::getTestUser()->getUser() ); + } + + public function testSuppressed() { + $this->setGroupPermissions( 'sysop', 'viewsuppressed', true ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => self::$revIds['suppressed'] + ] ); + + $this->assertParsedTo( "

Test for suppressed\n

", $res ); + $this->assertArrayHasKey( 'textsuppressed', $res[0]['parse'] ); + $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] ); + } + + public function testNonexistentPage() { try { $this->doApiRequest( [ 'action' => 'parse', @@ -130,24 +228,446 @@ class ApiParseTest extends ApiTestCase { } } - public function testSkinModules() { - $factory = new SkinFactory(); - $factory->register( 'testing', 'Testing', function () { - $skin = $this->getMockBuilder( SkinFallback::class ) - ->setMethods( [ 'getDefaultModules', 'setupSkinUserCss' ] ) - ->getMock(); - $skin->expects( $this->once() )->method( 'getDefaultModules' ) - ->willReturn( [ - 'core' => [ 'foo', 'bar' ], - 'content' => [ 'baz' ] - ] ); - $skin->expects( $this->once() )->method( 'setupSkinUserCss' ) - ->will( $this->returnCallback( function ( OutputPage $out ) { - $out->addModuleStyles( 'foo.styles' ); - } ) ); - return $skin; - } ); - $this->setService( 'SkinFactory', $factory ); + public function testTitleProvided() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => 'Some interesting page', + 'text' => '{{PAGENAME}} has attracted my attention', + ] ); + + $this->assertParsedTo( "

Some interesting page has attracted my attention\n

", $res ); + } + + public function testSection() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, + "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'section' => 1, + ] ); + + $this->assertParsedToRegExp( '!

.*Section 1.*

\n

Content 1\n

!', $res ); + } + + public function testInvalidSection() { + $this->setExpectedException( ApiUsageException::class, + 'The "section" parameter must be a valid section ID or "new".' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'section' => 'T-new', + ] ); + } + + public function testSectionNoContent() { + $name = ucfirst( __FUNCTION__ ); + + $status = $this->editPage( $name, + "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" ); + + $this->setExpectedException( ApiUsageException::class, + "Missing content for page ID {$status->value['revision']->getPage()}." ); + + $this->db->delete( 'revision', [ 'rev_id' => $status->value['revision']->getId() ] ); + + // Suppress warning in WikiPage::getContentModel + Wikimedia\suppressWarnings(); + try { + $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'section' => 1, + ] ); + } finally { + Wikimedia\restoreWarnings(); + } + } + + public function testNewSectionWithPage() { + $this->setExpectedException( ApiUsageException::class, + '"section=new" cannot be combined with the "oldid", "pageid" or "page" ' . + 'parameters. Please use "title" and "text".' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'page' => __CLASS__, + 'section' => 'new', + ] ); + } + + public function testNonexistentOldId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no revision with ID 2147483647.' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => pow( 2, 31 ) - 1, + ] ); + } + + public function testUnfollowedRedirect() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "#REDIRECT [[$name 2]]" ); + $this->editPage( "$name 2", "Some ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + ] ); + + // Can't use assertParsedTo because the parser output is different for + // redirects + $this->assertRegExp( "/Redirect to:.*$name 2/", $res[0]['parse']['text'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testFollowedRedirect() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "#REDIRECT [[$name 2]]" ); + $this->editPage( "$name 2", "Some ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'redirects' => true, + ] ); + + $this->assertParsedTo( "

Some text\n

", $res ); + } + + public function testFollowedRedirectById() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->editPage( $name, "#REDIRECT [[$name 2]]" )->value['revision']->getPage(); + $this->editPage( "$name 2", "Some ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pageid' => $id, + 'redirects' => true, + ] ); + + $this->assertParsedTo( "

Some text\n

", $res ); + } + + public function testInvalidTitle() { + $this->setExpectedException( ApiUsageException::class, 'Bad title "|".' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'title' => '|', + ] ); + } + + public function testTitleWithNonexistentRevId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no revision with ID 2147483647.' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'revid' => pow( 2, 31 ) - 1, + ] ); + } + + public function testTitleWithNonMatchingRevId() { + $name = ucfirst( __FUNCTION__ ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => $name, + 'revid' => self::$revIds['latest'], + 'text' => 'Some text', + ] ); + + $this->assertParsedTo( "

Some text\n

", $res, + 'r' . self::$revIds['latest'] . " is not a revision of $name." ); + } + + public function testRevId() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'revid' => self::$revIds['latest'], + 'text' => 'My revid is {{REVISIONID}}!', + ] ); + + $this->assertParsedTo( "

My revid is " . self::$revIds['latest'] . "!\n

", $res ); + } + + public function testTitleNoText() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => 'Special:AllPages', + ] ); + + $this->assertParsedTo( '', $res, + '"title" used without "text", and parsed page properties were requested. ' . + 'Did you mean to use "page" instead of "title"?' ); + } + + public function testRevidNoText() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'revid' => self::$revIds['latest'], + ] ); + + $this->assertParsedTo( '', $res, + '"revid" used without "text", and parsed page properties were requested. ' . + 'Did you mean to use "oldid" instead of "revid"?' ); + } + + public function testTextNoContentModel() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Some ''text''", + ] ); + + $this->assertParsedTo( "

Some text\n

", $res, + 'No "title" or "contentmodel" was given, assuming wikitext.' ); + } + + public function testSerializationError() { + $this->setExpectedException( APIUsageException::class, + 'Content serialization failed: Could not unserialize content' ); + + $this->mergeMwGlobalArrayValue( 'wgContentHandlers', + [ 'testing-serialize-error' => 'DummySerializeErrorContentHandler' ] ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Some ''text''", + 'contentmodel' => 'testing-serialize-error', + ] ); + } + + public function testNewSection() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'section' => 'new', + 'sectiontitle' => 'Title', + 'text' => 'Content', + ] ); + + $this->assertParsedToRegExp( '!

.*Title.*

\n

Content\n

!', $res ); + } + + public function testExistingSection() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'section' => 1, + 'text' => "Intro\n\n== Section 1 ==\n\nContent\n\n== Section 2 ==\n\nMore content", + ] ); + + $this->assertParsedToRegExp( '!

.*Section 1.*

\n

Content\n

!', $res ); + } + + public function testNoPst() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Template:$name", "Template ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "{{subst:$name}}", + 'contentmodel' => 'wikitext', + ] ); + + $this->assertParsedTo( "

{{subst:$name}}\n

", $res ); + } + + public function testPst() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Template:$name", "Template ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pst' => '', + 'text' => "{{subst:$name}}", + 'contentmodel' => 'wikitext', + 'prop' => 'text|wikitext', + ] ); + + $this->assertParsedTo( "

Template text\n

", $res ); + $this->assertSame( "{{subst:$name}}", $res[0]['parse']['wikitext'] ); + } + + public function testOnlyPst() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Template:$name", "Template ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'onlypst' => '', + 'text' => "{{subst:$name}}", + 'contentmodel' => 'wikitext', + 'prop' => 'text|wikitext', + 'summary' => 'Summary', + ] ); + + $this->assertSame( + [ 'parse' => [ + 'text' => "Template ''text''", + 'wikitext' => "{{subst:$name}}", + 'parsedsummary' => 'Summary', + ] ], + $res[0] + ); + } + + public function testHeadHtml() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => __CLASS__, + 'prop' => 'headhtml', + ] ); + + // Just do a rough sanity check + $this->assertRegExp( '#.*assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testCategoriesHtml() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "[[Category:$name]]" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'prop' => 'categorieshtml', + ] ); + + $this->assertRegExp( "#Category.*Category:$name.*$name#", + $res[0]['parse']['categorieshtml'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testEffectiveLangLinks() { + $hookRan = false; + $this->setTemporaryHook( 'LanguageLinks', + function () use ( &$hookRan ) { + $hookRan = true; + } + ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => '[[zh:' . __CLASS__ . ']]', + 'effectivelanglinks' => '', + ] ); + + $this->assertTrue( $hookRan ); + $this->assertSame( 'The parameter "effectivelanglinks" has been deprecated.', + $res[0]['warnings']['parse']['warnings'] ); + } + + /** + * @param array $arr Extra params to add to API request + */ + private function doTestLangLinks( array $arr = [] ) { + $this->setupInterwiki(); + + $res = $this->doApiRequest( array_merge( [ + 'action' => 'parse', + 'title' => 'Omelette', + 'text' => '[[madeuplanguage:Omelette]]', + 'prop' => 'langlinks', + ], $arr ) ); + + $langLinks = $res[0]['parse']['langlinks']; + + $this->assertCount( 1, $langLinks ); + $this->assertSame( 'madeuplanguage', $langLinks[0]['lang'] ); + $this->assertSame( 'Omelette', $langLinks[0]['title'] ); + $this->assertSame( 'https://example.com/wiki/Omelette', $langLinks[0]['url'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testLangLinks() { + $this->doTestLangLinks(); + } + + public function testLangLinksWithSkin() { + $this->setupSkin(); + $this->doTestLangLinks( [ 'useskin' => 'testing' ] ); + } + + public function testHeadItems() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => '', + 'prop' => 'headitems', + ] ); + + $this->assertSame( [], $res[0]['parse']['headitems'] ); + $this->assertSame( + '"prop=headitems" is deprecated since MediaWiki 1.28. ' . + 'Use "prop=headhtml" when creating new HTML documents, ' . + 'or "prop=modules|jsconfigvars" when updating a document client-side.', + $res[0]['warnings']['parse']['warnings'] + ); + } + + public function testHeadItemsWithSkin() { + $this->setupSkin(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => '', + 'prop' => 'headitems', + 'useskin' => 'testing', + ] ); + + $this->assertSame( [], $res[0]['parse']['headitems'] ); + $this->assertSame( + '"prop=headitems" is deprecated since MediaWiki 1.28. ' . + 'Use "prop=headhtml" when creating new HTML documents, ' . + 'or "prop=modules|jsconfigvars" when updating a document client-side.', + $res[0]['warnings']['parse']['warnings'] + ); + } + + public function testModules() { + $this->setTemporaryHook( 'ParserAfterParse', + function ( $parser ) { + $output = $parser->getOutput(); + $output->addModules( [ 'foo', 'bar' ] ); + $output->addModuleScripts( [ 'baz', 'quuz' ] ); + $output->addModuleStyles( [ 'aaa', 'zzz' ] ); + $output->addJsConfigVars( [ 'x' => 'y', 'z' => -3 ] ); + } + ); + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => 'Content', + 'prop' => 'modules|jsconfigvars|encodedjsconfigvars', + ] ); + + $this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] ); + $this->assertSame( [ 'baz', 'quuz' ], $res[0]['parse']['modulescripts'] ); + $this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] ); + $this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] ); + $this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testModulesWithSkin() { + $this->setupSkin(); $res = $this->doApiRequest( [ 'action' => 'parse', @@ -170,5 +690,160 @@ class ApiParseTest extends ApiTestCase { $res[0]['parse']['modulestyles'], 'resp.parse.modulestyles' ); + $this->assertSame( + [ 'parse' => + [ 'warnings' => + 'Property "modules" was set but not "jsconfigvars" or ' . + '"encodedjsconfigvars". Configuration variables are necessary for ' . + 'proper module usage.' + ] + ], + $res[0]['warnings'] + ); + } + + public function testIndicators() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => + 'BBB!Some textaaa', + 'prop' => 'indicators', + ] ); + + $this->assertSame( + // It seems we return in markup order and not display order + [ 'b' => 'BBB!', 'a' => 'aaa' ], + $res[0]['parse']['indicators'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testIndicatorsWithSkin() { + $this->setupSkin(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => + 'BBB!Some textaaa', + 'prop' => 'indicators', + 'useskin' => 'testing', + ] ); + + $this->assertSame( + // Now we return in display order rather than markup order + [ 'a' => 'aaa', 'b' => 'BBB!' ], + $res[0]['parse']['indicators'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testIwlinks() { + $this->setupInterwiki(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => 'Omelette', + 'text' => '[[:madeuplanguage:Omelette]][[madeuplanguage:Spaghetti]]', + 'prop' => 'iwlinks', + ] ); + + $iwlinks = $res[0]['parse']['iwlinks']; + + $this->assertCount( 1, $iwlinks ); + $this->assertSame( 'madeuplanguage', $iwlinks[0]['prefix'] ); + $this->assertSame( 'https://example.com/wiki/Omelette', $iwlinks[0]['url'] ); + $this->assertSame( 'madeuplanguage:Omelette', $iwlinks[0]['title'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testLimitReports() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pageid' => self::$pageId, + 'prop' => 'limitreportdata|limitreporthtml', + ] ); + + // We don't bother testing the actual values here + $this->assertInternalType( 'array', $res[0]['parse']['limitreportdata'] ); + $this->assertInternalType( 'string', $res[0]['parse']['limitreporthtml'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testParseTreeNonWikitext() { + $this->setExpectedException( ApiUsageException::class, + '"prop=parsetree" is only supported for wikitext content.' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'text' => '', + 'contentmodel' => 'json', + 'prop' => 'parsetree', + ] ); + } + + public function testParseTree() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Some ''text'' is {{nice|to have|i=think}}", + 'contentmodel' => 'wikitext', + 'prop' => 'parsetree', + ] ); + + // Preprocessor_DOM and Preprocessor_Hash give different results here, + // so we'll accept either + $this->assertRegExp( + '#^Some \'\'text\'\' is $#', + $res[0]['parse']['parsetree'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testDisableTidy() { + $this->setMwGlobals( 'wgTidyConfig', [ 'driver' => 'RemexHtml' ] ); + + // Check that disabletidy doesn't have an effect just because tidying + // doesn't work for some other reason + $res1 = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Mixed up", + 'contentmodel' => 'wikitext', + ] ); + $this->assertParsedTo( "

Mixed up\n

", $res1 ); + + $res2 = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Mixed up", + 'contentmodel' => 'wikitext', + 'disabletidy' => '', + ] ); + + $this->assertParsedTo( "

Mixed up\n

", $res2 ); + } + + public function testFormatCategories() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Category:$name", 'Content' ); + $this->editPage( 'Category:Hidden', '__HIDDENCAT__' ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => "[[Category:$name]][[Category:Foo|Sort me]][[Category:Hidden]]", + 'prop' => 'categories', + ] ); + + $this->assertSame( + [ [ 'sortkey' => '', 'category' => $name ], + [ 'sortkey' => 'Sort me', 'category' => 'Foo', 'missing' => true ], + [ 'sortkey' => '', 'category' => 'Hidden', 'hidden' => true ] ], + $res[0]['parse']['categories'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); } } diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 31c8136bab..6506ea4df0 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -56,6 +56,28 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary ); } + /** + * Revision-deletes a revision. + * + * @param Revision|int $rev Revision to delete + * @param array $value Keys are Revision::DELETED_* flags. Values are 1 to set the bit, 0 to + * clear, -1 to leave alone. (All other values also clear the bit.) + * @param string $comment Deletion comment + */ + protected function revisionDelete( + $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = '' + ) { + if ( is_int( $rev ) ) { + $rev = Revision::newFromId( $rev ); + } + RevisionDeleter::createList( + 'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ] + )->setVisibility( [ + 'value' => $value, + 'comment' => $comment, + ] ); + } + /** * Does the API request and returns the result. *