From 9b6a3dcb1df347017b781876a8c860f000515978 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 16 Aug 2018 17:45:10 +0200 Subject: [PATCH] Add tests for article viewing Bug: T174035 Change-Id: I06dc78853169812b17e0bde733d9306ccd687564 --- includes/page/Article.php | 29 +- .../phpunit/includes/page/ArticleViewTest.php | 488 ++++++++++++++++++ 2 files changed, 509 insertions(+), 8 deletions(-) create mode 100644 tests/phpunit/includes/page/ArticleViewTest.php diff --git a/includes/page/Article.php b/includes/page/Article.php index 3a7b18e4c0..e90334fce6 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -33,23 +33,30 @@ use MediaWiki\MediaWikiServices; * moved to separate EditPage and HTMLFileCache classes. */ class Article implements Page { - /** @var IContextSource The context this Article is executed in */ + /** + * @var IContextSource|null The context this Article is executed in. + * If null, REquestContext::getMain() is used. + */ protected $mContext; /** @var WikiPage The WikiPage object of this instance */ protected $mPage; - /** @var ParserOptions ParserOptions object for $wgUser articles */ + /** + * @var ParserOptions|null ParserOptions object for $wgUser articles. + * Initialized by getParserOptions by calling $this->mPage->makeParserOptions(). + */ public $mParserOptions; /** - * @var string Text of the revision we are working on + * @var string|null Text of the revision we are working on * @todo BC cruft */ public $mContent; /** - * @var Content Content of the revision we are working on + * @var Content|null Content of the revision we are working on. + * Initialized by fetchContentObject(). * @since 1.21 */ public $mContentObject; @@ -60,7 +67,7 @@ class Article implements Page { /** @var int|null The oldid of the article that is to be shown, 0 for the current revision */ public $mOldId; - /** @var Title Title from which we were redirected here */ + /** @var Title|null Title from which we were redirected here, if any. */ public $mRedirectedFrom = null; /** @var string|bool URL to redirect to or false if none */ @@ -69,10 +76,16 @@ class Article implements Page { /** @var int Revision ID of revision we are working on */ public $mRevIdFetched = 0; - /** @var Revision Revision we are working on */ + /** + * @var Revision|null Revision we are working on. Initialized by getOldIDFromRequest() + * or fetchContentObject(). + */ public $mRevision = null; - /** @var ParserOutput */ + /** + * @var ParserOutput|null|false The ParserOutput generated for viewing the page, + * initialized by view(). If no ParserOutput could be generated, this is set to false. + */ public $mParserOutput; /** @@ -641,7 +654,7 @@ class Article implements Page { # Note that $this->mParserOutput is the *current*/oldid version output. $pOutput = ( $outputDone instanceof ParserOutput ) ? $outputDone // object fetched by hook - : $this->mParserOutput; + : $this->mParserOutput ?: null; // ParserOutput or null, avoid false # Adjust title for main page & pages with displaytitle if ( $pOutput ) { diff --git a/tests/phpunit/includes/page/ArticleViewTest.php b/tests/phpunit/includes/page/ArticleViewTest.php new file mode 100644 index 0000000000..d7212746b8 --- /dev/null +++ b/tests/phpunit/includes/page/ArticleViewTest.php @@ -0,0 +1,488 @@ +setUserLang( 'qqx' ); + } + + private function getHtml( OutputPage $output ) { + return preg_replace( '//s', '', $output->getHTML() ); + } + + /** + * @param string|Title $title + * @param Content[]|string[] $revisionContents Content of the revisions to create + * (as Content or string). + * @param RevisionRecord[] &$revisions will be filled with the RevisionRecord for $content. + * + * @return WikiPage + * @throws MWException + */ + private function getPage( $title, array $revisionContents = [], array &$revisions = [] ) { + if ( is_string( $title ) ) { + $title = Title::makeTitle( $this->getDefaultWikitextNS(), $title ); + } + + $page = WikiPage::factory( $title ); + + $user = $this->getTestUser()->getUser(); + + foreach ( $revisionContents as $key => $cont ) { + if ( is_string( $cont ) ) { + $cont = new WikitextContent( $cont ); + } + + $u = $page->newPageUpdater( $user ); + $u->setContent( 'main', $cont ); + $rev = $u->saveRevision( CommentStoreComment::newUnsavedComment( 'Rev ' . $key ) ); + + $revisions[ $key ] = $rev; + } + + return $page; + } + + /** + * @covers Article::getOldId() + * @covers Article::getRevIdFetched() + */ + public function testGetOldId() { + $revisions = []; + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); + + $idA = $revisions[1]->getId(); + $idB = $revisions[2]->getId(); + + // oldid in constructor + $article = new Article( $page->getTitle(), $idA ); + $this->assertSame( $idA, $article->getOldID() ); + $article->getRevisionFetched(); + $this->assertSame( $idA, $article->getRevIdFetched() ); + + // oldid 0 in constructor + $article = new Article( $page->getTitle(), 0 ); + $this->assertSame( 0, $article->getOldID() ); + $article->getRevisionFetched(); + $this->assertSame( $idB, $article->getRevIdFetched() ); + + // oldid in request + $article = new Article( $page->getTitle() ); + $context = new RequestContext(); + $context->setRequest( new FauxRequest( [ 'oldid' => $idA ] ) ); + $article->setContext( $context ); + $this->assertSame( $idA, $article->getOldID() ); + $article->getRevisionFetched(); + $this->assertSame( $idA, $article->getRevIdFetched() ); + + // no oldid + $article = new Article( $page->getTitle() ); + $context = new RequestContext(); + $context->setRequest( new FauxRequest( [] ) ); + $article->setContext( $context ); + $this->assertSame( 0, $article->getOldID() ); + $article->getRevisionFetched(); + $this->assertSame( $idB, $article->getRevIdFetched() ); + } + + public function testView() { + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] ); + + $article = new Article( $page->getTitle(), 0 ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'Test B', $this->getHtml( $output ) ); + $this->assertNotContains( 'id="mw-revision-info"', $this->getHtml( $output ) ); + $this->assertNotContains( 'id="mw-revision-nav"', $this->getHtml( $output ) ); + } + + public function testViewCached() { + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] ); + + $po = new ParserOutput( 'Cached Text' ); + + $article = new Article( $page->getTitle(), 0 ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + + $cache = MediaWikiServices::getInstance()->getParserCache(); + $cache->save( $po, $page, $article->getParserOptions() ); + + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'Cached Text', $this->getHtml( $output ) ); + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + $this->assertNotContains( 'Test B', $this->getHtml( $output ) ); + } + + /** + * @covers Article::getRedirectTarget() + */ + public function testViewRedirect() { + $target = Title::makeTitle( $this->getDefaultWikitextNS(), 'Test_Target' ); + $redirectText = '#REDIRECT [[' . $target->getPrefixedText() . ']]'; + + $page = $this->getPage( __METHOD__, [ $redirectText ] ); + + $article = new Article( $page->getTitle(), 0 ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $this->assertNotNull( + $article->getRedirectTarget()->getPrefixedDBkey() + ); + $this->assertSame( + $target->getPrefixedDBkey(), + $article->getRedirectTarget()->getPrefixedDBkey() + ); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'class="redirectText"', $this->getHtml( $output ) ); + $this->assertContains( + '>' . htmlspecialchars( $target->getPrefixedText() ) . '<', + $this->getHtml( $output ) + ); + } + + public function testViewNonText() { + $dummy = $this->getPage( __METHOD__, [ 'Dummy' ] ); + $dummyRev = $dummy->getRevision()->getRevisionRecord(); + $title = $dummy->getTitle(); + + /** @var MockObject|ContentHandler $mockHandler */ + $mockHandler = $this->getMockBuilder( ContentHandler::class ) + ->setMethods( + [ + 'isParserCacheSupported', + 'serializeContent', + 'unserializeContent', + 'makeEmptyContent', + ] + ) + ->setConstructorArgs( [ 'NotText', [ 'application/frobnitz' ] ] ) + ->getMock(); + + $mockHandler->method( 'isParserCacheSupported' ) + ->willReturn( false ); + + $this->setTemporaryHook( + 'ContentHandlerForModelID', + function ( $id, &$handler ) use ( $mockHandler ) { + $handler = $mockHandler; + } + ); + + /** @var MockObject|Content $content */ + $content = $this->getMock( Content::class ); + $content->method( 'getParserOutput' ) + ->willReturn( new ParserOutput( 'Structured Output' ) ); + $content->method( 'getModel' ) + ->willReturn( 'NotText' ); + $content->method( 'getNativeData' ) + ->willReturn( [ (object)[ 'x' => 'stuff' ] ] ); + $content->method( 'copy' ) + ->willReturn( $content ); + + $rev = new MutableRevisionRecord( $title ); + $rev->setId( $dummyRev->getId() ); + $rev->setPageId( $title->getArticleID() ); + $rev->setUser( $dummyRev->getUser() ); + $rev->setComment( $dummyRev->getComment() ); + $rev->setTimestamp( $dummyRev->getTimestamp() ); + + $rev->setContent( 'main', $content ); + + $rev = new Revision( $rev ); + + /** @var MockObject|WikiPage $page */ + $page = $this->getMockBuilder( WikiPage::class ) + ->setMethods( [ 'getRevision', 'getLatest' ] ) + ->setConstructorArgs( [ $title ] ) + ->getMock(); + + $page->method( 'getRevision' ) + ->willReturn( $rev ); + $page->method( 'getLatest' ) + ->willReturn( $rev->getId() ); + + $article = Article::newFromWikiPage( $page, RequestContext::getMain() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'Structured Output', $this->getHtml( $output ) ); + $this->assertNotContains( 'Dummy', $this->getHtml( $output ) ); + } + + public function testViewOfOldRevision() { + $revisions = []; + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); + $idA = $revisions[1]->getId(); + + $article = new Article( $page->getTitle(), $idA ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'Test A', $this->getHtml( $output ) ); + $this->assertContains( 'id="mw-revision-info"', $output->getSubtitle() ); + $this->assertContains( 'id="mw-revision-nav"', $output->getSubtitle() ); + + $this->assertNotContains( 'id="revision-info-current"', $output->getSubtitle() ); + $this->assertNotContains( 'Test B', $this->getHtml( $output ) ); + } + + public function testViewOfCurrentRevision() { + $revisions = []; + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); + $idB = $revisions[2]->getId(); + + $article = new Article( $page->getTitle(), $idB ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'Test B', $this->getHtml( $output ) ); + $this->assertContains( 'id="mw-revision-info-current"', $output->getSubtitle() ); + $this->assertContains( 'id="mw-revision-nav"', $output->getSubtitle() ); + } + + public function testViewOfMissingRevision() { + $revisions = []; + $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ], $revisions ); + $badId = $revisions[1]->getId() + 100; + + $article = new Article( $page->getTitle(), $badId ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'missing-revision: ' . $badId, $this->getHtml( $output ) ); + + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + } + + public function testViewOfDeletedRevision() { + $revisions = []; + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); + $idA = $revisions[1]->getId(); + + $revDelList = new RevDelRevisionList( + RequestContext::getMain(), $page->getTitle(), [ $idA ] + ); + $revDelList->setVisibility( [ + 'value' => [ RevisionRecord::DELETED_TEXT => 1 ], + 'comment' => "Testing", + ] ); + + $article = new Article( $page->getTitle(), $idA ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( '(rev-deleted-text-permission)', $this->getHtml( $output ) ); + + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + $this->assertNotContains( 'Test B', $this->getHtml( $output ) ); + } + + public function testViewMissingPage() { + $page = $this->getPage( __METHOD__ ); + + $article = new Article( $page->getTitle() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) ); + } + + public function testViewDeletedPage() { + $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] ); + $page->doDeleteArticle( 'Test' ); + + $article = new Article( $page->getTitle() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( 'moveddeleted', $this->getHtml( $output ) ); + $this->assertContains( 'logentry-delete-delete', $this->getHtml( $output ) ); + $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) ); + + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + $this->assertNotContains( 'Test B', $this->getHtml( $output ) ); + } + + public function testViewMessagePage() { + $title = Title::makeTitle( NS_MEDIAWIKI, 'Mainpage' ); + $page = $this->getPage( $title ); + + $article = new Article( $page->getTitle() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( + wfMessage( 'mainpage' )->inContentLanguage()->parse(), + $this->getHtml( $output ) + ); + $this->assertNotContains( '(noarticletextanon)', $this->getHtml( $output ) ); + } + + public function testViewMissingUserPage() { + $user = $this->getTestUser()->getUser(); + $user->addToDatabase(); + + $title = Title::makeTitle( NS_USER, $user->getName() ); + + $page = $this->getPage( $title ); + + $article = new Article( $page->getTitle() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) ); + $this->assertNotContains( '(userpage-userdoesnotexist-view)', $this->getHtml( $output ) ); + } + + public function testViewUserPageOfNonexistingUser() { + $user = User::newFromName( 'Testing ' . __METHOD__ ); + + $title = Title::makeTitle( NS_USER, $user->getName() ); + + $page = $this->getPage( $title ); + + $article = new Article( $page->getTitle() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) ); + $this->assertContains( '(userpage-userdoesnotexist-view:', $this->getHtml( $output ) ); + } + + public function testArticleViewHeaderHook() { + $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] ); + + $article = new Article( $page->getTitle(), 0 ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + + $this->setTemporaryHook( + 'ArticleViewHeader', + function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) { + $this->assertSame( $article, $articlePage, '$articlePage' ); + + $outputDone = new ParserOutput( 'Hook Text' ); + $outputDone->setTitleText( 'Hook Title' ); + + $articlePage->getContext()->getOutput()->addParserOutput( $outputDone ); + } + ); + + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + $this->assertContains( 'Hook Text', $this->getHtml( $output ) ); + $this->assertSame( 'Hook Title', $output->getPageTitle() ); + } + + public function testArticleContentViewCustomHook() { + $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] ); + + $article = new Article( $page->getTitle(), 0 ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + + // use ArticleViewHeader hook to bypass the parser cache + $this->setTemporaryHook( + 'ArticleViewHeader', + function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) { + $useParserCache = false; + } + ); + + $this->setTemporaryHook( + 'ArticleContentViewCustom', + function ( Content $content, Title $title, OutputPage $output ) use ( $page ) { + $this->assertSame( $page->getTitle(), $title, '$title' ); + $this->assertSame( 'Test A', $content->getNativeData(), '$content' ); + + $output->addHTML( 'Hook Text' ); + return false; + } + ); + + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + $this->assertContains( 'Hook Text', $this->getHtml( $output ) ); + } + + public function testArticleAfterFetchContentObjectHook() { + $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] ); + + $article = new Article( $page->getTitle(), 0 ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + + // use ArticleViewHeader hook to bypass the parser cache + $this->setTemporaryHook( + 'ArticleViewHeader', + function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) { + $useParserCache = false; + } + ); + + $this->setTemporaryHook( + 'ArticleAfterFetchContentObject', + function ( Article &$articlePage, Content &$content ) use ( $page, $article ) { + $this->assertSame( $article, $articlePage, '$articlePage' ); + $this->assertSame( 'Test A', $content->getNativeData(), '$content' ); + + $content = new WikitextContent( 'Hook Text' ); + } + ); + + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertNotContains( 'Test A', $this->getHtml( $output ) ); + $this->assertContains( 'Hook Text', $this->getHtml( $output ) ); + } + + public function testShowMissingArticleHook() { + $page = $this->getPage( __METHOD__ ); + + $article = new Article( $page->getTitle() ); + $article->getContext()->getOutput()->setTitle( $page->getTitle() ); + + $this->setTemporaryHook( + 'ShowMissingArticle', + function ( Article $articlePage ) use ( $article ) { + $this->assertSame( $article, $articlePage, '$articlePage' ); + + $articlePage->getContext()->getOutput()->addHTML( 'Hook Text' ); + } + ); + + $article->view(); + + $output = $article->getContext()->getOutput(); + $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) ); + $this->assertContains( 'Hook Text', $this->getHtml( $output ) ); + } + +} -- 2.20.1