diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/specials')
18 files changed, 1552 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php b/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php new file mode 100644 index 00000000..1147805c --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php @@ -0,0 +1,118 @@ +<?php + +/** + * @group Database + */ +class ContribsPagerTest extends MediaWikiTestCase { + /** @var ContribsPager */ + private $pager; + + function setUp() { + $context = new RequestContext(); + $this->pager = new ContribsPager( $context, [ + 'start' => '2017-01-01', + 'end' => '2017-02-02', + ] ); + + parent::setUp(); + } + + /** + * @covers ContribsPager::processDateFilter + * @dataProvider dateFilterOptionProcessingProvider + * @param array $inputOpts Input options + * @param array $expectedOpts Expected options + */ + public function testDateFilterOptionProcessing( $inputOpts, $expectedOpts ) { + $this->assertArraySubset( $expectedOpts, ContribsPager::processDateFilter( $inputOpts ) ); + } + + public static function dateFilterOptionProcessingProvider() { + return [ + [ [ 'start' => '2016-05-01', + 'end' => '2016-06-01', + 'year' => null, + 'month' => null ], + [ 'start' => '2016-05-01', + 'end' => '2016-06-01' ] ], + [ [ 'start' => '2016-05-01', + 'end' => '2016-06-01', + 'year' => '', + 'month' => '' ], + [ 'start' => '2016-05-01', + 'end' => '2016-06-01' ] ], + [ [ 'start' => '2016-05-01', + 'end' => '2016-06-01', + 'year' => '2012', + 'month' => '5' ], + [ 'start' => '', + 'end' => '2012-05-31' ] ], + [ [ 'start' => '', + 'end' => '', + 'year' => '2012', + 'month' => '5' ], + [ 'start' => '', + 'end' => '2012-05-31' ] ], + [ [ 'start' => '', + 'end' => '', + 'year' => '2012', + 'month' => '' ], + [ 'start' => '', + 'end' => '2012-12-31' ] ], + ]; + } + + /** + * @covers ContribsPager::isQueryableRange + * @dataProvider provideQueryableRanges + */ + public function testQueryableRanges( $ipRange ) { + $this->setMwGlobals( [ + 'wgRangeContributionsCIDRLimit' => [ + 'IPv4' => 16, + 'IPv6' => 32, + ], + ] ); + + $this->assertTrue( + $this->pager->isQueryableRange( $ipRange ), + "$ipRange is a queryable IP range" + ); + } + + public function provideQueryableRanges() { + return [ + [ '116.17.184.5/32' ], + [ '0.17.184.5/16' ], + [ '2000::/32' ], + [ '2001:db8::/128' ], + ]; + } + + /** + * @covers ContribsPager::isQueryableRange + * @dataProvider provideUnqueryableRanges + */ + public function testUnqueryableRanges( $ipRange ) { + $this->setMwGlobals( [ + 'wgRangeContributionsCIDRLimit' => [ + 'IPv4' => 16, + 'IPv6' => 32, + ], + ] ); + + $this->assertFalse( + $this->pager->isQueryableRange( $ipRange ), + "$ipRange is not a queryable IP range" + ); + } + + public function provideUnqueryableRanges() { + return [ + [ '116.17.184.5/33' ], + [ '0.17.184.5/15' ], + [ '2000::/31' ], + [ '2001:db8::/9999' ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php b/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php new file mode 100644 index 00000000..10c6d04c --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php @@ -0,0 +1,21 @@ +<?php +/** + * Test class for ImageListPagerTest class. + * + * Copyright © 2013, Antoine Musso + * Copyright © 2013, Siebrand Mazeland + * Copyright © 2013, Wikimedia Foundation Inc. + * + * @group Database + */ +class ImageListPagerTest extends MediaWikiTestCase { + /** + * @expectedException MWException + * @expectedExceptionMessage invalid_field + * @covers ImageListPager::formatValue + */ + public function testFormatValuesThrowException() { + $page = new ImageListPager( RequestContext::getMain() ); + $page->formatValue( 'invalid_field', 'invalid_value' ); + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php new file mode 100644 index 00000000..d53a9b8f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Test class to run the query of most of all our special pages + * + * Copyright © 2011, Antoine Musso + * + * @author Antoine Musso + */ + +/** + * @group Database + * @covers QueryPage<extended> + */ +class QueryAllSpecialPagesTest extends MediaWikiTestCase { + + /** + * @var SpecialPage[] + */ + private $queryPages; + + /** List query pages that can not be tested automatically */ + protected $manualTest = [ + LinkSearchPage::class + ]; + + /** + * Pages whose query use the same DB table more than once. + * This is used to skip testing those pages when run against a MySQL backend + * which does not support reopening a temporary table. See upstream bug: + * https://bugs.mysql.com/bug.php?id=10327 + */ + protected $reopensTempTable = [ + BrokenRedirects::class, + ]; + + /** + * Initialize all query page objects + */ + function __construct() { + parent::__construct(); + + foreach ( QueryPage::getPages() as $page ) { + $class = $page[0]; + $name = $page[1]; + if ( !in_array( $class, $this->manualTest ) ) { + $this->queryPages[$class] = SpecialPageFactory::getPage( $name ); + } + } + } + + /** + * Test SQL for each of our QueryPages objects + * @group Database + */ + public function testQuerypageSqlQuery() { + global $wgDBtype; + + foreach ( $this->queryPages as $page ) { + // With MySQL, skips special pages reopening a temporary table + // See https://bugs.mysql.com/bug.php?id=10327 + if ( + $wgDBtype === 'mysql' + && in_array( $page->getName(), $this->reopensTempTable ) + ) { + $this->markTestSkipped( "SQL query for page {$page->getName()} " + . "can not be tested on MySQL backend (it reopens a temporary table)" ); + continue; + } + + $msg = "SQL query for page {$page->getName()} should give a result wrapper object"; + + $result = $page->reallyDoQuery( 50 ); + if ( $result instanceof ResultWrapper ) { + $this->assertTrue( true, $msg ); + } else { + $this->assertFalse( false, $msg ); + } + } + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php new file mode 100644 index 00000000..e0d059fb --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php @@ -0,0 +1,25 @@ +<?php + +/** + * @license GNU GPL v2+ + * @author Addshore + * + * @covers SpecialBlankpage + */ +class SpecialBlankPageTest extends SpecialPageTestBase { + + /** + * Returns a new instance of the special page under test. + * + * @return SpecialPage + */ + protected function newSpecialPage() { + return new SpecialBlankpage(); + } + + public function testHasWikiMsg() { + list( $html, ) = $this->executeSpecialPage(); + $this->assertContains( wfMessage( 'intentionallyblankpage' )->text(), $html ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialBooksourcesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialBooksourcesTest.php new file mode 100644 index 00000000..9c71261e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialBooksourcesTest.php @@ -0,0 +1,51 @@ +<?php +class SpecialBooksourcesTest extends SpecialPageTestBase { + public static function provideISBNs() { + return [ + [ '978-0-300-14424-6', true ], + [ '0-14-020652-3', true ], + [ '020652-3', false ], + [ '9781234567897', true ], + [ '1-4133-0454-0', true ], + [ '978-1413304541', true ], + [ '0136091814', true ], + [ '0136091812', false ], + [ '9780136091813', true ], + [ '9780136091817', false ], + [ '123456789X', true ], + + // T69021 + [ '1413304541', false ], + [ '141330454X', false ], + [ '1413304540', true ], + [ '14133X4540', false ], + [ '97814133X4541', false ], + [ '978035642615X', false ], + [ '9781413304541', true ], + [ '9780356426150', true ], + ]; + } + + /** + * @covers SpecialBookSources::isValidISBN + * @dataProvider provideISBNs + */ + public function testIsValidISBN( $isbn, $isValid ) { + $this->assertSame( $isValid, SpecialBookSources::isValidISBN( $isbn ) ); + } + + protected function newSpecialPage() { + return new SpecialBookSources(); + } + + /** + * @covers SpecialBookSources::execute + */ + public function testExecute() { + list( $html, ) = $this->executeSpecialPage( 'Invalid', null, 'qqx' ); + $this->assertContains( '(booksources-invalid-isbn)', $html ); + list( $html, ) = $this->executeSpecialPage( '0-7475-3269-9', null, 'qqx' ); + $this->assertNotContains( '(booksources-invalid-isbn)', $html ); + $this->assertContains( '(booksources-text)', $html ); + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php new file mode 100644 index 00000000..05a63dbc --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php @@ -0,0 +1,50 @@ +<?php + +/** + * @author Addshore + * + * @group Database + * + * @covers SpecialEditWatchlist + */ +class SpecialEditWatchlistTest extends SpecialPageTestBase { + + /** + * Returns a new instance of the special page under test. + * + * @return SpecialPage + */ + protected function newSpecialPage() { + return new SpecialEditWatchlist(); + } + + public function testNotLoggedIn_throwsException() { + $this->setExpectedException( UserNotLoggedIn::class ); + $this->executeSpecialPage(); + } + + public function testRootPage_displaysExplanationMessage() { + $user = new TestUser( __METHOD__ ); + list( $html, ) = $this->executeSpecialPage( '', null, 'qqx', $user->getUser() ); + $this->assertContains( '(watchlistedit-normal-explain)', $html ); + } + + public function testClearPage_hasClearButtonForm() { + $user = new TestUser( __METHOD__ ); + list( $html, ) = $this->executeSpecialPage( 'clear', null, 'qqx', $user->getUser() ); + $this->assertRegExp( + '/<form class="mw-htmlform" action=".*?Special:EditWatchlist\/clear" method="post">/', + $html + ); + } + + public function testEditRawPage_hasTitlesBox() { + $user = new TestUser( __METHOD__ ); + list( $html, ) = $this->executeSpecialPage( 'raw', null, 'qqx', $user->getUser() ); + $this->assertContains( + '<textarea id="mw-input-wpTitles"', + $html + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php new file mode 100644 index 00000000..4ecb813f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php @@ -0,0 +1,49 @@ +<?php + +/** + * @group Database + * @covers MIMEsearchPage + */ +class SpecialMIMESearchTest extends MediaWikiTestCase { + + /** @var MIMEsearchPage */ + private $page; + + function setUp() { + $this->page = new MIMEsearchPage; + $context = new RequestContext(); + $context->setTitle( Title::makeTitle( NS_SPECIAL, 'MIMESearch' ) ); + $context->setRequest( new FauxRequest() ); + $this->page->setContext( $context ); + + parent::setUp(); + } + + /** + * @dataProvider providerMimeFiltering + * @param string $par Subpage for special page + * @param string $major Major MIME type we expect to look for + * @param string $minor Minor MIME type we expect to look for + */ + function testMimeFiltering( $par, $major, $minor ) { + $this->page->run( $par ); + $qi = $this->page->getQueryInfo(); + $this->assertEquals( $qi['conds']['img_major_mime'], $major ); + if ( $minor !== null ) { + $this->assertEquals( $qi['conds']['img_minor_mime'], $minor ); + } else { + $this->assertArrayNotHasKey( 'img_minor_mime', $qi['conds'] ); + } + $this->assertContains( 'image', $qi['tables'] ); + } + + function providerMimeFiltering() { + return [ + [ 'image/gif', 'image', 'gif' ], + [ 'image/png', 'image', 'png' ], + [ 'application/pdf', 'application', 'pdf' ], + [ 'image/*', 'image', null ], + [ 'multipart/*', 'multipart', null ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php new file mode 100644 index 00000000..84fa71a2 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php @@ -0,0 +1,78 @@ +<?php + +/** + * @group Database + * @covers SpecialMyLanguage + */ +class SpecialMyLanguageTest extends MediaWikiTestCase { + public function addDBDataOnce() { + $titles = [ + 'Page/Another', + 'Page/Another/ar', + 'Page/Another/en', + 'Page/Another/ru', + 'Page/Another/zh-hans', + ]; + foreach ( $titles as $title ) { + $page = WikiPage::factory( Title::newFromText( $title ) ); + if ( $page->getId() == 0 ) { + $page->doEditContent( + new WikitextContent( 'UTContent' ), + 'UTPageSummary', + EDIT_NEW, + false, + User::newFromName( 'UTSysop' ) ); + } + } + } + + /** + * @covers SpecialMyLanguage::findTitle + * @dataProvider provideFindTitle + * @param string $expected + * @param string $subpage + * @param string $langCode + * @param string $userLang + */ + public function testFindTitle( $expected, $subpage, $langCode, $userLang ) { + $this->setMwGlobals( 'wgLanguageCode', $langCode ); + $special = new SpecialMyLanguage(); + $special->getContext()->setLanguage( $userLang ); + // Test with subpages both enabled and disabled + $this->mergeMwGlobalArrayValue( 'wgNamespacesWithSubpages', [ NS_MAIN => true ] ); + $this->assertTitle( $expected, $special->findTitle( $subpage ) ); + $this->mergeMwGlobalArrayValue( 'wgNamespacesWithSubpages', [ NS_MAIN => false ] ); + $this->assertTitle( $expected, $special->findTitle( $subpage ) ); + } + + /** + * @param string $expected + * @param Title|null $title + */ + private function assertTitle( $expected, $title ) { + if ( $title ) { + $title = $title->getPrefixedText(); + } + $this->assertEquals( $expected, $title ); + } + + public static function provideFindTitle() { + // See addDBDataOnce() for page declarations + return [ + // [ $expected, $subpage, $langCode, $userLang ] + [ null, '::Fail', 'en', 'en' ], + [ 'Page/Another', 'Page/Another/en', 'en', 'en' ], + [ 'Page/Another', 'Page/Another', 'en', 'en' ], + [ 'Page/Another/ru', 'Page/Another', 'en', 'ru' ], + [ 'Page/Another', 'Page/Another', 'en', 'es' ], + [ 'Page/Another/zh-hans', 'Page/Another', 'en', 'zh-hans' ], + [ 'Page/Another/zh-hans', 'Page/Another', 'en', 'zh-mo' ], + [ 'Page/Another/en', 'Page/Another', 'de', 'es' ], + [ 'Page/Another/ar', 'Page/Another', 'en', 'ar' ], + [ 'Page/Another/ar', 'Page/Another', 'en', 'arz' ], + [ 'Page/Another/ar', 'Page/Another/de', 'en', 'arz' ], + [ 'Page/Another/ru', 'Page/Another/ru', 'en', 'arz' ], + [ 'Page/Another/ar', 'Page/Another/ru', 'en', 'ar' ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php new file mode 100644 index 00000000..40754063 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php @@ -0,0 +1,146 @@ +<?php + +/** + * @covers SpecialPageData + * @group Database + * @group SpecialPage + * + * @author Daniel Kinzler + */ +class SpecialPageDataTest extends SpecialPageTestBase { + + protected function newSpecialPage() { + $page = new SpecialPageData(); + + // why is this needed? + $page->getContext()->setOutput( new OutputPage( $page->getContext() ) ); + + $page->setRequestHandler( new PageDataRequestHandler() ); + + return $page; + } + + public function provideExecute() { + $cases = []; + + $cases['Empty request'] = [ '', [], [], '!!', 200 ]; + + $cases['Only title specified'] = [ + '', + [ 'target' => 'Helsinki' ], + [], + '!!', + 303, + [ 'Location' => '!.+!' ] + ]; + + $cases['Accept only HTML'] = [ + '', + [ 'target' => 'Helsinki' ], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki$!' ] + ]; + + $cases['Accept only HTML with revid'] = [ + '', + [ + 'target' => 'Helsinki', + 'revision' => '4242', + ], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki(\?|&)oldid=4242!' ] + ]; + + $cases['Nothing specified'] = [ + 'main/Helsinki', + [], + [], + '!!', + 303, + [ 'Location' => '!Helsinki&action=raw!' ] + ]; + + $cases['Nothing specified'] = [ + '/Helsinki', + [], + [], + '!!', + 303, + [ 'Location' => '!Helsinki&action=raw!' ] + ]; + + $cases['Invalid Accept header'] = [ + 'main/Helsinki', + [], + [ 'Accept' => 'text/foobar' ], + '!!', + 406, + [], + ]; + + return $cases; + } + + /** + * @dataProvider provideExecute + * + * @param string $subpage The subpage to request (or '') + * @param array $params Request parameters + * @param array $headers Request headers + * @param string $expRegExp Regex to match the output against. + * @param int $expCode Expected HTTP status code + * @param array $expHeaders Expected HTTP response headers + */ + public function testExecute( + $subpage, + array $params, + array $headers, + $expRegExp, + $expCode = 200, + array $expHeaders = [] + ) { + $request = new FauxRequest( $params ); + $request->response()->header( 'Status: 200 OK', true, 200 ); // init/reset + + foreach ( $headers as $name => $value ) { + $request->setHeader( strtoupper( $name ), $value ); + } + + try { + /* @var FauxResponse $response */ + list( $output, $response ) = $this->executeSpecialPage( $subpage, $request ); + + $this->assertEquals( $expCode, $response->getStatusCode(), "status code" ); + $this->assertRegExp( $expRegExp, $output, "output" ); + + foreach ( $expHeaders as $name => $exp ) { + $value = $response->getHeader( $name ); + $this->assertNotNull( $value, "header: $name" ); + $this->assertInternalType( 'string', $value, "header: $name" ); + $this->assertRegExp( $exp, $value, "header: $name" ); + } + } catch ( HttpError $e ) { + $this->assertEquals( $expCode, $e->getStatusCode(), "status code" ); + $this->assertRegExp( $expRegExp, $e->getHTML(), "error output" ); + } + } + + public function testSpecialPageWithoutParameters() { + $this->setContentLang( Language::factory( 'en' ) ); + $request = new FauxRequest(); + $request->response()->header( 'Status: 200 OK', true, 200 ); // init/reset + + list( $output, ) = $this->executeSpecialPage( '', $request ); + + $this->assertContains( + "Content negotiation applies based on your client's Accept header.", + $output, + "output" + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPageExecutor.php b/www/wiki/tests/phpunit/includes/specials/SpecialPageExecutor.php new file mode 100644 index 00000000..e7cfca7f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPageExecutor.php @@ -0,0 +1,129 @@ +<?php + +/** + * @author Addshore + * + * @since 1.27 + */ +class SpecialPageExecutor { + + /** + * @param SpecialPage $page The special page to execute + * @param string $subPage The subpage parameter to call the page with + * @param WebRequest|null $request Web request that may contain URL parameters, etc + * @param Language|string|null $language The language which should be used in the context + * @param User|null $user The user which should be used in the context of this special page + * + * @throws Exception + * @return array [ string, WebResponse ] A two-elements array containing the HTML output + * generated by the special page as well as the response object. + */ + public function executeSpecialPage( + SpecialPage $page, + $subPage = '', + WebRequest $request = null, + $language = null, + User $user = null + ) { + $context = $this->newContext( $request, $language, $user ); + + $output = new OutputPage( $context ); + $context->setOutput( $output ); + + $page->setContext( $context ); + $output->setTitle( $page->getPageTitle() ); + + $html = $this->getHTMLFromSpecialPage( $page, $subPage ); + $response = $context->getRequest()->response(); + + if ( $response instanceof FauxResponse ) { + $code = $response->getStatusCode(); + + if ( $code > 0 ) { + $response->header( 'Status: ' . $code . ' ' . HttpStatus::getMessage( $code ) ); + } + } + + return [ $html, $response ]; + } + + /** + * @param WebRequest|null $request + * @param Language|string|null $language + * @param User|null $user + * + * @return DerivativeContext + */ + private function newContext( + WebRequest $request = null, + $language = null, + User $user = null + ) { + $context = new DerivativeContext( RequestContext::getMain() ); + + $context->setRequest( $request ?: new FauxRequest() ); + + if ( $language !== null ) { + $context->setLanguage( $language ); + } + + if ( $user !== null ) { + $context->setUser( $user ); + } + + $this->setEditTokenFromUser( $context ); + + return $context; + } + + /** + * If we are trying to edit and no token is set, supply one. + * + * @param DerivativeContext $context + */ + private function setEditTokenFromUser( DerivativeContext $context ) { + $request = $context->getRequest(); + + // Edits via GET are a security issue and should not succeed. On the other hand, not all + // POST requests are edits, but should ignore unused parameters. + if ( !$request->getCheck( 'wpEditToken' ) && $request->wasPosted() ) { + $request->setVal( 'wpEditToken', $context->getUser()->getEditToken() ); + } + } + + /** + * @param SpecialPage $page + * @param string $subPage + * + * @throws Exception + * @return string HTML + */ + private function getHTMLFromSpecialPage( SpecialPage $page, $subPage ) { + ob_start(); + + try { + $page->execute( $subPage ); + + $output = $page->getOutput(); + + if ( $output->getRedirect() !== '' ) { + $output->output(); + $html = ob_get_contents(); + } elseif ( $output->isDisabled() ) { + $html = ob_get_contents(); + } else { + $html = $output->getHTML(); + } + } catch ( Exception $ex ) { + ob_end_clean(); + + // Re-throw exception after "finally" handling because PHP 5.3 doesn't have "finally". + throw $ex; + } + + ob_end_clean(); + + return $html; + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php b/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php new file mode 100644 index 00000000..274a23c4 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php @@ -0,0 +1,72 @@ +<?php + +/** + * Base class for testing special pages. + * + * @since 1.26 + * + * @license GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Daniel Kinzler + * @author Addshore + * @author Thiemo Kreuz + */ +abstract class SpecialPageTestBase extends MediaWikiTestCase { + + private $obLevel; + + protected function setUp() { + parent::setUp(); + + $this->obLevel = ob_get_level(); + } + + protected function tearDown() { + $obLevel = ob_get_level(); + + while ( ob_get_level() > $this->obLevel ) { + ob_end_clean(); + } + + if ( $obLevel !== $this->obLevel ) { + $this->fail( + "Test changed output buffer level: was {$this->obLevel} before test, but $obLevel after test." + ); + } + + parent::tearDown(); + } + + /** + * Returns a new instance of the special page under test. + * + * @return SpecialPage + */ + abstract protected function newSpecialPage(); + + /** + * @param string $subPage The subpage parameter to call the page with + * @param WebRequest|null $request Web request that may contain URL parameters, etc + * @param Language|string|null $language The language which should be used in the context + * @param User|null $user The user which should be used in the context of this special page + * + * @throws Exception + * @return array [ string, WebResponse ] A two-elements array containing the HTML output + * generated by the special page as well as the response object. + */ + protected function executeSpecialPage( + $subPage = '', + WebRequest $request = null, + $language = null, + User $user = null + ) { + return ( new SpecialPageExecutor() )->executeSpecialPage( + $this->newSpecialPage(), + $subPage, + $request, + $language, + $user + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php new file mode 100644 index 00000000..bdfbb62e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php @@ -0,0 +1,58 @@ +<?php +/** + * Test class for SpecialPreferences class. + * + * Copyright © 2013, Antoine Musso + * Copyright © 2013, Wikimedia Foundation Inc. + */ + +/** + * @group Preferences + * @group Database + * + * @covers SpecialPreferences + */ +class SpecialPreferencesTest extends MediaWikiTestCase { + + /** + * Make sure a nickname which is longer than $wgMaxSigChars + * is not throwing a fatal error. + * + * Test specifications by Alexandre "ialex" Emsenhuber. + * @todo give this test a real name explaining what is being tested here + */ + public function testBug41337() { + // Set a low limit + $this->setMwGlobals( 'wgMaxSigChars', 2 ); + + $user = $this->createMock( User::class ); + $user->expects( $this->any() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( false ) ); + + # Yeah foreach requires an array, not NULL =( + $user->expects( $this->any() ) + ->method( 'getEffectiveGroups' ) + ->will( $this->returnValue( [] ) ); + + # The mocked user has a long nickname + $user->expects( $this->any() ) + ->method( 'getOption' ) + ->will( $this->returnValueMap( [ + [ 'nickname', null, false, 'superlongnickname' ], + ] + ) ); + + # Forge a request to call the special page + $context = new RequestContext(); + $context->setRequest( new FauxRequest() ); + $context->setUser( $user ); + $context->setTitle( Title::newFromText( 'Test' ) ); + + # Do the call, should not spurt a fatal error. + $special = new SpecialPreferences(); + $special->setContext( $context ); + $this->assertNull( $special->execute( [] ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialRecentchangesTest.php new file mode 100644 index 00000000..0b6962d5 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialRecentchangesTest.php @@ -0,0 +1,52 @@ +<?php + +use Wikimedia\TestingAccessWrapper; + +/** + * Test class for SpecialRecentchanges class + * + * @group Database + * + * @covers SpecialRecentChanges + */ +class SpecialRecentchangesTest extends AbstractChangesListSpecialPageTestCase { + protected function getPage() { + return TestingAccessWrapper::newFromObject( + new SpecialRecentChanges + ); + } + + // Below providers should only be for features specific to + // RecentChanges. Otherwise, it should go in ChangesListSpecialPageTest + + public function provideParseParameters() { + return [ + [ 'limit=123', [ 'limit' => '123' ] ], + + [ '234', [ 'limit' => '234' ] ], + + [ 'days=3', [ 'days' => '3' ] ], + + [ 'days=0.25', [ 'days' => '0.25' ] ], + + [ 'namespace=5', [ 'namespace' => '5' ] ], + + [ 'namespace=5|3', [ 'namespace' => '5|3' ] ], + + [ 'tagfilter=foo', [ 'tagfilter' => 'foo' ] ], + + [ 'tagfilter=foo;bar', [ 'tagfilter' => 'foo;bar' ] ], + ]; + } + + public function validateOptionsProvider() { + return [ + [ + // hidebots=1 is default for Special:RecentChanges + [ 'hideanons' => 1, 'hideliu' => 1 ], + true, + [ 'hideliu' => 1 ], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php new file mode 100644 index 00000000..f0a57266 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -0,0 +1,296 @@ +<?php +use MediaWiki\MediaWikiServices; + +/** + * Test class for SpecialSearch class + * Copyright © 2012, Antoine Musso + * + * @author Antoine Musso + * @group Database + */ +class SpecialSearchTest extends MediaWikiTestCase { + + /** + * @covers SpecialSearch::load + * @dataProvider provideSearchOptionsTests + * @param array $requested Request parameters. For example: + * array( 'ns5' => true, 'ns6' => true). Null to use default options. + * @param array $userOptions User options to test with. For example: + * array('searchNs5' => 1 );. Null to use default options. + * @param string $expectedProfile An expected search profile name + * @param array $expectedNS Expected namespaces + * @param string $message + */ + public function testProfileAndNamespaceLoading( $requested, $userOptions, + $expectedProfile, $expectedNS, $message = 'Profile name and namespaces mismatches!' + ) { + $context = new RequestContext; + $context->setUser( + $this->newUserWithSearchNS( $userOptions ) + ); + /* + $context->setRequest( new FauxRequest( [ + 'ns5'=>true, + 'ns6'=>true, + ] )); + */ + $context->setRequest( new FauxRequest( $requested ) ); + $search = new SpecialSearch(); + $search->setContext( $context ); + $search->load(); + + /** + * Verify profile name and namespace in the same assertion to make + * sure we will be able to fully compare the above code. PHPUnit stop + * after an assertion fail. + */ + $this->assertEquals( + [ /** Expected: */ + 'ProfileName' => $expectedProfile, + 'Namespaces' => $expectedNS, + ], + [ /** Actual: */ + 'ProfileName' => $search->getProfile(), + 'Namespaces' => $search->getNamespaces(), + ], + $message + ); + } + + public static function provideSearchOptionsTests() { + $defaultNS = MediaWikiServices::getInstance()->getSearchEngineConfig()->defaultNamespaces(); + $EMPTY_REQUEST = []; + $NO_USER_PREF = null; + + return [ + /** + * Parameters: + * <Web Request>, <User options> + * Followed by expected values: + * <ProfileName>, <NSList> + * Then an optional message. + */ + [ + $EMPTY_REQUEST, $NO_USER_PREF, + 'default', $defaultNS, + 'T35270: No request nor user preferences should give default profile' + ], + [ + [ 'ns5' => 1 ], $NO_USER_PREF, + 'advanced', [ 5 ], + 'Web request with specific NS should override user preference' + ], + [ + $EMPTY_REQUEST, [ + 'searchNs2' => 1, + 'searchNs14' => 1, + ] + array_fill_keys( array_map( function ( $ns ) { + return "searchNs$ns"; + }, $defaultNS ), 0 ), + 'advanced', [ 2, 14 ], + 'T35583: search with no option should honor User search preferences' + . ' and have all other namespace disabled' + ], + ]; + } + + /** + * Helper to create a new User object with given options + * User remains anonymous though + * @param array|null $opt + */ + function newUserWithSearchNS( $opt = null ) { + $u = User::newFromId( 0 ); + if ( $opt === null ) { + return $u; + } + foreach ( $opt as $name => $value ) { + $u->setOption( $name, $value ); + } + + return $u; + } + + /** + * Verify we do not expand search term in <title> on search result page + * https://gerrit.wikimedia.org/r/4841 + */ + public function testSearchTermIsNotExpanded() { + $this->setMwGlobals( [ + 'wgSearchType' => null, + ] ); + + # Initialize [[Special::Search]] + $ctx = new RequestContext(); + $term = '{{SITENAME}}'; + $ctx->setRequest( new FauxRequest( [ 'search' => $term, 'fulltext' => 1 ] ) ); + $ctx->setTitle( Title::newFromText( 'Special:Search' ) ); + $search = new SpecialSearch(); + $search->setContext( $ctx ); + + # Simulate a user searching for a given term + $search->execute( '' ); + + # Lookup the HTML page title set for that page + $pageTitle = $search + ->getContext() + ->getOutput() + ->getHTMLTitle(); + + # Compare :-] + $this->assertRegExp( + '/' . preg_quote( $term, '/' ) . '/', + $pageTitle, + "Search term '{$term}' should not be expanded in Special:Search <title>" + ); + } + + public function provideRewriteQueryWithSuggestion() { + return [ + [ + 'With suggestion and no rewritten query shows did you mean', + '/Did you mean: <a[^>]+>first suggestion/', + 'first suggestion', + null, + [ Title::newMainPage() ] + ], + + [ + 'With rewritten query informs user of change', + '/Showing results for <a[^>]+>first suggestion/', + 'asdf', + 'first suggestion', + [ Title::newMainPage() ] + ], + + [ + 'When both queries have no results user gets no results', + '/There were no results matching the query/', + 'first suggestion', + 'first suggestion', + [] + ], + ]; + } + + /** + * @dataProvider provideRewriteQueryWithSuggestion + */ + public function testRewriteQueryWithSuggestion( + $message, + $expectRegex, + $suggestion, + $rewrittenQuery, + array $resultTitles + ) { + $results = array_map( function ( $title ) { + return SearchResult::newFromTitle( $title ); + }, $resultTitles ); + + $searchResults = new SpecialSearchTestMockResultSet( + $suggestion, + $rewrittenQuery, + $results + ); + + $mockSearchEngine = $this->mockSearchEngine( $searchResults ); + $search = $this->getMockBuilder( SpecialSearch::class ) + ->setMethods( [ 'getSearchEngine' ] ) + ->getMock(); + $search->expects( $this->any() ) + ->method( 'getSearchEngine' ) + ->will( $this->returnValue( $mockSearchEngine ) ); + + $search->getContext()->setTitle( Title::makeTitle( NS_SPECIAL, 'Search' ) ); + $search->getContext()->setLanguage( Language::factory( 'en' ) ); + $search->load(); + $search->showResults( 'this is a fake search' ); + + $html = $search->getContext()->getOutput()->getHTML(); + foreach ( (array)$expectRegex as $regex ) { + $this->assertRegExp( $regex, $html, $message ); + } + } + + protected function mockSearchEngine( $results ) { + $mock = $this->getMockBuilder( SearchEngine::class ) + ->setMethods( [ 'searchText', 'searchTitle' ] ) + ->getMock(); + + $mock->expects( $this->any() ) + ->method( 'searchText' ) + ->will( $this->returnValue( $results ) ); + + return $mock; + } + + public function testSubPageRedirect() { + $this->setMwGlobals( [ + 'wgScript' => '/w/index.php', + ] ); + + $ctx = new RequestContext; + $sp = Title::newFromText( 'Special:Search/foo_bar' ); + SpecialPageFactory::executePath( $sp, $ctx ); + $url = $ctx->getOutput()->getRedirect(); + // some older versions of hhvm have a bug that doesn't parse relative + // urls with a port, so help it out a little bit. + // https://github.com/facebook/hhvm/issues/7136 + $url = wfExpandUrl( $url, PROTO_CURRENT ); + + $parts = parse_url( $url ); + $this->assertEquals( '/w/index.php', $parts['path'] ); + parse_str( $parts['query'], $query ); + $this->assertEquals( 'Special:Search', $query['title'] ); + $this->assertEquals( 'foo bar', $query['search'] ); + } +} + +class SpecialSearchTestMockResultSet extends SearchResultSet { + protected $results; + protected $suggestion; + + public function __construct( + $suggestion = null, + $rewrittenQuery = null, + array $results = [], + $containedSyntax = false + ) { + $this->suggestion = $suggestion; + $this->rewrittenQuery = $rewrittenQuery; + $this->results = $results; + $this->containedSyntax = $containedSyntax; + } + + public function numRows() { + return count( $this->results ); + } + + public function getTotalHits() { + return $this->numRows(); + } + + public function hasSuggestion() { + return $this->suggestion !== null; + } + + public function getSuggestionQuery() { + return $this->suggestion; + } + + public function getSuggestionSnippet() { + return $this->suggestion; + } + + public function hasRewrittenQuery() { + return $this->rewrittenQuery !== null; + } + + public function getQueryAfterRewrite() { + return $this->rewrittenQuery; + } + + public function getQueryAfterRewriteSnippet() { + return htmlspecialchars( $this->rewrittenQuery ); + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php new file mode 100644 index 00000000..f799b115 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php @@ -0,0 +1,43 @@ +<?php + +/** + * Test class for SpecialShortpages class + * + * @since 1.30 + * + * @license GNU GPL v2+ + */ +class SpecialShortpagesTest extends MediaWikiTestCase { + + /** + * @dataProvider provideGetQueryInfoRespectsContentNs + * @covers ShortPagesPage::getQueryInfo() + */ + public function testGetQueryInfoRespectsContentNS( $contentNS, $blacklistNS, $expectedNS ) { + $this->setMwGlobals( [ + 'wgShortPagesNamespaceBlacklist' => $blacklistNS, + 'wgContentNamespaces' => $contentNS + ] ); + $this->setTemporaryHook( 'ShortPagesQuery', function () { + // empty hook handler + } ); + + $page = new ShortPagesPage(); + $queryInfo = $page->getQueryInfo(); + + $this->assertArrayHasKey( 'conds', $queryInfo ); + $this->assertArrayHasKey( 'page_namespace', $queryInfo[ 'conds' ] ); + $this->assertEquals( $expectedNS, $queryInfo[ 'conds' ][ 'page_namespace' ] ); + } + + public function provideGetQueryInfoRespectsContentNs() { + return [ + [ [ NS_MAIN, NS_FILE ], [], [ NS_MAIN, NS_FILE ] ], + [ [ NS_MAIN, NS_TALK ], [ NS_FILE ], [ NS_MAIN, NS_TALK ] ], + [ [ NS_MAIN, NS_FILE ], [ NS_FILE ], [ NS_MAIN ] ], + // NS_MAIN namespace is always forced + [ [], [ NS_FILE ], [ NS_MAIN ] ] + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php new file mode 100644 index 00000000..80bd365f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Tests for Special:Uncategorizedcategories + */ +class UncategorizedCategoriesPageTest extends MediaWikiTestCase { + /** + * @dataProvider provideTestGetQueryInfoData + * @covers UncategorizedCategoriesPage::getQueryInfo + */ + public function testGetQueryInfo( $msgContent, $expected ) { + $msg = new RawMessage( $msgContent ); + $mockContext = $this->getMockBuilder( RequestContext::class )->getMock(); + $mockContext->method( 'msg' )->willReturn( $msg ); + $special = new UncategorizedCategoriesPage(); + $special->setContext( $mockContext ); + $this->assertEquals( [ + 'tables' => [ + 0 => 'page', + 1 => 'categorylinks', + ], + 'fields' => [ + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title', + ], + 'conds' => [ + 0 => 'cl_from IS NULL', + 'page_namespace' => 14, + 'page_is_redirect' => 0, + ] + $expected, + 'join_conds' => [ + 'categorylinks' => [ + 0 => 'LEFT JOIN', + 1 => 'cl_from = page_id', + ], + ], + ], $special->getQueryInfo() ); + } + + public function provideTestGetQueryInfoData() { + return [ + [ + "* Stubs\n* Test\n* *\n* * test123", + [ 1 => "page_title not in ( 'Stubs','Test','*','*_test123' )" ] + ], + [ + "Stubs\n* Test\n* *\n* * test123", + [ 1 => "page_title not in ( 'Test','*','*_test123' )" ] + ], + [ + "* StubsTest\n* *\n* * test123", + [ 1 => "page_title not in ( 'StubsTest','*','*_test123' )" ] + ], + [ "", [] ], + [ "\n\n\n", [] ], + [ "\n", [] ], + [ "Test\n*Test2", [ 1 => "page_title not in ( 'Test2' )" ] ], + [ "Test", [] ], + [ "*Test\nTest2", [ 1 => "page_title not in ( 'Test' )" ] ], + [ "Test\nTest2", [] ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php new file mode 100644 index 00000000..95026c18 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php @@ -0,0 +1,29 @@ +<?php + +class SpecialUploadTest extends MediaWikiTestCase { + /** + * @covers SpecialUpload::getInitialPageText + * @dataProvider provideGetInitialPageText + */ + public function testGetInitialPageText( $expected, $inputParams ) { + $result = call_user_func_array( [ 'SpecialUpload', 'getInitialPageText' ], $inputParams ); + $this->assertEquals( $expected, $result ); + } + + public function provideGetInitialPageText() { + return [ + [ + 'expect' => "== Summary ==\nthis is a test\n", + 'params' => [ + 'this is a test' + ], + ], + [ + 'expect' => "== Summary ==\nthis is a test\n", + 'params' => [ + "== Summary ==\nthis is a test", + ], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php new file mode 100644 index 00000000..5adbed81 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php @@ -0,0 +1,192 @@ +<?php + +use Wikimedia\TestingAccessWrapper; + +/** + * @author Addshore + * + * @group Database + * + * @covers SpecialWatchlist + */ +class SpecialWatchlistTest extends SpecialPageTestBase { + public function setUp() { + parent::setUp(); + + $this->setTemporaryHook( + 'ChangesListSpecialPageFilters', + null + ); + + $this->setTemporaryHook( + 'SpecialWatchlistQuery', + null + ); + + $this->setTemporaryHook( + 'ChangesListSpecialPageQuery', + null + ); + + $this->setMwGlobals( + 'wgDefaultUserOptions', + [ + 'extendwatchlist' => 1, + 'watchlistdays' => 3.0, + 'watchlisthideanons' => 0, + 'watchlisthidebots' => 0, + 'watchlisthideliu' => 0, + 'watchlisthideminor' => 0, + 'watchlisthideown' => 0, + 'watchlisthidepatrolled' => 0, + 'watchlisthidecategorization' => 1, + 'watchlistreloadautomatically' => 0, + 'watchlistunwatchlinks' => 0, + ] + ); + } + + /** + * Returns a new instance of the special page under test. + * + * @return SpecialPage + */ + protected function newSpecialPage() { + return new SpecialWatchlist(); + } + + public function testNotLoggedIn_throwsException() { + $this->setExpectedException( UserNotLoggedIn::class ); + $this->executeSpecialPage(); + } + + public function testUserWithNoWatchedItems_displaysNoWatchlistMessage() { + $user = new TestUser( __METHOD__ ); + list( $html, ) = $this->executeSpecialPage( '', null, 'qqx', $user->getUser() ); + $this->assertContains( '(nowatchlist)', $html ); + } + + /** + * @dataProvider provideFetchOptionsFromRequest + */ + public function testFetchOptionsFromRequest( $expectedValues, $preferences, $inputParams ) { + $page = TestingAccessWrapper::newFromObject( + $this->newSpecialPage() + ); + + $context = new DerivativeContext( $page->getContext() ); + + $fauxRequest = new FauxRequest( $inputParams, /* $wasPosted= */ false ); + $user = $this->getTestUser()->getUser(); + + foreach ( $preferences as $key => $value ) { + $user->setOption( $key, $value ); + } + + $context->setRequest( $fauxRequest ); + $context->setUser( $user ); + $page->setContext( $context ); + + $page->registerFilters(); + $formOptions = $page->getDefaultOptions(); + $page->fetchOptionsFromRequest( $formOptions ); + + $this->assertArrayEquals( + $expectedValues, + $formOptions->getAllValues(), + /* $ordered= */ false, + /* $named= */ true + ); + } + + public function provideFetchOptionsFromRequest() { + // $defaults and $allFalse are just to make the expected values below + // shorter by hiding the background. + + $page = TestingAccessWrapper::newFromObject( + $this->newSpecialPage() + ); + + $this->setTemporaryHook( + 'ChangesListSpecialPageFilters', + null + ); + + $page->registerFilters(); + + // Does not consider $preferences, just wiki's defaults + $wikiDefaults = $page->getDefaultOptions()->getAllValues(); + + $allFalse = $wikiDefaults; + + foreach ( $allFalse as $key => &$value ) { + if ( $value === true ) { + $value = false; + } + } + + // This is not exposed on the form (only in preferences) so it + // respects the preference. + $allFalse['extended'] = true; + + return [ + [ + [ + 'hideminor' => true, + ] + $wikiDefaults, + [], + [ + 'hideMinor' => 1, + ], + ], + + [ + [ + // First two same as prefs + 'hideminor' => true, + 'hidebots' => false, + + // Second two overriden + 'hideanons' => false, + 'hideliu' => true, + 'userExpLevel' => 'registered' + ] + $wikiDefaults, + [ + 'watchlisthideminor' => 1, + 'watchlisthidebots' => 0, + + 'watchlisthideanons' => 1, + 'watchlisthideliu' => 0, + ], + [ + 'hideanons' => 0, + 'hideliu' => 1, + ], + ], + + // Defaults/preferences for form elements are entirely ignored for + // action=submit and omitted elements become false + [ + [ + 'hideminor' => false, + 'hidebots' => true, + 'hideanons' => false, + 'hideliu' => true, + 'userExpLevel' => 'unregistered' + ] + $allFalse, + [ + 'watchlisthideminor' => 0, + 'watchlisthidebots' => 1, + + 'watchlisthideanons' => 0, + 'watchlisthideliu' => 1, + ], + [ + 'hidebots' => 1, + 'hideliu' => 1, + 'action' => 'submit', + ], + ], + ]; + } +} |