Add tests for article viewing
authordaniel <daniel.kinzler@wikimedia.de>
Thu, 16 Aug 2018 15:45:10 +0000 (17:45 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 28 Aug 2018 17:22:26 +0000 (19:22 +0200)
Bug: T174035
Change-Id: I06dc78853169812b17e0bde733d9306ccd687564

includes/page/Article.php
tests/phpunit/includes/page/ArticleViewTest.php [new file with mode: 0644]

index 3a7b18e..e90334f 100644 (file)
@@ -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 (file)
index 0000000..d721274
--- /dev/null
@@ -0,0 +1,488 @@
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
+use PHPUnit\Framework\MockObject\MockObject;
+
+/**
+ * @covers \Article::view()
+ */
+class ArticleViewTest extends MediaWikiTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->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 ) );
+       }
+
+}