From 23f69f10ed07c7fbe7d752882a88d55351ce2e3d Mon Sep 17 00:00:00 2001 From: Chad Horohoe Date: Tue, 14 Dec 2010 16:26:35 +0000 Subject: [PATCH] Per wikitech-l discussion: Move tests from maintenance/tests/ to tests/. They're not strictly maintenance scripts, and some people want to do a selective checkout that doesn't include the tests. There's still debate on whether we should include these in the release downloads, but we had a pretty firm consensus to move this. --- tests/parser/extraParserTests.txt | Bin 0 -> 1261 bytes tests/parser/parserTest.inc | 1304 +++ tests/parser/parserTests.txt | 8315 +++++++++++++++++ tests/parser/parserTestsParserHook.php | 46 + tests/parser/parserTestsStaticParserHook.php | 58 + tests/phpunit/Makefile | 76 + tests/phpunit/README | 35 + tests/phpunit/TODO | 15 + tests/phpunit/bootstrap.php | 65 + tests/phpunit/includes/CdbTest.php | 84 + tests/phpunit/includes/ExternalStoreTest.php | 32 + tests/phpunit/includes/ExtraParserTest.php | 31 + tests/phpunit/includes/GlobalTest.php | 380 + tests/phpunit/includes/HttpTest.php | 526 ++ tests/phpunit/includes/IPTest.php | 298 + tests/phpunit/includes/ImageFunctionsTest.php | 48 + .../includes/LanguageConverterTest.php | 128 + tests/phpunit/includes/LicensesTest.php | 14 + tests/phpunit/includes/LocalFileTest.php | 99 + tests/phpunit/includes/MessageTest.php | 50 + tests/phpunit/includes/ParserOptionsTest.php | 36 + .../includes/ResourceLoaderFileModuleTest.php | 15 + tests/phpunit/includes/ResourceLoaderTest.php | 71 + tests/phpunit/includes/RevisionTest.php | 114 + tests/phpunit/includes/SampleTest.php | 95 + tests/phpunit/includes/SanitizerTest.php | 72 + .../includes/SeleniumConfigurationTest.php | 228 + .../includes/SiteConfigurationTest.php | 311 + tests/phpunit/includes/TimeAdjustTest.php | 49 + .../phpunit/includes/TitlePermissionTest.php | 652 ++ tests/phpunit/includes/TitleTest.php | 17 + tests/phpunit/includes/UploadFromUrlTest.php | 353 + tests/phpunit/includes/UploadTest.php | 90 + .../includes/UserIsValidEmailAddrTest.php | 64 + tests/phpunit/includes/XmlTest.php | 179 + tests/phpunit/includes/api/ApiSetup.php | 65 + tests/phpunit/includes/api/ApiTest.php | 227 + tests/phpunit/includes/api/ApiUploadTest.php | 671 ++ tests/phpunit/includes/api/ApiWatchTest.php | 237 + .../includes/api/RandomImageGenerator.php | 287 + .../includes/api/generateRandomImages.php | 25 + .../includes/db/DatabaseSqliteTest.php | 87 + tests/phpunit/includes/db/DatabaseTest.php | 95 + .../includes/parser/MediaWikiParserTest.php | 73 + .../phpunit/includes/parser/ParserHelpers.php | 132 + .../phpunit/includes/search/SearchDbTest.php | 37 + .../includes/search/SearchEngineTest.php | 195 + .../includes/search/SearchUpdateTest.php | 80 + tests/phpunit/install-phpunit.sh | 23 + .../languages/LanguageBe_taraskTest.php | 31 + tests/phpunit/languages/LanguageTest.php | 61 + tests/phpunit/phpunit.php | 31 + tests/phpunit/run-tests.bat | 1 + tests/phpunit/suite.xml | 35 + tests/phpunit/suites/ExtensionsTestSuite.php | 33 + .../phpunit/suites/UploadFromUrlTestSuite.php | 181 + tests/selenium/Selenium.php | 190 + tests/selenium/SeleniumConfig.php | 88 + tests/selenium/SeleniumLoader.php | 9 + tests/selenium/SeleniumServerManager.php | 239 + tests/selenium/SeleniumTestCase.php | 103 + tests/selenium/SeleniumTestConsoleLogger.php | 25 + tests/selenium/SeleniumTestHTMLLogger.php | 36 + tests/selenium/SeleniumTestListener.php | 68 + tests/selenium/SeleniumTestSuite.php | 46 + tests/selenium/data/Wikipedia-logo-v2-de.png | Bin 0 -> 21479 bytes .../selenium_settings.ini.php52.sample | 23 + tests/selenium/selenium_settings.ini.sample | 32 + .../selenium_settings_grid.ini.sample | 14 + .../suites/AddContentToNewPageTestCase.php | 182 + tests/selenium/suites/AddNewPageTestCase.php | 65 + .../selenium/suites/CreateAccountTestCase.php | 114 + .../suites/DeletePageAdminTestCase.php | 89 + .../selenium/suites/EmailPasswordTestCase.php | 81 + .../suites/MediaWikExtraTestSuite.php | 20 + .../selenium/suites/MediaWikiEditorConfig.php | 47 + .../suites/MediaWikiEditorTestSuite.php | 18 + .../suites/MediawikiCoreSmokeTestCase.php | 69 + .../suites/MediawikiCoreSmokeTestSuite.php | 19 + tests/selenium/suites/MovePageTestCase.php | 117 + .../suites/MyContributionsTestCase.php | 76 + tests/selenium/suites/MyWatchListTestCase.php | 73 + tests/selenium/suites/PageDeleteTestSuite.php | 16 + tests/selenium/suites/PageSearchTestCase.php | 105 + tests/selenium/suites/PreviewPageTestCase.php | 53 + tests/selenium/suites/SavePageTestCase.php | 58 + .../selenium/suites/SimpleSeleniumConfig.php | 15 + .../suites/SimpleSeleniumTestCase.php | 30 + .../suites/SimpleSeleniumTestSuite.php | 26 + .../suites/UserPreferencesTestCase.php | 179 + 90 files changed, 18882 insertions(+) create mode 100644 tests/parser/extraParserTests.txt create mode 100644 tests/parser/parserTest.inc create mode 100644 tests/parser/parserTests.txt create mode 100644 tests/parser/parserTestsParserHook.php create mode 100644 tests/parser/parserTestsStaticParserHook.php create mode 100644 tests/phpunit/Makefile create mode 100644 tests/phpunit/README create mode 100644 tests/phpunit/TODO create mode 100644 tests/phpunit/bootstrap.php create mode 100644 tests/phpunit/includes/CdbTest.php create mode 100644 tests/phpunit/includes/ExternalStoreTest.php create mode 100644 tests/phpunit/includes/ExtraParserTest.php create mode 100644 tests/phpunit/includes/GlobalTest.php create mode 100644 tests/phpunit/includes/HttpTest.php create mode 100644 tests/phpunit/includes/IPTest.php create mode 100644 tests/phpunit/includes/ImageFunctionsTest.php create mode 100644 tests/phpunit/includes/LanguageConverterTest.php create mode 100644 tests/phpunit/includes/LicensesTest.php create mode 100644 tests/phpunit/includes/LocalFileTest.php create mode 100644 tests/phpunit/includes/MessageTest.php create mode 100644 tests/phpunit/includes/ParserOptionsTest.php create mode 100644 tests/phpunit/includes/ResourceLoaderFileModuleTest.php create mode 100644 tests/phpunit/includes/ResourceLoaderTest.php create mode 100644 tests/phpunit/includes/RevisionTest.php create mode 100644 tests/phpunit/includes/SampleTest.php create mode 100644 tests/phpunit/includes/SanitizerTest.php create mode 100644 tests/phpunit/includes/SeleniumConfigurationTest.php create mode 100644 tests/phpunit/includes/SiteConfigurationTest.php create mode 100644 tests/phpunit/includes/TimeAdjustTest.php create mode 100644 tests/phpunit/includes/TitlePermissionTest.php create mode 100644 tests/phpunit/includes/TitleTest.php create mode 100644 tests/phpunit/includes/UploadFromUrlTest.php create mode 100644 tests/phpunit/includes/UploadTest.php create mode 100644 tests/phpunit/includes/UserIsValidEmailAddrTest.php create mode 100644 tests/phpunit/includes/XmlTest.php create mode 100644 tests/phpunit/includes/api/ApiSetup.php create mode 100644 tests/phpunit/includes/api/ApiTest.php create mode 100644 tests/phpunit/includes/api/ApiUploadTest.php create mode 100644 tests/phpunit/includes/api/ApiWatchTest.php create mode 100644 tests/phpunit/includes/api/RandomImageGenerator.php create mode 100644 tests/phpunit/includes/api/generateRandomImages.php create mode 100644 tests/phpunit/includes/db/DatabaseSqliteTest.php create mode 100644 tests/phpunit/includes/db/DatabaseTest.php create mode 100644 tests/phpunit/includes/parser/MediaWikiParserTest.php create mode 100644 tests/phpunit/includes/parser/ParserHelpers.php create mode 100644 tests/phpunit/includes/search/SearchDbTest.php create mode 100644 tests/phpunit/includes/search/SearchEngineTest.php create mode 100644 tests/phpunit/includes/search/SearchUpdateTest.php create mode 100755 tests/phpunit/install-phpunit.sh create mode 100644 tests/phpunit/languages/LanguageBe_taraskTest.php create mode 100644 tests/phpunit/languages/LanguageTest.php create mode 100755 tests/phpunit/phpunit.php create mode 100644 tests/phpunit/run-tests.bat create mode 100644 tests/phpunit/suite.xml create mode 100644 tests/phpunit/suites/ExtensionsTestSuite.php create mode 100644 tests/phpunit/suites/UploadFromUrlTestSuite.php create mode 100644 tests/selenium/Selenium.php create mode 100644 tests/selenium/SeleniumConfig.php create mode 100644 tests/selenium/SeleniumLoader.php create mode 100644 tests/selenium/SeleniumServerManager.php create mode 100644 tests/selenium/SeleniumTestCase.php create mode 100644 tests/selenium/SeleniumTestConsoleLogger.php create mode 100644 tests/selenium/SeleniumTestHTMLLogger.php create mode 100644 tests/selenium/SeleniumTestListener.php create mode 100644 tests/selenium/SeleniumTestSuite.php create mode 100644 tests/selenium/data/Wikipedia-logo-v2-de.png create mode 100644 tests/selenium/selenium_settings.ini.php52.sample create mode 100644 tests/selenium/selenium_settings.ini.sample create mode 100644 tests/selenium/selenium_settings_grid.ini.sample create mode 100644 tests/selenium/suites/AddContentToNewPageTestCase.php create mode 100644 tests/selenium/suites/AddNewPageTestCase.php create mode 100644 tests/selenium/suites/CreateAccountTestCase.php create mode 100644 tests/selenium/suites/DeletePageAdminTestCase.php create mode 100644 tests/selenium/suites/EmailPasswordTestCase.php create mode 100644 tests/selenium/suites/MediaWikExtraTestSuite.php create mode 100644 tests/selenium/suites/MediaWikiEditorConfig.php create mode 100644 tests/selenium/suites/MediaWikiEditorTestSuite.php create mode 100644 tests/selenium/suites/MediawikiCoreSmokeTestCase.php create mode 100644 tests/selenium/suites/MediawikiCoreSmokeTestSuite.php create mode 100644 tests/selenium/suites/MovePageTestCase.php create mode 100644 tests/selenium/suites/MyContributionsTestCase.php create mode 100644 tests/selenium/suites/MyWatchListTestCase.php create mode 100644 tests/selenium/suites/PageDeleteTestSuite.php create mode 100644 tests/selenium/suites/PageSearchTestCase.php create mode 100644 tests/selenium/suites/PreviewPageTestCase.php create mode 100644 tests/selenium/suites/SavePageTestCase.php create mode 100644 tests/selenium/suites/SimpleSeleniumConfig.php create mode 100644 tests/selenium/suites/SimpleSeleniumTestCase.php create mode 100644 tests/selenium/suites/SimpleSeleniumTestSuite.php create mode 100644 tests/selenium/suites/UserPreferencesTestCase.php diff --git a/tests/parser/extraParserTests.txt b/tests/parser/extraParserTests.txt new file mode 100644 index 0000000000000000000000000000000000000000..7eab3ea765cbe983c78491526caae5b2bc9942ca GIT binary patch literal 1261 zcmc&zQESvd5WYvvub8G!eYrzH#Bz7wgW!Wo!HN$>WU@QyF1OiPb~jD>@12dbcc-aT zQG`IU%QxRQ^W~dUxHL9_Jm@(aiV!d+ikFl!wARtGI)#hr3bNcKNQQf-;hG`y0XQx5 zm<2o0u_pm!pwT*unKTG9K|>1rL|}!ciM%5Z%3ws5DM~4x!D|-@^({CS`CK|HqR?`L zTaqqdFj5q$;~ODm`F!^L)yo%B+?>Hj?nv;X=xUo;(8`DiST9s0rR->$33M+&89;6q z2Q5@5CzD?FW|WRRI&jwS(z!6)ptE`pg%6+J6*Pxz*{90$*tsf13Xv71#Bp*j)!_XU%X!7_D*%Z3fm%0URUK^jDLg?4XRxVsHoKZRXd*i7d!4$ zkL +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html + +/** + * @todo Make this more independent of the configuration (and if possible the database) + * @todo document + * @file + * @ingroup Testing + */ + +/** + * @ingroup Testing + */ +class ParserTest { + /** + * boolean $color whereas output should be colorized + */ + private $color; + + /** + * boolean $showOutput Show test output + */ + private $showOutput; + + /** + * boolean $useTemporaryTables Use temporary tables for the temporary database + */ + private $useTemporaryTables = true; + + /** + * boolean $databaseSetupDone True if the database has been set up + */ + private $databaseSetupDone = false; + + /** + * Our connection to the database + */ + private $db; + + /** + * string $oldTablePrefix Original table prefix + */ + private $oldTablePrefix; + + private $maxFuzzTestLength = 300; + private $fuzzSeed = 0; + private $memoryLimit = 50; + private $uploadDir = null; + + public $regex = ""; + private $savedGlobals = array(); + /** + * Sets terminal colorization and diff/quick modes depending on OS and + * command-line options (--color and --quick). + */ + public function __construct( $options = array() ) { + # Only colorize output if stdout is a terminal. + $this->color = !wfIsWindows() && posix_isatty( 1 ); + + if ( isset( $options['color'] ) ) { + switch( $options['color'] ) { + case 'no': + $this->color = false; + break; + case 'yes': + default: + $this->color = true; + break; + } + } + + $this->term = $this->color + ? new AnsiTermColorer() + : new DummyTermColorer(); + + $this->showDiffs = !isset( $options['quick'] ); + $this->showProgress = !isset( $options['quiet'] ); + $this->showFailure = !( + isset( $options['quiet'] ) + && ( isset( $options['record'] ) + || isset( $options['compare'] ) ) ); // redundant output + + $this->showOutput = isset( $options['show-output'] ); + + + if ( isset( $options['regex'] ) ) { + if ( isset( $options['record'] ) ) { + echo "Warning: --record cannot be used with --regex, disabling --record\n"; + unset( $options['record'] ); + } + $this->regex = $options['regex']; + } else { + # Matches anything + $this->regex = ''; + } + + $this->setupRecorder( $options ); + $this->keepUploads = isset( $options['keep-uploads'] ); + + if ( isset( $options['seed'] ) ) { + $this->fuzzSeed = intval( $options['seed'] ) - 1; + } + + $this->runDisabled = isset( $options['run-disabled'] ); + + $this->hooks = array(); + $this->functionHooks = array(); + self::setUp(); + } + + static function setUp() { + global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, $wgDeferredUpdateList, + $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, $wgEnableParserCache, + $wgMessageCache, $wgUseDatabaseMessages, $wgMsgCacheExpiry, $parserMemc, + $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo, + $wgThumbnailScriptPath, $wgScriptPath, + $wgArticlePath, $wgStyleSheetPath, $wgScript, $wgStylePath; + + $wgScript = '/index.php'; + $wgScriptPath = '/'; + $wgArticlePath = '/wiki/$1'; + $wgStyleSheetPath = '/skins'; + $wgStylePath = '/skins'; + $wgThumbnailScriptPath = false; + $wgLocalFileRepo = array( + 'class' => 'LocalRepo', + 'name' => 'local', + 'directory' => wfTempDir() . '/test-repo', + 'url' => 'http://example.com/images', + 'deletedDir' => wfTempDir() . '/test-repo/delete', + 'hashLevels' => 2, + 'transformVia404' => false, + ); + $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + + + $wgEnableParserCache = false; + $wgDeferredUpdateList = array(); + $wgMemc = &wfGetMainCache(); + $messageMemc = &wfGetMessageCacheStorage(); + $parserMemc = &wfGetParserCacheStorage(); + + // $wgContLang = new StubContLang; + $wgUser = new User; + $wgLang = new StubUserLang; + $wgOut = new StubObject( 'wgOut', 'OutputPage' ); + $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + $wgRequest = new WebRequest; + + $wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', + array( $messageMemc, $wgUseDatabaseMessages, + $wgMsgCacheExpiry ) ); + if ( $wgStyleDirectory === false ) { + $wgStyleDirectory = "$IP/skins"; + } + + } + + public function setupRecorder ( $options ) { + if ( isset( $options['record'] ) ) { + $this->recorder = new DbTestRecorder( $this ); + $this->recorder->version = isset( $options['setversion'] ) ? + $options['setversion'] : SpecialVersion::getVersion(); + } elseif ( isset( $options['compare'] ) ) { + $this->recorder = new DbTestPreviewer( $this ); + } elseif ( isset( $options['upload'] ) ) { + $this->recorder = new RemoteTestRecorder( $this ); + } else { + $this->recorder = new TestRecorder( $this ); + } + } + + /** + * Remove last character if it is a newline + * @group utility + */ + static public function chomp( $s ) { + if ( substr( $s, -1 ) === "\n" ) { + return substr( $s, 0, -1 ); + } + else { + return $s; + } + } + + /** + * Run a fuzz test series + * Draw input from a set of test files + */ + function fuzzTest( $filenames ) { + $GLOBALS['wgContLang'] = Language::factory( 'en' ); + $dict = $this->getFuzzInput( $filenames ); + $dictSize = strlen( $dict ); + $logMaxLength = log( $this->maxFuzzTestLength ); + $this->setupDatabase(); + ini_set( 'memory_limit', $this->memoryLimit * 1048576 ); + + $numTotal = 0; + $numSuccess = 0; + $user = new User; + $opts = ParserOptions::newFromUser( $user ); + $title = Title::makeTitle( NS_MAIN, 'Parser_test' ); + + while ( true ) { + // Generate test input + mt_srand( ++$this->fuzzSeed ); + $totalLength = mt_rand( 1, $this->maxFuzzTestLength ); + $input = ''; + + while ( strlen( $input ) < $totalLength ) { + $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength; + $hairLength = min( intval( exp( $logHairLength ) ), $dictSize ); + $offset = mt_rand( 0, $dictSize - $hairLength ); + $input .= substr( $dict, $offset, $hairLength ); + } + + $this->setupGlobals(); + $parser = $this->getParser(); + + // Run the test + try { + $parser->parse( $input, $title, $opts ); + $fail = false; + } catch ( Exception $exception ) { + $fail = true; + } + + if ( $fail ) { + echo "Test failed with seed {$this->fuzzSeed}\n"; + echo "Input:\n"; + var_dump( $input ); + echo "\n\n"; + echo "$exception\n"; + } else { + $numSuccess++; + } + + $numTotal++; + $this->teardownGlobals(); + $parser->__destruct(); + + if ( $numTotal % 100 == 0 ) { + $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 ); + echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n"; + if ( $usage > 90 ) { + echo "Out of memory:\n"; + $memStats = $this->getMemoryBreakdown(); + + foreach ( $memStats as $name => $usage ) { + echo "$name: $usage\n"; + } + $this->abort(); + } + } + } + } + + /** + * Get an input dictionary from a set of parser test files + */ + function getFuzzInput( $filenames ) { + $dict = ''; + + foreach ( $filenames as $filename ) { + $contents = file_get_contents( $filename ); + preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches ); + + foreach ( $matches[1] as $match ) { + $dict .= $match . "\n"; + } + } + + return $dict; + } + + /** + * Get a memory usage breakdown + */ + function getMemoryBreakdown() { + $memStats = array(); + + foreach ( $GLOBALS as $name => $value ) { + $memStats['$' . $name] = strlen( serialize( $value ) ); + } + + $classes = get_declared_classes(); + + foreach ( $classes as $class ) { + $rc = new ReflectionClass( $class ); + $props = $rc->getStaticProperties(); + $memStats[$class] = strlen( serialize( $props ) ); + $methods = $rc->getMethods(); + + foreach ( $methods as $method ) { + $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) ); + } + } + + $functions = get_defined_functions(); + + foreach ( $functions['user'] as $function ) { + $rf = new ReflectionFunction( $function ); + $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) ); + } + + asort( $memStats ); + + return $memStats; + } + + function abort() { + $this->abort(); + } + + /** + * Run a series of tests listed in the given text files. + * Each test consists of a brief description, wikitext input, + * and the expected HTML output. + * + * Prints status updates on stdout and counts up the total + * number and percentage of passed tests. + * + * @param $filenames Array of strings + * @return Boolean: true if passed all tests, false if any tests failed. + */ + public function runTestsFromFiles( $filenames ) { + $ok = false; + $GLOBALS['wgContLang'] = Language::factory( 'en' ); + $this->recorder->start(); + try { + $this->setupDatabase(); + $ok = true; + + foreach ( $filenames as $filename ) { + $tests = new TestFileIterator( $filename, $this ); + $ok = $this->runTests( $tests ) && $ok; + } + + $this->teardownDatabase(); + $this->recorder->report(); + } catch (DBError $e) { + echo $e->getMessage(); + } + $this->recorder->end(); + + return $ok; + } + + function runTests( $tests ) { + $ok = true; + + foreach ( $tests as $t ) { + $result = + $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] ); + $ok = $ok && $result; + $this->recorder->record( $t['test'], $result ); + } + + if ( $this->showProgress ) { + print "\n"; + } + + return $ok; + } + + /** + * Get a Parser object + */ + function getParser( $preprocessor = null ) { + global $wgParserConf; + + $class = $wgParserConf['class']; + $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf ); + + foreach ( $this->hooks as $tag => $callback ) { + $parser->setHook( $tag, $callback ); + } + + foreach ( $this->functionHooks as $tag => $bits ) { + list( $callback, $flags ) = $bits; + $parser->setFunctionHook( $tag, $callback, $flags ); + } + + wfRunHooks( 'ParserTestParser', array( &$parser ) ); + + return $parser; + } + + /** + * Run a given wikitext input through a freshly-constructed wiki parser, + * and compare the output against the expected results. + * Prints status and explanatory messages to stdout. + * + * @param $desc String: test's description + * @param $input String: wikitext to try rendering + * @param $result String: result to output + * @param $opts Array: test's options + * @param $config String: overrides for global variables, one per line + * @return Boolean + */ + public function runTest( $desc, $input, $result, $opts, $config ) { + if ( $this->showProgress ) { + $this->showTesting( $desc ); + } + + $opts = $this->parseOptions( $opts ); + $this->setupGlobals( $opts, $config ); + + $user = new User(); + $options = ParserOptions::newFromUser( $user ); + + if ( isset( $opts['title'] ) ) { + $titleText = $opts['title']; + } + else { + $titleText = 'Parser test'; + } + + $local = isset( $opts['local'] ); + $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; + $parser = $this->getParser( $preprocessor ); + $title = Title::newFromText( $titleText ); + + if ( isset( $opts['pst'] ) ) { + $out = $parser->preSaveTransform( $input, $title, $user, $options ); + } elseif ( isset( $opts['msg'] ) ) { + $out = $parser->transformMsg( $input, $options ); + } elseif ( isset( $opts['section'] ) ) { + $section = $opts['section']; + $out = $parser->getSection( $input, $section ); + } elseif ( isset( $opts['replace'] ) ) { + $section = $opts['replace'][0]; + $replace = $opts['replace'][1]; + $out = $parser->replaceSection( $input, $section, $replace ); + } elseif ( isset( $opts['comment'] ) ) { + $linker = $user->getSkin(); + $out = $linker->formatComment( $input, $title, $local ); + } elseif ( isset( $opts['preload'] ) ) { + $out = $parser->getpreloadText( $input, $title, $options ); + } else { + $output = $parser->parse( $input, $title, $options, true, true, 1337 ); + $out = $output->getText(); + + if ( isset( $opts['showtitle'] ) ) { + if ( $output->getTitleText() ) { + $title = $output->getTitleText(); + } + + $out = "$title\n$out"; + } + + if ( isset( $opts['ill'] ) ) { + $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) ); + } elseif ( isset( $opts['cat'] ) ) { + global $wgOut; + + $wgOut->addCategoryLinks( $output->getCategories() ); + $cats = $wgOut->getCategoryLinks(); + + if ( isset( $cats['normal'] ) ) { + $out = $this->tidy( implode( ' ', $cats['normal'] ) ); + } else { + $out = ''; + } + } + + $result = $this->tidy( $result ); + } + + $this->teardownGlobals(); + return $this->showTestResult( $desc, $result, $out ); + } + + /** + * + */ + function showTestResult( $desc, $result, $out ) { + if ( $result === $out ) { + $this->showSuccess( $desc ); + return true; + } else { + $this->showFailure( $desc, $result, $out ); + return false; + } + } + + /** + * Use a regex to find out the value of an option + * @param $key String: name of option val to retrieve + * @param $opts Options array to look in + * @param $default Mixed: default value returned if not found + */ + private static function getOptionValue( $key, $opts, $default ) { + $key = strtolower( $key ); + + if ( isset( $opts[$key] ) ) { + return $opts[$key]; + } else { + return $default; + } + } + + private function parseOptions( $instring ) { + $opts = array(); + // foo + // foo=bar + // foo="bar baz" + // foo=[[bar baz]] + // foo=bar,"baz quux" + $regex = '/\b + ([\w-]+) # Key + \b + (?:\s* + = # First sub-value + \s* + ( + " + [^"]* # Quoted val + " + | + \[\[ + [^]]* # Link target + \]\] + | + [\w-]+ # Plain word + ) + (?:\s* + , # Sub-vals 1..N + \s* + ( + "[^"]*" # Quoted val + | + \[\[[^]]*\]\] # Link target + | + [\w-]+ # Plain word + ) + )* + )? + /x'; + + if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $bits ) { + array_shift( $bits ); + $key = strtolower( array_shift( $bits ) ); + if ( count( $bits ) == 0 ) { + $opts[$key] = true; + } elseif ( count( $bits ) == 1 ) { + $opts[$key] = $this->cleanupOption( array_shift( $bits ) ); + } else { + // Array! + $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits ); + } + } + } + return $opts; + } + + private function cleanupOption( $opt ) { + if ( substr( $opt, 0, 1 ) == '"' ) { + return substr( $opt, 1, -1 ); + } + + if ( substr( $opt, 0, 2 ) == '[[' ) { + return substr( $opt, 2, -2 ); + } + return $opt; + } + + /** + * Set up the global variables for a consistent environment for each test. + * Ideally this should replace the global configuration entirely. + */ + private function setupGlobals( $opts = '', $config = '' ) { + # Find out values for some special options. + $lang = + self::getOptionValue( 'language', $opts, 'en' ); + $variant = + self::getOptionValue( 'variant', $opts, false ); + $maxtoclevel = + self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); + $linkHolderBatchSize = + self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); + + $settings = array( + 'wgServer' => 'http://Britney-Spears', + 'wgScript' => '/index.php', + 'wgScriptPath' => '/', + 'wgArticlePath' => '/wiki/$1', + 'wgActionPaths' => array(), + 'wgLocalFileRepo' => array( + 'class' => 'LocalRepo', + 'name' => 'local', + 'directory' => $this->uploadDir, + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + ), + 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), + 'wgStylePath' => '/skins', + 'wgStyleSheetPath' => '/skins', + 'wgSitename' => 'MediaWiki', + 'wgLanguageCode' => $lang, + 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_', + 'wgRawHtml' => isset( $opts['rawhtml'] ), + 'wgLang' => null, + 'wgContLang' => null, + 'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ), + 'wgMaxTocLevel' => $maxtoclevel, + 'wgCapitalLinks' => true, + 'wgNoFollowLinks' => true, + 'wgNoFollowDomainExceptions' => array(), + 'wgThumbnailScriptPath' => false, + 'wgUseImageResize' => false, + 'wgUseTeX' => isset( $opts['math'] ), + 'wgMathDirectory' => $this->uploadDir . '/math', + 'wgLocaltimezone' => 'UTC', + 'wgAllowExternalImages' => true, + 'wgUseTidy' => false, + 'wgDefaultLanguageVariant' => $variant, + 'wgVariantArticlePath' => false, + 'wgGroupPermissions' => array( '*' => array( + 'createaccount' => true, + 'read' => true, + 'edit' => true, + 'createpage' => true, + 'createtalk' => true, + ) ), + 'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ), + 'wgDefaultExternalStore' => array(), + 'wgForeignFileRepos' => array(), + 'wgLinkHolderBatchSize' => $linkHolderBatchSize, + 'wgExperimentalHtmlIds' => false, + 'wgExternalLinkTarget' => false, + 'wgAlwaysUseTidy' => false, + 'wgHtml5' => true, + 'wgWellFormedXml' => true, + 'wgAllowMicrodataAttributes' => true, + ); + + if ( $config ) { + $configLines = explode( "\n", $config ); + + foreach ( $configLines as $line ) { + list( $var, $value ) = explode( '=', $line, 2 ); + + $settings[$var] = eval( "return $value;" ); + } + } + + $this->savedGlobals = array(); + + foreach ( $settings as $var => $val ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + $this->savedGlobals[$var] = $GLOBALS[$var]; + } + + $GLOBALS[$var] = $val; + } + + $langObj = Language::factory( $lang ); + $GLOBALS['wgLang'] = $langObj; + $GLOBALS['wgContLang'] = $langObj; + $GLOBALS['wgMemc'] = new FakeMemCachedClient; + $GLOBALS['wgOut'] = new OutputPage; + + global $wgHooks; + + $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; + $wgHooks['ParserTestParser'][] = 'ParserTestStaticParserHook::setup'; + $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp'; + + MagicWord::clearCache(); + + global $wgUser; + $wgUser = new User(); + } + + /** + * List of temporary tables to create, without prefix. + * Some of these probably aren't necessary. + */ + private function listTables() { + $tables = array( 'user', 'user_properties', 'page', 'page_restrictions', + 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks', + 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks', + 'site_stats', 'hitcounter', 'ipblocks', 'image', 'oldimage', + 'recentchanges', 'watchlist', 'math', 'interwiki', 'logging', + 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo', + 'archive', 'user_groups', 'page_props', 'category', 'msg_resource', 'msg_resource_links' + ); + + if ( in_array( $this->db->getType(), array( 'mysql', 'sqlite', 'oracle' ) ) ) + array_push( $tables, 'searchindex' ); + + // Allow extensions to add to the list of tables to duplicate; + // may be necessary if they hook into page save or other code + // which will require them while running tests. + wfRunHooks( 'ParserTestTables', array( &$tables ) ); + + return $tables; + } + + /** + * Set up a temporary set of wiki tables to work with for the tests. + * Currently this will only be done once per run, and any changes to + * the db will be visible to later tests in the run. + */ + public function setupDatabase() { + global $wgDBprefix; + + if ( $this->databaseSetupDone ) { + return; + } + + $this->db = wfGetDB( DB_MASTER ); + $dbType = $this->db->getType(); + + if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) { + throw new MWException( 'setupDatabase should be called before setupGlobals' ); + } + + $this->databaseSetupDone = true; + $this->oldTablePrefix = $wgDBprefix; + + # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892). + # It seems to have been fixed since (r55079?). + # If it fails, $wgCaches[CACHE_DB] = new HashBagOStuff(); should work around it. + + # CREATE TEMPORARY TABLE breaks if there is more than one server + if ( wfGetLB()->getServerCount() != 1 ) { + $this->useTemporaryTables = false; + } + + $temporary = $this->useTemporaryTables || $dbType == 'postgres'; + $tables = $this->listTables(); + + foreach ( $tables as $tbl ) { + # Clean up from previous aborted run. So that table escaping + # works correctly across DB engines, we need to change the pre- + # fix back and forth so tableName() works right. + $this->changePrefix( $this->oldTablePrefix ); + $oldTableName = $this->db->tableName( $tbl ); + $this->changePrefix( $dbType != 'oracle' ? 'parsertest_' : 'pt_' ); + $newTableName = $this->db->tableName( $tbl ); + + if ( $dbType == 'mysql' ) { + $this->db->query( "DROP TABLE IF EXISTS $newTableName" ); + } elseif ( in_array( $dbType, array( 'postgres', 'oracle' ) ) ) { + /* DROPs wouldn't work due to Foreign Key Constraints (bug 14990, r58669) + * Use "DROP TABLE IF EXISTS $newTableName CASCADE" for postgres? That + * syntax would also work for mysql. + */ + } elseif ( $this->db->tableExists( $tbl ) ) { + $this->db->query( "DROP TABLE $newTableName" ); + } + + # Create new table + $this->db->duplicateTableStructure( $oldTableName, $newTableName, $temporary ); + } + + if ( $dbType == 'oracle' ) + $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + + $this->changePrefix( $dbType != 'oracle' ? 'parsertest_' : 'pt_' ); + + if ( $dbType == 'oracle' ) { + # Insert 0 user to prevent FK violations + + # Anonymous user + $this->db->insert( 'user', array( + 'user_id' => 0, + 'user_name' => 'Anonymous' ) ); + } + + # Hack: insert a few Wikipedia in-project interwiki prefixes, + # for testing inter-language links + $this->db->insert( 'interwiki', array( + array( 'iw_prefix' => 'wikipedia', + 'iw_url' => 'http://en.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0 ), + array( 'iw_prefix' => 'meatball', + 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0 ), + array( 'iw_prefix' => 'zh', + 'iw_url' => 'http://zh.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ), + array( 'iw_prefix' => 'es', + 'iw_url' => 'http://es.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ), + array( 'iw_prefix' => 'fr', + 'iw_url' => 'http://fr.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ), + array( 'iw_prefix' => 'ru', + 'iw_url' => 'http://ru.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ), + ) ); + + + # Update certain things in site_stats + $this->db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ) ); + + # Reinitialise the LocalisationCache to match the database state + Language::getLocalisationCache()->unloadAll(); + + # Make a new message cache + global $wgMessageCache, $wgMemc; + $wgMessageCache = new MessageCache( $wgMemc, true, 3600 ); + + $this->uploadDir = $this->setupUploadDir(); + $user = User::createNew( 'WikiSysop' ); + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); + $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array( + 'size' => 12345, + 'width' => 1941, + 'height' => 220, + 'bits' => 24, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( array() ), + 'sha1' => wfBaseConvert( '', 16, 36, 31 ), + 'fileExists' => true + ), $this->db->timestamp( '20010115123500' ), $user ); + + # This image will be blacklisted in [[MediaWiki:Bad image list]] + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); + $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', array( + 'size' => 12345, + 'width' => 320, + 'height' => 240, + 'bits' => 24, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( array() ), + 'sha1' => wfBaseConvert( '', 16, 36, 31 ), + 'fileExists' => true + ), $this->db->timestamp( '20010115123500' ), $user ); + } + + /** + * Change the table prefix on all open DB connections/ + */ + protected function changePrefix( $prefix ) { + global $wgDBprefix; + wfGetLBFactory()->forEachLB( array( $this, 'changeLBPrefix' ), array( $prefix ) ); + $wgDBprefix = $prefix; + } + + public function changeLBPrefix( $lb, $prefix ) { + $lb->forEachOpenConnection( array( $this, 'changeDBPrefix' ), array( $prefix ) ); + } + + public function changeDBPrefix( $db, $prefix ) { + $db->tablePrefix( $prefix ); + } + + public function teardownDatabase() { + if ( !$this->databaseSetupDone ) { + $this->teardownGlobals(); + return; + } + $this->teardownUploadDir( $this->uploadDir ); + + $this->changePrefix( $this->oldTablePrefix ); + $this->databaseSetupDone = false; + + if ( $this->useTemporaryTables ) { + # Don't need to do anything + $this->teardownGlobals(); + return; + } + + $tables = $this->listTables(); + + foreach ( $tables as $table ) { + $sql = $this->db->getType() == 'oracle' ? "DROP TABLE pt_$table DROP CONSTRAINTS" : "DROP TABLE `parsertest_$table`"; + $this->db->query( $sql ); + } + + if ( $this->db->getType() == 'oracle' ) + $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + + $this->teardownGlobals(); + } + + /** + * Create a dummy uploads directory which will contain a couple + * of files in order to pass existence tests. + * + * @return String: the directory + */ + private function setupUploadDir() { + global $IP; + + if ( $this->keepUploads ) { + $dir = wfTempDir() . '/mwParser-images'; + + if ( is_dir( $dir ) ) { + return $dir; + } + } else { + $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; + } + + // wfDebug( "Creating upload directory $dir\n" ); + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + return $dir; + } + + wfMkdirParents( $dir . '/3/3a' ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); + wfMkdirParents( $dir . '/0/09' ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" ); + + return $dir; + } + + /** + * Restore default values and perform any necessary clean-up + * after each test runs. + */ + private function teardownGlobals() { + RepoGroup::destroySingleton(); + LinkCache::singleton()->clear(); + + foreach ( $this->savedGlobals as $var => $val ) { + $GLOBALS[$var] = $val; + } + } + + /** + * Remove the dummy uploads directory + */ + private function teardownUploadDir( $dir ) { + if ( $this->keepUploads ) { + return; + } + + // delete the files first, then the dirs. + self::deleteFiles( + array ( + "$dir/3/3a/Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", + + "$dir/0/09/Bad.jpg", + + "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", + ) + ); + + self::deleteDirs( + array ( + "$dir/3/3a", + "$dir/3", + "$dir/thumb/6/65", + "$dir/thumb/6", + "$dir/thumb/3/3a/Foobar.jpg", + "$dir/thumb/3/3a", + "$dir/thumb/3", + + "$dir/0/09/", + "$dir/0/", + "$dir/thumb", + "$dir/math/f/a/5", + "$dir/math/f/a", + "$dir/math/f", + "$dir/math", + "$dir", + ) + ); + } + + /** + * Delete the specified files, if they exist. + * @param $files Array: full paths to files to delete. + */ + private static function deleteFiles( $files ) { + foreach ( $files as $file ) { + if ( file_exists( $file ) ) { + unlink( $file ); + } + } + } + + /** + * Delete the specified directories, if they exist. Must be empty. + * @param $dirs Array: full paths to directories to delete. + */ + private static function deleteDirs( $dirs ) { + foreach ( $dirs as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); + } + } + } + + /** + * "Running test $desc..." + */ + protected function showTesting( $desc ) { + print "Running test $desc... "; + } + + /** + * Print a happy success message. + * + * @param $desc String: the test name + * @return Boolean + */ + protected function showSuccess( $desc ) { + if ( $this->showProgress ) { + print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n"; + } + + return true; + } + + /** + * Print a failure message and provide some explanatory output + * about what went wrong if so configured. + * + * @param $desc String: the test name + * @param $result String: expected HTML output + * @param $html String: actual HTML output + * @return Boolean + */ + protected function showFailure( $desc, $result, $html ) { + if ( $this->showFailure ) { + if ( !$this->showProgress ) { + # In quiet mode we didn't show the 'Testing' message before the + # test, in case it succeeded. Show it now: + $this->showTesting( $desc ); + } + + print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n"; + + if ( $this->showOutput ) { + print "--- Expected ---\n$result\n--- Actual ---\n$html\n"; + } + + if ( $this->showDiffs ) { + print $this->quickDiff( $result, $html ); + if ( !$this->wellFormed( $html ) ) { + print "XML error: $this->mXmlError\n"; + } + } + } + + return false; + } + + /** + * Run given strings through a diff and return the (colorized) output. + * Requires writable /tmp directory and a 'diff' command in the PATH. + * + * @param $input String + * @param $output String + * @param $inFileTail String: tailing for the input file name + * @param $outFileTail String: tailing for the output file name + * @return String + */ + protected function quickDiff( $input, $output, $inFileTail = 'expected', $outFileTail = 'actual' ) { + $prefix = wfTempDir() . "/mwParser-" . mt_rand(); + + $infile = "$prefix-$inFileTail"; + $this->dumpToFile( $input, $infile ); + + $outfile = "$prefix-$outFileTail"; + $this->dumpToFile( $output, $outfile ); + + $diff = `diff -au $infile $outfile`; + unlink( $infile ); + unlink( $outfile ); + + return $this->colorDiff( $diff ); + } + + /** + * Write the given string to a file, adding a final newline. + * + * @param $data String + * @param $filename String + */ + private function dumpToFile( $data, $filename ) { + $file = fopen( $filename, "wt" ); + fwrite( $file, $data . "\n" ); + fclose( $file ); + } + + /** + * Colorize unified diff output if set for ANSI color output. + * Subtractions are colored blue, additions red. + * + * @param $text String + * @return String + */ + protected function colorDiff( $text ) { + return preg_replace( + array( '/^(-.*)$/m', '/^(\+.*)$/m' ), + array( $this->term->color( 34 ) . '$1' . $this->term->reset(), + $this->term->color( 31 ) . '$1' . $this->term->reset() ), + $text ); + } + + /** + * Show "Reading tests from ..." + * + * @param $path String + */ + public function showRunFile( $path ) { + print $this->term->color( 1 ) . + "Reading tests from \"$path\"..." . + $this->term->reset() . + "\n"; + } + + /** + * Insert a temporary test article + * @param $name String: the title, including any prefix + * @param $text String: the article text + * @param $line Integer: the input line number, for reporting errors + */ + static public function addArticle( $name, $text, $line = 'unknown' ) { + global $wgCapitalLinks; + + $text = self::chomp($text); + + $oldCapitalLinks = $wgCapitalLinks; + $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637 + + $name = self::chomp( $name ); + $title = Title::newFromText( $name ); + + if ( is_null( $title ) ) { + wfDie( "invalid title ('$name' => '$title') at line $line\n" ); + } + + $aid = $title->getArticleID( Title::GAID_FOR_UPDATE ); + + if ( $aid != 0 ) { + debug_print_backtrace(); + wfDie( "duplicate article '$name' at line $line\n" ); + } + + $art = new Article( $title ); + $art->doEdit( $text, '', EDIT_NEW ); + + $wgCapitalLinks = $oldCapitalLinks; + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param $name String + * @return Bool true if tag hook is present + */ + public function requireHook( $name ) { + global $wgParser; + + $wgParser->firstCallInit( ); // make sure hooks are loaded. + + if ( isset( $wgParser->mTagHooks[$name] ) ) { + $this->hooks[$name] = $wgParser->mTagHooks[$name]; + } else { + echo " This test suite requires the '$name' hook extension, skipping.\n"; + return false; + } + + return true; + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param $name String + * @return Bool true if function hook is present + */ + public function requireFunctionHook( $name ) { + global $wgParser; + + $wgParser->firstCallInit( ); // make sure hooks are loaded. + + if ( isset( $wgParser->mFunctionHooks[$name] ) ) { + $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name]; + } else { + echo " This test suite requires the '$name' function hook extension, skipping.\n"; + return false; + } + + return true; + } + + /* + * Run the "tidy" command on text if the $wgUseTidy + * global is true + * + * @param $text String: the text to tidy + * @return String + * @static + */ + private function tidy( $text ) { + global $wgUseTidy; + + if ( $wgUseTidy ) { + $text = MWTidy::tidy( $text ); + } + + return $text; + } + + private function wellFormed( $text ) { + $html = + Sanitizer::hackDocType() . + '' . + $text . + ''; + + $parser = xml_parser_create( "UTF-8" ); + + # case folding violates XML standard, turn it off + xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); + + if ( !xml_parse( $parser, $html, true ) ) { + $err = xml_error_string( xml_get_error_code( $parser ) ); + $position = xml_get_current_byte_index( $parser ); + $fragment = $this->extractFragment( $html, $position ); + $this->mXmlError = "$err at byte $position:\n$fragment"; + xml_parser_free( $parser ); + + return false; + } + + xml_parser_free( $parser ); + + return true; + } + + private function extractFragment( $text, $position ) { + $start = max( 0, $position - 10 ); + $before = $position - $start; + $fragment = '...' . + $this->term->color( 34 ) . + substr( $text, $start, $before ) . + $this->term->color( 0 ) . + $this->term->color( 31 ) . + $this->term->color( 1 ) . + substr( $text, $position, 1 ) . + $this->term->color( 0 ) . + $this->term->color( 34 ) . + substr( $text, $position + 1, 9 ) . + $this->term->color( 0 ) . + '...'; + $display = str_replace( "\n", ' ', $fragment ); + $caret = ' ' . + str_repeat( ' ', $before ) . + $this->term->color( 31 ) . + '^' . + $this->term->color( 0 ); + + return "$display\n$caret"; + } + + static function getFakeTimestamp( &$parser, &$ts ) { + $ts = 123; + return true; + } +} diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt new file mode 100644 index 0000000000..daccf7ccf4 --- /dev/null +++ b/tests/parser/parserTests.txt @@ -0,0 +1,8315 @@ +# MediaWiki Parser test cases +# Some taken from http://meta.wikimedia.org/wiki/Parser_testing +# All (C) their respective authors and released under the GPL +# +# The syntax should be fairly self-explanatory. +# +# Currently supported test options: +# One of the following three: +# +# (default) generate HTML output +# pst apply pre-save transform +# msg apply message transform +# +# Plus any combination of these: +# +# cat add category links +# ill add inter-language links +# subpage enable subpages (disabled by default) +# noxml don't check for XML well formdness +# title=[[XXX]] run test using article title XXX +# language=XXX set content language to XXX for this test +# variant=XXX set the variant of language for this test (eg zh-tw) +# disabled do not run test +# showtitle make the first line the title +# comment run through Linker::formatComment() instead of main parser +# local format section links in edit comment text as local links +# +# For testing purposes, temporary articles can created: +# !!article / NAMESPACE:TITLE / !!text / ARTICLE TEXT / !!endarticle +# where '/' denotes a newline. + +# This is the standard article assumed to exist. +!! article +Main Page +!! text +blah blah +!! endarticle + +!!article +Template:Foo +!!text +FOO +!!endarticle + +!! article +Template:Blank +!! text +!! endarticle + +!! article +Template:! +!! text +| +!! endarticle + +!!article +MediaWiki:bad image list +!!text +* [[File:Bad.jpg]] except [[Nasty page]] +!!endarticle + +### +### Basic tests +### +!! test +Blank input +!! input +!! result +!! end + + +!! test +Simple paragraph +!! input +This is a simple paragraph. +!! result +

This is a simple paragraph. +

+!! end + +!! test +Simple list +!! input +* Item 1 +* Item 2 +!! result +
  • Item 1 +
  • Item 2 +
+ +!! end + +!! test +Italics and bold +!! input +* plain +* plain''italic''plain +* plain''italic''plain''italic''plain +* plain'''bold'''plain +* plain'''bold'''plain'''bold'''plain +* plain''italic''plain'''bold'''plain +* plain'''bold'''plain''italic''plain +* plain''italic'''bold-italic'''italic''plain +* plain'''bold''bold-italic''bold'''plain +* plain'''''bold-italic'''italic''plain +* plain'''''bold-italic''bold'''plain +* plain''italic'''bold-italic'''''plain +* plain'''bold''bold-italic'''''plain +* plain l'''italic''plain +* plain l''''bold''' plain +!! result +
  • plain +
  • plainitalicplain +
  • plainitalicplainitalicplain +
  • plainboldplain +
  • plainboldplainboldplain +
  • plainitalicplainboldplain +
  • plainboldplainitalicplain +
  • plainitalicbold-italicitalicplain +
  • plainboldbold-italicboldplain +
  • plainbold-italicitalicplain +
  • plainbold-italicboldplain +
  • plainitalicbold-italicplain +
  • plainboldbold-italicplain +
  • plain l'italicplain +
  • plain l'bold plain +
+ +!! end + +### +### test cases +### + +!! test + unordered list +!! input +* This is not an unordered list item. +!! result +

* This is not an unordered list item. +

+!! end + +!! test + spacing +!! input +Lorem ipsum dolor + +sed abit. + sed nullum. + +:and a colon + +!! result +

Lorem ipsum dolor + +sed abit. + sed nullum. + +:and a colon + +

+!! end + +!! test +nowiki 3 +!! input +:There is not nowiki. +:There is nowiki. + +#There is not nowiki. +#There is nowiki. + +*There is not nowiki. +*There is nowiki. +!! result +
There is not nowiki. +
There is nowiki. +
+
  1. There is not nowiki. +
  2. There is nowiki. +
+
  • There is not nowiki. +
  • There is nowiki. +
+ +!! end + + +### +### Comments +### +!! test +Comment test 1 +!! input + asdf + +!! result +
asdf
+
+ +!! end + +!! test +Comment test 2 +!! input +asdf + +jkl +!! result +

asdf +jkl +

+!! end + +!! test +Comment test 3 +!! input +asdf + + +jkl +!! result +

asdf +jkl +

+!! end + +!! test +Comment test 4 +!! input +asdfjkl +!! result +

asdfjkl +

+!! end + +!! test +Comment spacing +!! input +a + b +c +!! result +

a +

+
 b 
+
+

c +

+!! end + +!! test +Comment whitespace +!! input + +!! result + +!! end + +!! test +Comment semantics and delimiters +!! input + +!! result + +!! end + +!! test +Comment semantics and delimiters, redux +!! input + +!! result + +!! end + +!! test +Comment semantics and delimiters: directors cut +!! input +--> +!! result +

--> +

+!! end + +!! test +Comment semantics: nesting +!! input +--> +!! result +

--> +

+!! end + +!! test +Comment semantics: unclosed comment at end +!! input +oo}} +!! result +

FOO +

+!! end + +!! test +Comment on its own line post-expand +!! input +a +{{blank}} +b +!! result +

a +

b +

+!! end + +### +### Preformatted text +### +!! test +Preformatted text +!! input + This is some + Preformatted text + With ''italic'' + And '''bold''' + And a [[Main Page|link]] +!! result +
This is some
+Preformatted text
+With italic
+And bold
+And a link
+
+!! end + +!! test +
 with  inside (compatibility with 1.6 and earlier)
+!! input
+

+
+
+
+
+!! result +
+<b>
+<cite>
+<em>
+
+ +!! end + +!! test +Regression with preformatted in
+!! input +
+ Blah +
+!! result +
+
Blah
+
+
+ +!! end + +# Expected output in the following test is not really expected (there should be +#
 in the output) -- it's only testing for well-formedness.
+!! test
+Bug 6200: Preformatted in 
+!! input +
+ Blah +
+!! result +
+ Blah +
+ +!! end + +!! test +
 with attributes (bug 3202)
+!! input
+
Bluescreen of WikiDeath
+!! result +
Bluescreen of WikiDeath
+ +!! end + +!! test +
 with width attribute (bug 3202)
+!! input
+
Narrow screen goodies
+!! result +
Narrow screen goodies
+ +!! end + +!! test +
 with forbidden attribute (bug 3202)
+!! input
+
Narrow screen goodies
+!! result +
Narrow screen goodies
+ +!! end + +!! test +
 with forbidden attribute values (bug 3202)
+!! input
+
Narrow screen goodies
+!! result +
Narrow screen goodies
+ +!! end + +!! test + inside
 (bug 13238)
+!! input
+
+
+
+
+
+
+
Foo
+!! result +
+<nowiki>
+
+
+
+
+
<nowiki>Foo</nowiki>
+ +!! end + +!! test + and
 preference (first one wins)
+!! input
+
+
+
+ +
+ + +
+
+
+
+
+ +!! result +
+<nowiki>
+
+

</nowiki> +</pre> +

+<pre> +<nowiki> +</pre> + +</pre> +

+!! end + + +### +### Definition lists +### +!! test +Simple definition +!! input +; name : Definition +!! result +
name 
Definition +
+ +!! end + +!! test +Definition list for indentation only +!! input +: Indented text +!! result +
Indented text +
+ +!! end + +!! test +Definition list with no space +!! input +;name:Definition +!! result +
name
Definition +
+ +!!end + +!! test +Definition list with URL link +!! input +; http://example.com/ : definition +!! result +
http://example.com/ 
definition +
+ +!! end + +!! test +Definition list with bracketed URL link +!! input +;[http://www.example.com/ Example]:Something about it +!! result +
Example
Something about it +
+ +!! end + +!! test +Definition list with wikilink containing colon +!! input +; [[Help:FAQ]]: The least-read page on Wikipedia +!! result +
Help:FAQ
The least-read page on Wikipedia +
+ +!! end + +# At Brion's and JeLuF's insistence... :) +!! test +Definition list with news link containing colon +!! input +; news:alt.wikipedia.rox: This isn't even a real newsgroup! +!! result +
news:alt.wikipedia.rox
This isn't even a real newsgroup! +
+ +!! end + +!! test +Malformed definition list with colon +!! input +; news:alt.wikipedia.rox -- don't crash or enter an infinite loop +!! result +
news:alt.wikipedia.rox -- don't crash or enter an infinite loop +
+ +!! end + +!! test +Definition lists: colon in external link text +!! input +; [http://www.wikipedia2.org/ Wikipedia : The Next Generation]: OK, I made that up +!! result +
Wikipedia : The Next Generation
OK, I made that up +
+ +!! end + +!! test +Definition lists: colon in HTML attribute +!! input +;bold +!! result +
bold +
+ +!! end + + +!! test +Definition lists: self-closed tag +!! input +;one
two : two-line fun +!! result +
one
two 
two-line fun +
+ +!! end + + +### +### External links +### +!! test +External links: non-bracketed +!! input +Non-bracketed: http://example.com +!! result +

Non-bracketed: http://example.com +

+!! end + +!! test +External links: numbered +!! input +Numbered: [http://example.com] +Numbered: [http://example.net] +Numbered: [http://example.com] +!! result +

Numbered: [1] +Numbered: [2] +Numbered: [3] +

+!!end + +!! test +External links: specified text +!! input +Specified text: [http://example.com link] +!! result +

Specified text: link +

+!!end + +!! test +External links: trail +!! input +Linktrails should not work for external links: [http://example.com link]s +!! result +

Linktrails should not work for external links: links +

+!! end + +!! test +External links: dollar sign in URL +!! input +http://example.com/1$2345 +!! result +

http://example.com/1$2345 +

+!! end + +!! test +External links: dollar sign in URL (named) +!! input +[http://example.com/1$2345] +!! result +

[1] +

+!!end + +!! test +External links: open square bracket forbidden in URL (bug 4377) +!! input +http://example.com/1[2345 +!! result +

http://example.com/1[2345 +

+!! end + +!! test +External links: open square bracket forbidden in URL (named) (bug 4377) +!! input +[http://example.com/1[2345] +!! result +

[2345 +

+!!end + +!! test +External links: nowiki in URL link text (bug 6230) +!!input +[http://example.com/ ''example site''] +!! result +

''example site'' +

+!! end + +!! test +External links: newline forbidden in text (bug 6230 regression check) +!! input +[http://example.com/ first +second] +!! result +

[http://example.com/ first +second] +

+!!end + +!! test +External image +!! input +External image: http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png +!! result +

External image: Ncwikicol.png +

+!! end + +!! test +External image from https +!! input +External image from https: https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png +!! result +

External image from https: Ncwikicol.png +

+!! end + +!! test +Link to non-http image, no img tag +!! input +Link to non-http image, no img tag: ftp://example.com/test.jpg +!! result +

Link to non-http image, no img tag: ftp://example.com/test.jpg +

+!! end + +!! test +External links: terminating separator +!! input +Terminating separator: http://example.com/thing, +!! result +

Terminating separator: http://example.com/thing, +

+!! end + +!! test +External links: intervening separator +!! input +Intervening separator: http://example.com/1,2,3 +!! result +

Intervening separator: http://example.com/1,2,3 +

+!! end + +!! test +External links: old bug with URL in query +!! input +Old bug with URL in query: [http://example.com/thing?url=http://example.com link] +!! result +

Old bug with URL in query: link +

+!! end + +!! test +External links: old URL-in-URL bug, mixed protocols +!! input +And again with mixed protocols: [ftp://example.com?url=http://example.com link] +!! result +

And again with mixed protocols: link +

+!!end + +!! test +External links: URL in text +!! input +URL in text: [http://example.com http://example.com] +!! result +

URL in text: http://example.com +

+!! end + +!! test +External links: Clickable images +!! input +ja-style clickable images: [http://example.com http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png] +!! result +

ja-style clickable images: Ncwikicol.png +

+!!end + +!! test +External links: raw ampersand +!! input +Old & use: http://x&y +!! result +

Old & use: http://x&y +

+!! end + +!! test +External links: encoded ampersand +!! input +Old & use: http://x&y +!! result +

Old & use: http://x&y +

+!! end + +!! test +External links: encoded equals (bug 6102) +!! input +http://example.com/?foo=bar +!! result +

http://example.com/?foo=bar +

+!! end + +!! test +External links: [raw ampersand] +!! input +Old & use: [http://x&y] +!! result +

Old & use: [1] +

+!! end + +!! test +External links: [encoded ampersand] +!! input +Old & use: [http://x&y] +!! result +

Old & use: [1] +

+!! end + +!! test +External links: [encoded equals] (bug 6102) +!! input +[http://example.com/?foo=bar] +!! result +

[1] +

+!! end + +!! test +External links: [IDN ignored character reference in hostname; strip it right off] +!! input +[http://e‌xample.com/] +!! result +

[1] +

+!! end + +!! test +External links: IDN ignored character reference in hostname; strip it right off +!! input +http://e‌xample.com/ +!! result +

http://example.com/ +

+!! end + +!! test +External links: www.jpeg.org (bug 554) +!! input +http://www.jpeg.org +!!result +

http://www.jpeg.org +

+!! end + +!! test +External links: URL within URL (original bug 2) +!! input +[http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp] +!! result +

[1] +

+!! end + +!! test +BUG 361: URL inside bracketed URL +!! input +[http://www.example.com/foo http://www.example.com/bar] +!! result +

http://www.example.com/bar +

+!! end + +!! test +BUG 361: URL within URL, not bracketed +!! input +http://www.example.com/foo?=http://www.example.com/bar +!! result +

http://www.example.com/foo?=http://www.example.com/bar +

+!! end + +!! test +BUG 289: ">"-token in URL-tail +!! input +http://www.example.com/ +!! result +

http://www.example.com/<hello> +

+!!end + +!! test +BUG 289: literal ">"-token in URL-tail +!! input +http://www.example.com/html +!! result +

http://www.example.com/html +

+!!end + +!! test +BUG 289: ">"-token in bracketed URL +!! input +[http://www.example.com/ stuff] +!! result +

<hello> stuff +

+!!end + +!! test +BUG 289: literal ">"-token in bracketed URL +!! input +[http://www.example.com/html stuff] +!! result +

html stuff +

+!!end + +!! test +BUG 289: literal double quote at end of URL +!! input +http://www.example.com/"hello" +!! result +

http://www.example.com/"hello" +

+!!end + +!! test +BUG 289: literal double quote in bracketed URL +!! input +[http://www.example.com/"hello" stuff] +!! result +

"hello" stuff +

+!!end + +!! test +External links: multiple legal whitespace is fine, Magnus. Don't break it please. (bug 5081) +!! input +[http://www.example.com test] +!! result +

test +

+!! end + +!! test +External links: wiki links within external link (Bug 3695) +!! input +[http://example.com [[wikilink]] embedded in ext link] +!! result +

wikilink embedded in ext link +

+!! end + +!! test +BUG 787: Links with one slash after the url protocol are invalid +!! input +http:/example.com + +[http:/example.com title] +!! result +

http:/example.com +

[http:/example.com title] +

+!! end + +!! test +Bug 2702: Mismatched , and tags are invalid +!! input +''[http://example.com text''] +[http://example.com '''text]''' +''Something [http://example.com in italic''] +''Something [http://example.com mixed''''', even bold]''' +'''''Now [http://example.com both'''''] +!! result +

text +text +Something in italic +Something mixed, even bold +Now both +

+!! end + + +!! test +Bug 4781: %26 in URL +!! input +http://www.example.com/?title=AT%26T +!! result +

http://www.example.com/?title=AT%26T +

+!! end + +!! test +Bug 4781, 5267: %26 in URL +!! input +http://www.example.com/?title=100%25_Bran +!! result +

http://www.example.com/?title=100%25_Bran +

+!! end + +!! test +Bug 4781, 5267: %28, %29 in URL +!! input +http://www.example.com/?title=Ben-Hur_%281959_film%29 +!! result +

http://www.example.com/?title=Ben-Hur_%281959_film%29 +

+!! end + + +!! test +Bug 4781: %26 in autonumber URL +!! input +[http://www.example.com/?title=AT%26T] +!! result +

[1] +

+!! end + +!! test +Bug 4781, 5267: %26 in autonumber URL +!! input +[http://www.example.com/?title=100%25_Bran] +!! result +

[1] +

+!! end + +!! test +Bug 4781, 5267: %28, %29 in autonumber URL +!! input +[http://www.example.com/?title=Ben-Hur_%281959_film%29] +!! result +

[1] +

+!! end + + +!! test +Bug 4781: %26 in bracketed URL +!! input +[http://www.example.com/?title=AT%26T link] +!! result +

link +

+!! end + +!! test +Bug 4781, 5267: %26 in bracketed URL +!! input +[http://www.example.com/?title=100%25_Bran link] +!! result +

link +

+!! end + +!! test +Bug 4781, 5267: %28, %29 in bracketed URL +!! input +[http://www.example.com/?title=Ben-Hur_%281959_film%29 link] +!! result +

link +

+!! end + +!! test +External link containing double-single-quotes in text '' (bug 4598 sanity check) +!! input +Some [http://example.com/ pretty ''italics'' and stuff]! +!! result +

Some pretty italics and stuff! +

+!! end + +!! test +External link containing double-single-quotes in text embedded in italics (bug 4598 sanity check) +!! input +''Some [http://example.com/ pretty ''italics'' and stuff]!'' +!! result +

Some pretty italics and stuff! +

+!! end + +!! test +External link containing double-single-quotes with no space separating the url from text in italics +!! input +[http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm''La muerte de Casagemas'' (1901) en el sitio de [[Museo Picasso (París)|Museo Picasso]].] +!! result +

La muerte de Casagemas (1901) en el sitio de Museo Picasso. +

+!! end + +!! test +URL-encoding in URL functions (single parameter) +!! input +{{localurl:Some page|amp=&}} +!! result +

/index.php?title=Some_page&amp=& +

+!! end + +!! test +URL-encoding in URL functions (multiple parameters) +!! input +{{localurl:Some page|q=?&=&}} +!! result +

/index.php?title=Some_page&q=?&amp=& +

+!! end + +### +### Quotes +### + +!! test +Quotes +!! input +Normal text. '''Bold text.''' Normal text. ''Italic text.'' + +Normal text. '''''Bold italic text.''''' Normal text. +!!result +

Normal text. Bold text. Normal text. Italic text. +

Normal text. Bold italic text. Normal text. +

+!! end + + +!! test +Unclosed and unmatched quotes +!! input +'''''Bold italic text '''with bold deactivated''' in between.''''' + +'''''Bold italic text ''with italic deactivated'' in between.''''' + +'''Bold text.. + +..spanning two paragraphs (should not work).''' + +'''Bold tag left open + +''Italic tag left open + +Normal text. + + +'''This year''''s election ''should'' beat '''last year''''s. + +''Tom'''s car is bigger than ''Susan'''s. +!! result +

Bold italic text with bold deactivated in between. +

Bold italic text with italic deactivated in between. +

Bold text.. +

..spanning two paragraphs (should not work). +

Bold tag left open +

Italic tag left open +

Normal text. +

This year's election should beat last year's. +

Toms car is bigger than Susans. +

+!! end + +### +### Tables +### +### some content taken from http://meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide:_Using_tables +### + +# This should not produce
as
+# is the bare minimun required by the spec, see: +# http://www.w3.org/TR/xhtml-modularization/dtd_module_defs.html#a_module_Basic_Tables +!! test +A table with no data. +!! input +{||} +!! result +!! end + +# A table with nothing but a caption is invalid XHTML, we might want to render +# this as

caption

+!! test +A table with nothing but a caption +!! input +{| +|+ caption +|} +!! result + +
caption +
+ +!! end + +!! test +Simple table +!! input +{| +| 1 || 2 +|- +| 3 || 4 +|} +!! result + + + + + + +
1 2 +
3 4 +
+ +!! end + +!! test +Multiplication table +!! input +{| border="1" cellpadding="2" +|+Multiplication table +|- +! × !! 1 !! 2 !! 3 +|- +! 1 +| 1 || 2 || 3 +|- +! 2 +| 2 || 4 || 6 +|- +! 3 +| 3 || 6 || 9 +|- +! 4 +| 4 || 8 || 12 +|- +! 5 +| 5 || 10 || 15 +|} +!! result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Multiplication table +
× 1 2 3 +
1 + 1 2 3 +
2 + 2 4 6 +
3 + 3 6 9 +
4 + 4 8 12 +
5 + 5 10 15 +
+ +!! end + +!! test +Table rowspan +!! input +{| align=right border=1 +| Cell 1, row 1 +|rowspan=2| Cell 2, row 1 (and 2) +| Cell 3, row 1 +|- +| Cell 1, row 2 +| Cell 3, row 2 +|} +!! result + + + + + + + +
Cell 1, row 1 + Cell 2, row 1 (and 2) + Cell 3, row 1 +
Cell 1, row 2 + Cell 3, row 2 +
+ +!! end + +!! test +Nested table +!! input +{| border=1 +| α +| +{| bgcolor=#ABCDEF border=2 +|nested +|- +|table +|} +|the original table again +|} +!! result + + + + +
α + + + + + +
nested +
table +
+
the original table again +
+ +!! end + +!! test +Invalid attributes in table cell (bug 1830) +!! input +{| +|Cell:|broken +|} +!! result + + +
broken +
+ +!! end + + +!! test +Table security: embedded pipes (http://lists.wikimedia.org/mailman/htdig/wikitech-l/2006-April/022293.html) +!! input +{| +| |[ftp://|x||]" onmouseover="alert(document.cookie)">test +!! result + + + + + +
[ftp://%7Cx]" onmouseover="alert(document.cookie)">test +
+ +!! end + + +### +### Internal links +### +!! test +Plain link, capitalized +!! input +[[Main Page]] +!! result +

Main Page +

+!! end + +!! test +Plain link, uncapitalized +!! input +[[main Page]] +!! result +

main Page +

+!! end + +!! test +Piped link +!! input +[[Main Page|The Main Page]] +!! result +

The Main Page +

+!! end + +!! test +Broken link +!! input +[[Zigzagzogzagzig]] +!! result +

Zigzagzogzagzig +

+!! end + +!! test +Broken link with fragment +!! input +[[Zigzagzogzagzig#zug]] +!! result +

Zigzagzogzagzig#zug +

+!! end + +!! test +Special page link with fragment +!! input +[[Special:Version#anchor]] +!! result +

Special:Version#anchor +

+!! end + +!! test +Nonexistent special page link with fragment +!! input +[[Special:ThisNameWillHopefullyNeverBeUsed#anchor]] +!! result +

Special:ThisNameWillHopefullyNeverBeUsed#anchor +

+!! end + +!! test +Link with prefix +!! input +xxx[[main Page]], xxx[[Main Page]], Xxx[[main Page]] XXX[[main Page]], XXX[[Main Page]] +!! result +

xxxmain Page, xxxMain Page, Xxxmain Page XXXmain Page, XXXMain Page +

+!! end + +!! test +Link with suffix +!! input +[[Main Page]]xxx, [[Main Page]]XXX, [[Main Page]]!!! +!! result +

Main Pagexxx, Main PageXXX, Main Page!!! +

+!! end + +!! test +Link with 3 brackets +!! input +[[[main page]]] +!! result +

[[[main page]]] +

+!! end + +!! test +Piped link with 3 brackets +!! input +[[[main page|the main page]]] +!! result +

[[[main page|the main page]]] +

+!! end + +!! test +Link with multiple pipes +!! input +[[Main Page|The|Main|Page]] +!! result +

The|Main|Page +

+!! end + +!! test +Link to namespaces +!! input +[[Talk:Parser testing]], [[Meta:Disclaimers]] +!! result +

Talk:Parser testing, Meta:Disclaimers +

+!! end + +!! test +Piped link to namespace +!! input +[[Meta:Disclaimers|The disclaimers]] +!! result +

The disclaimers +

+!! end + +!! test +Link containing } +!! input +[[Usually caused by a typo (oops}]] +!! result +

[[Usually caused by a typo (oops}]] +

+!! end + +!! test +Link containing % (not as a hex sequence) +!! input +[[7% Solution]] +!! result +

7% Solution +

+!! end + +!! test +Link containing % as a single hex sequence interpreted to char +!! input +[[7%25 Solution]] +!! result +

7% Solution +

+!!end + +!! test +Link containing % as a double hex sequence interpreted to hex sequence +!! input +[[7%2525 Solution]] +!! result +

[[7%2525 Solution]] +

+!!end + +!! test +Link containing "#<" and "#>" % as a hex sequences- these are valid section anchors +Example for such a section: == < == +!! input +[[%23%3c]][[%23%3e]] +!! result +

#<#> +

+!! end + +!! test +Link containing "<#" and ">#" as a hex sequences +!! input +[[%3c%23]][[%3e%23]] +!! result +

[[%3c%23]][[%3e%23]] +

+!! end + +!! test +Link containing double-single-quotes '' (bug 4598) +!! input +[[Lista d''e paise d''o munno]] +!! result +

Lista d''e paise d''o munno +

+!! end + +!! test +Link containing double-single-quotes '' in text (bug 4598 sanity check) +!! input +Some [[Link|pretty ''italics'' and stuff]]! +!! result +

Some pretty italics and stuff! +

+!! end + +!! test +Link containing double-single-quotes '' in text embedded in italics (bug 4598 sanity check) +!! input +''Some [[Link|pretty ''italics'' and stuff]]! +!! result +

Some pretty italics and stuff! +

+!! end + +!! test +Link with double quotes in title part (literal) and alternate part (interpreted) +!! input +[[File:Denys Savchenko ''Pentecoste''.jpg]] + +[[''Pentecoste'']] + +[[''Pentecoste''|Pentecoste]] + +[[''Pentecoste''|''Pentecoste'']] +!! result +

File:Denys Savchenko Pentecoste.jpg +

''Pentecoste'' +

Pentecoste +

Pentecoste +

+!! end + +!! test +Plain link to URL +!! input +[[http://www.example.com]] +!! result +

[[1]] +

+!! end + +# I'm fairly sure the expected result here is wrong. +# We want these to be URL links, not pseudo-pages with URLs for titles.... +# However the current output is also pretty screwy. +# +# ---- +# I'm changing it to match the current output--it arguably makes more +# sense in the light of the test above. Old expected result was: +#

Piped link to URL: an example URL +#

+# But I think this test is bordering on "garbage in, garbage out" anyway. +# -- wtm +!! test +Piped link to URL +!! input +Piped link to URL: [[http://www.example.com|an example URL]] +!! result +

Piped link to URL: [example URL] +

+!! end + +!! test +BUG 2: [[page|http://url/]] should link to page, not http://url/ +!! input +[[Main Page|http://url/]] +!! result +

http://url/ +

+!! end + +!! test +BUG 337: Escaped self-links should be bold +!! options +title=[[Bug462]] +!! input +[[Bug462]] [[Bug462]] +!! result +

Bug462 Bug462 +

+!! end + +!! test +Self-link to section should not be bold +!! options +title=[[Main Page]] +!! input +[[Main Page#section]] +!! result +

Main Page#section +

+!! end + +!! article +00 +!! text +This is 00. +!! endarticle + +!!test +Self-link to numeric title +!!options +title=[[0]] +!!input +[[0]] +!!result +

0 +

+!!end + +!!test +Link to numeric-equivalent title +!!options +title=[[0]] +!!input +[[00]] +!!result +

00 +

+!!end + +!! test + inside a link +!! input +[[Main Page]] [[Main Page|the main page [it's not very good]]] +!! result +

[[Main Page]] the main page [it's not very good] +

+!! end + +!! test +Non-breaking spaces in title +!! input +[[  Main   Page  ]] +!! result +

  Main   Page   +

+!!end + + +### +### Interwiki links (see maintenance/interwiki.sql) +### + +!! test +Inline interwiki link +!! input +[[MeatBall:SoftSecurity]] +!! result +

MeatBall:SoftSecurity +

+!! end + +!! test +Inline interwiki link with empty title (bug 2372) +!! input +[[MeatBall:]] +!! result +

MeatBall: +

+!! end + +!! test +Interwiki link encoding conversion (bug 1636) +!! input +*[[Wikipedia:ro:Olteniţa]] +*[[Wikipedia:ro:Olteniţa]] +!! result + + +!! end + +!! test +Interwiki link with fragment (bug 2130) +!! input +[[MeatBall:SoftSecurity#foo]] +!! result +

MeatBall:SoftSecurity#foo +

+!! end + +!! test +Interlanguage link +!! input +Blah blah blah +[[zh:Chinese]] +!!result +

Blah blah blah +

+!! end + +!! test +Double interlanguage link +!! input +Blah blah blah +[[es:Spanish]] +[[zh:Chinese]] +!!result +

Blah blah blah +

+!! end + +!! test +Interlanguage link, with prefix links +!! options +language=ln +!! input +Blah blah blah +[[zh:Chinese]] +!!result +

Blah blah blah +

+!! end + +!! test +Double interlanguage link, with prefix links (bug 8897) +!! options +language=ln +!! input +Blah blah blah +[[es:Spanish]] +[[zh:Chinese]] +!!result +

Blah blah blah +

+!! end + + +## +## XHTML tidiness +### + +!! test +
to
+!! input +1
2
3 +!! result +

1
2
3 +

+!! end + +!! test +Incorrecly removing closing slashes from correctly formed XHTML +!! input +
+!! result +


+

+!! end + +!! test +Failing to transform badly formed HTML into correct XHTML +!! input +
+
+
+!! result +


+
+
+

+!!end + +!! test +Horizontal ruler (should it add that extra space?) +!! input +
+
+foo
bar +!! result +
+
+foo
bar + +!! end + +### +### Block-level elements +### +!! test +Common list +!! input +*Common list +* item 2 +*item 3 +!! result +
  • Common list +
  • item 2 +
  • item 3 +
+ +!! end + +!! test +Numbered list +!! input +#Numbered list +#item 2 +# item 3 +!! result +
  1. Numbered list +
  2. item 2 +
  3. item 3 +
+ +!! end + +!! test +Mixed list +!! input +*Mixed list +*# with numbers +** and bullets +*# and numbers +*bullets again +**bullet level 2 +***bullet level 3 +***#Number on level 4 +**bullet level 2 +**#Number on level 3 +**#Number on level 3 +*#number level 2 +*Level 1 +!! result +
  • Mixed list +
    1. with numbers +
    +
    • and bullets +
    +
    1. and numbers +
    +
  • bullets again +
    • bullet level 2 +
      • bullet level 3 +
        1. Number on level 4 +
        +
      +
    • bullet level 2 +
      1. Number on level 3 +
      2. Number on level 3 +
      +
    +
    1. number level 2 +
    +
  • Level 1 +
+ +!! end + +!! test +List items are not parsed correctly following a
 block (bug 785)
+!! input
+* 
foo
+*
bar
+* zar +!! result +
  • foo
    +
  • bar
    +
  • zar +
+ +!! end + +### +### Magic Words +### + +!! test +Magic Word: {{CURRENTDAY}} +!! input +{{CURRENTDAY}} +!! result +

1 +

+!! end + +!! test +Magic Word: {{CURRENTDAY2}} +!! input +{{CURRENTDAY2}} +!! result +

01 +

+!! end + +!! test +Magic Word: {{CURRENTDAYNAME}} +!! input +{{CURRENTDAYNAME}} +!! result +

Thursday +

+!! end + +!! test +Magic Word: {{CURRENTDOW}} +!! input +{{CURRENTDOW}} +!! result +

4 +

+!! end + +!! test +Magic Word: {{CURRENTMONTH}} +!! input +{{CURRENTMONTH}} +!! result +

01 +

+!! end + +!! test +Magic Word: {{CURRENTMONTHABBREV}} +!! input +{{CURRENTMONTHABBREV}} +!! result +

Jan +

+!! end + +!! test +Magic Word: {{CURRENTMONTHNAME}} +!! input +{{CURRENTMONTHNAME}} +!! result +

January +

+!! end + +!! test +Magic Word: {{CURRENTMONTHNAMEGEN}} +!! input +{{CURRENTMONTHNAMEGEN}} +!! result +

January +

+!! end + +!! test +Magic Word: {{CURRENTTIME}} +!! input +{{CURRENTTIME}} +!! result +

00:02 +

+!! end + +!! test +Magic Word: {{CURRENTWEEK}} (@bug 4594) +!! input +{{CURRENTWEEK}} +!! result +

1 +

+!! end + +!! test +Magic Word: {{CURRENTYEAR}} +!! input +{{CURRENTYEAR}} +!! result +

1970 +

+!! end + +!! test +Magic Word: {{FULLPAGENAME}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{FULLPAGENAME}} +!! result +

User:Ævar Arnfjörð Bjarmason +

+!! end + +!! test +Magic Word: {{FULLPAGENAMEE}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{FULLPAGENAMEE}} +!! result +

User:%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason +

+!! end + +!! test +Magic Word: {{NAMESPACE}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{NAMESPACE}} +!! result +

User +

+!! end + +!! test +Magic Word: {{NAMESPACEE}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{NAMESPACEE}} +!! result +

User +

+!! end + +!! test +Magic Word: {{NUMBEROFFILES}} +!! input +{{NUMBEROFFILES}} +!! result +

2 +

+!! end + +!! test +Magic Word: {{PAGENAME}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{PAGENAME}} +!! result +

Ævar Arnfjörð Bjarmason +

+!! end + +!! test +Magic Word: {{PAGENAMEE}} +!! options +title=[[User:Ævar Arnfjörð Bjarmason]] +!! input +{{PAGENAMEE}} +!! result +

%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason +

+!! end + +!! test +Magic Word: {{REVISIONID}} +!! input +{{REVISIONID}} +!! result +

1337 +

+!! end + +!! test +Magic Word: {{SCRIPTPATH}} +!! input +{{SCRIPTPATH}} +!! result +

/ +

+!! end + +!! test +Magic Word: {{SERVER}} +!! input +{{SERVER}} +!! result +

http://Britney-Spears +

+!! end + +!! test +Magic Word: {{SERVERNAME}} +!! input +{{SERVERNAME}} +!! result +

Britney-Spears +

+!! end + +!! test +Magic Word: {{SITENAME}} +!! input +{{SITENAME}} +!! result +

MediaWiki +

+!! end + +!! test +Namespace 1 {{ns:1}} +!! input +{{ns:1}} +!! result +

Talk +

+!! end + +!! test +Namespace 1 {{ns:01}} +!! input +{{ns:01}} +!! result +

Talk +

+!! end + +!! test +Namespace 0 {{ns:0}} (bug 4783) +!! input +{{ns:0}} +!! result + +!! end + +!! test +Namespace 0 {{ns:00}} (bug 4783) +!! input +{{ns:00}} +!! result + +!! end + +!! test +Namespace -1 {{ns:-1}} +!! input +{{ns:-1}} +!! result +

Special +

+!! end + +!! test +Namespace User {{ns:User}} +!! input +{{ns:User}} +!! result +

User +

+!! end + +!! test +Namespace User talk {{ns:User_talk}} +!! input +{{ns:User_talk}} +!! result +

User talk +

+!! end + +!! test +Namespace User talk {{ns:uSeR tAlK}} +!! input +{{ns:uSeR tAlK}} +!! result +

User talk +

+!! end + +!! test +Namespace File {{ns:File}} +!! input +{{ns:File}} +!! result +

File +

+!! end + +!! test +Namespace File {{ns:Image}} +!! input +{{ns:Image}} +!! result +

File +

+!! end + +!! test +Namespace (lang=de) Benutzer {{ns:User}} +!! options +language=de +!! input +{{ns:User}} +!! result +

Benutzer +

+!! end + +!! test +Namespace (lang=de) Benutzer Diskussion {{ns:3}} +!! options +language=de +!! input +{{ns:3}} +!! result +

Benutzer Diskussion +

+!! end + + +!! test +Urlencode +!! input +{{urlencode:hi world?!}} +{{urlencode:hi world?!|WIKI}} +{{urlencode:hi world?!|PATH}} +{{urlencode:hi world?!|QUERY}} +!! result +

hi+world%3F%21 +hi_world%3F! +hi%20world%3F%21 +hi+world%3F%21 +

+!! end + +### +### Magic links +### +!! test +Magic links: internal link to RFC (bug 479) +!! input +[[RFC 123]] +!! result +

RFC 123 +

+!! end + +!! test +Magic links: RFC (bug 479) +!! input +RFC 822 +!! result +

RFC 822 +

+!! end + +!! test +Magic links: ISBN (bug 1937) +!! input +ISBN 0-306-40615-2 +!! result +

ISBN 0-306-40615-2 +

+!! end + +!! test +Magic links: PMID incorrectly converts space to underscore +!! input +PMID 1234 +!! result +

PMID 1234 +

+!! end + +### +### Templates +#### + +!! test +Nonexistent template +!! input +{{thistemplatedoesnotexist}} +!! result +

Template:Thistemplatedoesnotexist +

+!! end + +!! article +Template:test +!! text +This is a test template +!! endarticle + +!! test +Simple template +!! input +{{test}} +!! result +

This is a test template +

+!! end + +!! test +Template with explicit namespace +!! input +{{Template:test}} +!! result +

This is a test template +

+!! end + + +!! article +Template:paramtest +!! text +This is a test template with parameter {{{param}}} +!! endarticle + +!! test +Template parameter +!! input +{{paramtest|param=foo}} +!! result +

This is a test template with parameter foo +

+!! end + +!! article +Template:paramtestnum +!! text +[[{{{1}}}|{{{2}}}]] +!! endarticle + +!! test +Template unnamed parameter +!! input +{{paramtestnum|Main Page|the main page}} +!! result +

the main page +

+!! end + +!! article +Template:templatesimple +!! text +(test) +!! endarticle + +!! article +Template:templateredirect +!! text +#redirect [[Template:templatesimple]] +!! endarticle + +!! article +Template:templateasargtestnum +!! text +{{{{{1}}}}} +!! endarticle + +!! article +Template:templateasargtest +!! text +{{template{{{templ}}}}} +!! endarticle + +!! article +Template:templateasargtest2 +!! text +{{{{{templ}}}}} +!! endarticle + +!! test +Template with template name as unnamed argument +!! input +{{templateasargtestnum|templatesimple}} +!! result +

(test) +

+!! end + +!! test +Template with template name as argument +!! input +{{templateasargtest|templ=simple}} +!! result +

(test) +

+!! end + +!! test +Template with template name as argument (2) +!! input +{{templateasargtest2|templ=templatesimple}} +!! result +

(test) +

+!! end + +!! article +Template:templateasargtestdefault +!! text +{{{{{templ|templatesimple}}}}} +!! endarticle + +!! article +Template:templa +!! text +'''templ''' +!! endarticle + +!! test +Template with default value +!! input +{{templateasargtestdefault}} +!! result +

(test) +

+!! end + +!! test +Template with default value (value set) +!! input +{{templateasargtestdefault|templ=templa}} +!! result +

templ +

+!! end + +!! test +Template redirect +!! input +{{templateredirect}} +!! result +

(test) +

+!! end + +!! test +Template with argument in separate line +!! input +{{ templateasargtest | + templ = simple }} +!! result +

(test) +

+!! end + +!! test +Template with complex template as argument +!! input +{{paramtest| + param ={{ templateasargtest | + templ = simple }}}} +!! result +

This is a test template with parameter (test) +

+!! end + +!! test +Template with thumb image (with link in description) +!! input +{{paramtest| + param =[[Image:noimage.png|thumb|[[no link|link]] [[no link|caption]]]]}} +!! result +This is a test template with parameter + +!! end + +!! article +Template:complextemplate +!! text +{{{1}}} {{paramtest| + param ={{{param}}}}} +!! endarticle + +!! test +Template with complex arguments +!! input +{{complextemplate| + param ={{ templateasargtest | + templ = simple }}|[[Template:complextemplate|link]]}} +!! result +

link This is a test template with parameter (test) +

+!! end + +!! test +BUG 553: link with two variables in a piped link +!! input +{| +|[[{{{1}}}|{{{2}}}]] +|} +!! result + + +
[[{{{1}}}|{{{2}}}]] +
+ +!! end + +!! test +Magic variable as template parameter +!! input +{{paramtest|param={{SITENAME}}}} +!! result +

This is a test template with parameter MediaWiki +

+!! end + +!! article +Template:linktest +!! text +[[{{{param}}}|link]] +!! endarticle + +!! test +Template parameter as link source +!! input +{{linktest|param=Main Page}} +!! result +

link +

+!! end + + +!!article +Template:paramtest2 +!! text +including another template, {{paramtest|param={{{arg}}}}} +!! endarticle + +!! test +Template passing argument to another template +!! input +{{paramtest2|arg='hmm'}} +!! result +

including another template, This is a test template with parameter 'hmm' +

+!! end + +!! article +Template:Linktest2 +!! text +Main Page +!! endarticle + +!! test +Template as link source +!! input +[[{{linktest2}}]] +!! result +

Main Page +

+!! end + + +!! article +Template:loop1 +!! text +{{loop2}} +!! endarticle + +!! article +Template:loop2 +!! text +{{loop1}} +!! endarticle + +!! test +Template infinite loop +!! input +{{loop1}} +!! result +

Template loop detected: Template:Loop1 +

+!! end + +!! test +Template from main namespace +!! input +{{:Main Page}} +!! result +

blah blah +

+!! end + +!! article +Template:table +!! text +{| +| 1 || 2 +|- +| 3 || 4 +|} +!! endarticle + +!! test +BUG 529: Template with table, not included at beginning of line +!! input +foo {{table}} +!! result +

foo +

+ + + + + + +
1 2 +
3 4 +
+ +!! end + +!! test +BUG 523: Template shouldn't eat newline (or add an extra one before table) +!! input +foo +{{table}} +!! result +

foo +

+ + + + + + +
1 2 +
3 4 +
+ +!! end + +!! test +BUG 41: Template parameters shown as broken links +!! input +{{{parameter}}} +!! result +

{{{parameter}}} +

+!! end + + +!! article +Template:MSGNW test +!! text +''None'' of '''this''' should be +* interpreted + but rather passed unmodified +{{test}} +!! endarticle + +# hmm, fix this or just deprecate msgnw and document its behavior? +!! test +msgnw keyword +!! options +disabled +!! input +{{msgnw:MSGNW test}} +!! result +

''None'' of '''this''' should be +* interpreted + but rather passed unmodified +{{test}} +

+!! end + +!! test +int keyword +!! input +{{int:youhavenewmessages|lots of money|not!}} +!! result +

You have lots of money (not!). +

+!! end + +!! article +Template:Includes +!! text +Foozarbar +!! endarticle + +!! test + and being included +!! input +{{Includes}} +!! result +

Foobar +

+!! end + +!! article +Template:Includes2 +!! text +Foobar +!! endarticle + +!! test + being included +!! input +{{Includes2}} +!! result +

Foo +

+!! end + + +!! article +Template:Includes3 +!! text +Foobarzar +!! endarticle + +!! test + and being included +!! input +{{Includes3}} +!! result +

Foo +

+!! end + +!! test + and on a page +!! input +Foozarbar +!! result +

Foozar +

+!! end + +!! test + on a page +!! input +Foobar +!! result +

Foobar +

+!! end + +!! article +Template:Includeonly section +!! text + +==Includeonly section== + +==Section T-1== +!!endarticle + +!! test +Bug 6563: Edit link generation for section shown by +!! input +{{includeonly section}} +!! result +

[edit] Includeonly section

+

[edit] Section T-1

+ +!! end + +# Uses same input as the contents of [[Template:Includeonly section]] +!! test +Bug 6563: Section extraction for section shown by +!! options +section=T-2 +!! input + +==Includeonly section== + +==Section T-2== +!! result +==Section T-2== +!! end + +!! test +Bug 6563: Edit link generation for section suppressed by +!! input + +==Includeonly section== + +==Section 1== +!! result +

[edit] Section 1

+ +!! end + +!! test +Bug 6563: Section extraction for section suppressed by +!! options +section=1 +!! input + +==Includeonly section== + +==Section 1== +!! result +==Section 1== +!! end + +### +### Pre-save transform tests +### +!! test +pre-save transform: subst: +!! options +PST +!! input +{{subst:test}} +!! result +This is a test template +!! end + +!! test +pre-save transform: normal template +!! options +PST +!! input +{{test}} +!! result +{{test}} +!! end + +!! test +pre-save transform: nonexistent template +!! options +PST +!! input +{{thistemplatedoesnotexist}} +!! result +{{thistemplatedoesnotexist}} +!! end + + +!! test +pre-save transform: subst magic variables +!! options +PST +!! input +{{subst:SITENAME}} +!! result +MediaWiki +!! end + +# This is bug 89, which I fixed. -- wtm +!! test +pre-save transform: subst: templates with parameters +!! options +pst +!! input +{{subst:paramtest|param="something else"}} +!! result +This is a test template with parameter "something else" +!! end + +!! article +Template:nowikitest +!! text +'''not wiki''' +!! endarticle + +!! test +pre-save transform: nowiki in subst (bug 1188) +!! options +pst +!! input +{{subst:nowikitest}} +!! result +'''not wiki''' +!! end + + +!! article +Template:commenttest +!! text +This template has in it. +!! endarticle + +!! test +pre-save transform: comment in subst (bug 1936) +!! options +pst +!! input +{{subst:commenttest}} +!! result +This template has in it. +!! end + +!! test +pre-save transform: unclosed tag +!! options +pst noxml +!! input +'''not wiki''' +!! result +'''not wiki''' +!! end + +!! test +pre-save transform: mixed tag case +!! options +pst noxml +!! input +'''not wiki''' +!! result +'''not wiki''' +!! end + +!! test +pre-save transform: unclosed comment in +!! options +pst noxml +!! input +wikinowiki +!!result + +!!end + +!! test +pre-save transform: comment containing extension +!! options +pst +!! input + +!!result + +!!end + +!! test +pre-save transform: comment containing nowiki +!! options +pst +!! input + +!!result + +!!end + +!! test +pre-save transform: comment containing math +!! options +pst +!! input + +!!result + +!!end + +!! test +pre-save transform: in subst (bug 3298) +!! options +pst +!! input +{{subst:Includes}} +!! result +Foobar +!! end + +!! test +pre-save transform: in subst (bug 3298) +!! options +pst +!! input +{{subst:Includes2}} +!! result +Foo +!! end + +!! article +Template:SubstTest +!!text +{{subst:Includes}} +!! endarticle + +!! article +Template:SafeSubstTest +!! text +{{safesubst:Includes}} +!! endarticle + +!! test +bug 22297: safesubst: works during PST +!! options +pst +!! input +{{subst:SafeSubstTest}}{{safesubst:SubstTest}} +!! result +FoobarFoobar +!! end + +!! test +bug 22297: safesubst: works during normal parse +!! input +{{SafeSubstTest}} +!! result +

Foobar +

+!! end + +!! test: +subst: does not work during normal parse +!! input +{{SubstTest}} +!! result +

{{subst:Includes}} +

+!! end + +!! test +pre-save transform: context links ("pipe trick") +!! options +pst +!! input +[[Article (context)|]] +[[Bar:Article|]] +[[:Bar:Article|]] +[[Bar:Article (context)|]] +[[:Bar:Article (context)|]] +[[|Article]] +[[|Article (context)]] +[[Bar:X (Y) Z|]] +[[:Bar:X (Y) Z|]] +!! result +[[Article (context)|Article]] +[[Bar:Article|Article]] +[[:Bar:Article|Article]] +[[Bar:Article (context)|Article]] +[[:Bar:Article (context)|Article]] +[[Article]] +[[Article (context)]] +[[Bar:X (Y) Z|X (Y) Z]] +[[:Bar:X (Y) Z|X (Y) Z]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with interwiki prefix +!! options +pst +!! input +[[interwiki:Article|]] +[[:interwiki:Article|]] +[[interwiki:Bar:Article|]] +[[:interwiki:Bar:Article|]] +!! result +[[interwiki:Article|Article]] +[[:interwiki:Article|Article]] +[[interwiki:Bar:Article|Bar:Article]] +[[:interwiki:Bar:Article|Bar:Article]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with parens in title +!! options +pst title=[[Somearticle (context)]] +!! input +[[|Article]] +!! result +[[Article (context)|Article]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with comma in title +!! options +pst title=[[Someplace, Somewhere]] +!! input +[[|Otherplace]] +[[Otherplace, Elsewhere|]] +[[Otherplace, Elsewhere, Anywhere|]] +!! result +[[Otherplace, Somewhere|Otherplace]] +[[Otherplace, Elsewhere|Otherplace]] +[[Otherplace, Elsewhere, Anywhere|Otherplace]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with parens and comma +!! options +pst title=[[Someplace (IGNORED), Somewhere]] +!! input +[[|Otherplace]] +[[Otherplace (place), Elsewhere|]] +!! result +[[Otherplace, Somewhere|Otherplace]] +[[Otherplace (place), Elsewhere|Otherplace]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with comma and parens +!! options +pst title=[[Who, me? (context)]] +!! input +[[|Yes, you.]] +[[Me, Myself, and I (1937 song)|]] +!! result +[[Yes, you. (context)|Yes, you.]] +[[Me, Myself, and I (1937 song)|Me, Myself, and I]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with namespace +!! options +pst title=[[Ns:Somearticle]] +!! input +[[|Article]] +!! result +[[Ns:Article|Article]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with namespace and parens +!! options +pst title=[[Ns:Somearticle (context)]] +!! input +[[|Article]] +!! result +[[Ns:Article (context)|Article]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with namespace and comma +!! options +pst title=[[Ns:Somearticle, Context, Whatever]] +!! input +[[|Article]] +!! result +[[Ns:Article, Context, Whatever|Article]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with namespace, comma and parens +!! options +pst title=[[Ns:Somearticle, Context (context)]] +!! input +[[|Article]] +!! result +[[Ns:Article (context)|Article]] +!! end + +!! test +pre-save transform: context links ("pipe trick") with namespace, parens and comma +!! options +pst title=[[Ns:Somearticle (IGNORED), Context]] +!! input +[[|Article]] +!! result +[[Ns:Article, Context|Article]] +!! end + + +### +### Message transform tests +### +!! test +message transform: magic variables +!! options +msg +!! input +{{SITENAME}} +!! result +MediaWiki +!! end + +!! test +message transform: should not transform wiki markup +!! options +msg +!! input +''test'' +!! result +''test'' +!! end + +!! test +message transform: in transcluded template (bug 4926) +!! options +msg +!! input +{{Includes}} +!! result +Foobar +!! end + +!! test +message transform: in transcluded template (bug 4926) +!! options +msg +!! input +{{Includes2}} +!! result +Foo +!! end + +!! test +{{#special:}} page name, known +!! options +msg +!! input +{{#special:Recentchanges}} +!! result +Special:RecentChanges +!! end + +!! test +{{#special:}} page name with subpage, known +!! options +msg +!! input +{{#special:Recentchanges/param}} +!! result +Special:RecentChanges/param +!! end + +!! test +{{#special:}} page name, unknown +!! options +msg +!! input +{{#special:foobarnonexistent}} +!! result +No such special page +!! end + +### +### Images +### +!! test +Simple image +!! input +[[Image:foobar.jpg]] +!! result +

Foobar.jpg +

+!! end + +!! test +Right-aligned image +!! input +[[Image:foobar.jpg|right]] +!! result +
Foobar.jpg
+ +!! end + +!! test +Simple image (using File: namespace, now canonical) +!! input +[[File:foobar.jpg]] +!! result +

Foobar.jpg +

+!! end + +!! test +Image with caption +!! input +[[Image:foobar.jpg|right|Caption text]] +!! result +
Caption text
+ +!! end + +!! test +Image with link parameter, wiki target +!! input +[[Image:foobar.jpg|link=Target page]] +!! result +

Foobar.jpg +

+!! end + +!! test +Image with link parameter, URL target +!! input +[[Image:foobar.jpg|link=http://example.com/]] +!! result +

Foobar.jpg +

+!! end + +!! test +Image with empty link parameter +!! input +[[Image:foobar.jpg|link=]] +!! result +

Foobar.jpg +

+!! end + +!! test +Image with link parameter (wiki target) and unnamed parameter +!! input +[[Image:foobar.jpg|link=Target page|Title]] +!! result +

Title +

+!! end + +!! test +Image with link parameter (URL target) and unnamed parameter +!! input +[[Image:foobar.jpg|link=http://example.com/|Title]] +!! result +

Title +

+!! end + +!! test +Thumbnail image with link parameter +!! input +[[Image:foobar.jpg|thumb|link=http://example.com/|Title]] +!! result +
Title
+ +!! end + +!! test +Image with frame and link +!! input +[[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]]] +!! result +
This is a test image Main Page
+ +!! end + +!! test +Image with frame and link and explicit alt +!! input +[[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]|alt=Altitude]] +!! result +
Altitude
This is a test image Main Page
+ +!! end + +!! test +Image with wiki markup in implicit alt +!! input +[[Image:Foobar.jpg|testing '''bold''' in alt]] +!! result +

testing bold in alt +

+!! end + +!! test +Image with wiki markup in explicit alt +!! input +[[Image:Foobar.jpg|alt=testing '''bold''' in alt]] +!! result +

testing bold in alt +

+!! end + +!! test +Link to image page- image page normally doesn't exists, hence edit link +Add test with existing image page +#

Image:test +!! input +[[:Image:test]] +!! result +

Image:test +

+!! end + +!! test +bug 18784 Link to non-existent image page with caption should use caption as link text +!! input +[[:Image:test|caption]] +!! result +

caption +

+!! end + +!! test +Frameless image caption with a free URL +!! input +[[Image:foobar.jpg|http://example.com]] +!! result +

http://example.com +

+!! end + +!! test +Thumbnail image caption with a free URL +!! input +[[Image:foobar.jpg|thumb|http://example.com]] +!! result + + +!! end + +!! test +Thumbnail image caption with a free URL and explicit alt +!! input +[[Image:foobar.jpg|thumb|http://example.com|alt=Alteration]] +!! result + + +!! end + +!! test +BUG 1887: A ISBN with a thumbnail +!! input +[[Image:foobar.jpg|thumb|ISBN 1235467890]] +!! result + + +!! end + +!! test +BUG 1887: A RFC with a thumbnail +!! input +[[Image:foobar.jpg|thumb|This is RFC 12354]] +!! result +
This is RFC 12354
+ +!! end + +!! test +BUG 1887: A mailto link with a thumbnail +!! input +[[Image:foobar.jpg|thumb|Please mailto:nobody@example.com]] +!! result + + +!! end + +!! test +BUG 1887: A with a thumbnail- we don't render math in the parsertests by default, +so math is not stripped and turns up as escaped <math> tags. +!! input +[[Image:foobar.jpg|thumb|2+2]] +!! result +
<math>2+2</math>
+ +!! end + +!! test +BUG 1887, part 2: A with a thumbnail- math enabled +!! options +math +!! input +[[Image:foobar.jpg|thumb|2+2]] +!! result +
2 + 2
+ +!! end + +# Pending resolution to bug 368 +!! test +BUG 648: Frameless image caption with a link +!! input +[[Image:foobar.jpg|text with a [[link]] in it]] +!! result +

text with a link in it +

+!! end + +!! test +BUG 648: Frameless image caption with a link (suffix) +!! input +[[Image:foobar.jpg|text with a [[link]]foo in it]] +!! result +

text with a linkfoo in it +

+!! end + +!! test +BUG 648: Frameless image caption with an interwiki link +!! input +[[Image:foobar.jpg|text with a [[MeatBall:Link]] in it]] +!! result +

text with a MeatBall:Link in it +

+!! end + +!! test +BUG 648: Frameless image caption with a piped interwiki link +!! input +[[Image:foobar.jpg|text with a [[MeatBall:Link|link]] in it]] +!! result +

text with a link in it +

+!! end + +!! test +Escape HTML special chars in image alt text +!! input +[[Image:foobar.jpg|& < > "]] +!! result +

& < > " +

+!! end + +!! test +BUG 499: Alt text should have Ӓ, not &1234; +!! input +[[Image:foobar.jpg|♀]] +!! result +

♀ +

+!! end + +!! test +Broken image caption with link +!! input +[[Image:Foobar.jpg|thumb|This is a broken caption. But [[Main Page|this]] is just an ordinary link. +!! result +

[[Image:Foobar.jpg|thumb|This is a broken caption. But this is just an ordinary link. +

+!! end + +!! test +Image caption containing another image +!! input +[[Image:Foobar.jpg|thumb|This is a caption with another [[Image:icon.png|image]] inside it!]] +!! result +
This is a caption with another image inside it!
+ +!! end + +!! test +Image caption containing a newline +!! input +[[Image:Foobar.jpg|This +*is some text]] +!! result +

This *is some text +

+!!end + + +!! test +Bug 3090: External links other than http: in image captions +!! input +[[Image:Foobar.jpg|thumb|200px|This caption has [irc://example.net irc] and [https://example.com Secure] ext links in it.]] +!! result +
This caption has irc and Secure ext links in it.
+ +!! end + +!! article +File:Barfoo.jpg +!! text +#REDIRECT [[File:Barfoo.jpg]] +!! endarticle + +!! test +Redirected image +!! input +[[Image:Barfoo.jpg]] +!! result +

File:Barfoo.jpg +

+!! end + +!! test +Missing image with uploads disabled +!! options +wgEnableUploads=0 +!! input +[[Image:Foobaz.jpg]] +!! result +

File:Foobaz.jpg +

+!! end + + +### +### Subpages +### +!! article +Subpage test/subpage +!! text +foo +!! endarticle + +!! test +Subpage link +!! options +subpage title=[[Subpage test]] +!! input +[[/subpage]] +!! result +

/subpage +

+!! end + +!! test +Subpage noslash link +!! options +subpage title=[[Subpage test]] +!!input +[[/subpage/]] +!! result +

subpage +

+!! end + +!! test +Disabled subpages +!! input +[[/subpage]] +!! result +

/subpage +

+!! end + +!! test +BUG 561: {{/Subpage}} +!! options +subpage title=[[Page]] +!! input +{{/Subpage}} +!! result +

Page/Subpage +

+!! end + +### +### Categories +### +!! article +Category:MediaWiki User's Guide +!! text +blah +!! endarticle + +!! test +Link to category +!! input +[[:Category:MediaWiki User's Guide]] +!! result +

Category:MediaWiki User's Guide +

+!! end + +!! test +Simple category +!! options +cat +!! input +[[Category:MediaWiki User's Guide]] +!! result +MediaWiki User's Guide +!! end + +!! test +PAGESINCATEGORY invalid title fatal (r33546 fix) +!! input +{{PAGESINCATEGORY:}} +!! result +

0 +

+!! end + +### +### Inter-language links +### +!! test +Inter-language links +!! options +ill +!! input +[[es:Alimento]] +[[fr:Nourriture]] +[[zh:食品]] +!! result +es:Alimento fr:Nourriture zh:食品 +!! end + +### +### Sections +### +!! test +Basic section headings +!! input +== Headline 1 == +Some text + +==Headline 2== +More +===Smaller headline=== +Blah blah +!! result +

[edit] Headline 1

+

Some text +

+

[edit] Headline 2

+

More +

+

[edit] Smaller headline

+

Blah blah +

+!! end + +!! test +Section headings with TOC +!! input +== Headline 1 == +=== Subheadline 1 === +===== Skipping a level ===== +====== Skipping a level ====== + +== Headline 2 == +Some text +===Another headline=== +!! result +

Contents

+ +
+

[edit] Headline 1

+

[edit] Subheadline 1

+
[edit] Skipping a level
+
[edit] Skipping a level
+

[edit] Headline 2

+

Some text +

+

[edit] Another headline

+ +!! end + +# perl -e 'print "="x$_," Level $_ heading","="x$_,"\n" for 1..10' +!! test +Handling of sections up to level 6 and beyond +!! input += Level 1 Heading= +== Level 2 Heading== +=== Level 3 Heading=== +==== Level 4 Heading==== +===== Level 5 Heading===== +====== Level 6 Heading====== +======= Level 7 Heading======= +======== Level 8 Heading======== +========= Level 9 Heading========= +========== Level 10 Heading========== +!! result +

Contents

+ +
+

[edit] Level 1 Heading

+

[edit] Level 2 Heading

+

[edit] Level 3 Heading

+

[edit] Level 4 Heading

+
[edit] Level 5 Heading
+
[edit] Level 6 Heading
+
[edit] = Level 7 Heading=
+
[edit] == Level 8 Heading==
+
[edit] === Level 9 Heading===
+
[edit] ==== Level 10 Heading====
+ +!! end + +!! test +TOC regression (bug 9764) +!! input +== title 1 == +=== title 1.1 === +==== title 1.1.1 ==== +=== title 1.2 === +== title 2 == +=== title 2.1 === +!! result +

Contents

+ +
+

[edit] title 1

+

[edit] title 1.1

+

[edit] title 1.1.1

+

[edit] title 1.2

+

[edit] title 2

+

[edit] title 2.1

+ +!! end + +!! test +TOC with wgMaxTocLevel=3 (bug 6204) +!! options +wgMaxTocLevel=3 +!! input +== title 1 == +=== title 1.1 === +==== title 1.1.1 ==== +=== title 1.2 === +== title 2 == +=== title 2.1 === +!! result +

Contents

+ +
+

[edit] title 1

+

[edit] title 1.1

+

[edit] title 1.1.1

+

[edit] title 1.2

+

[edit] title 2

+

[edit] title 2.1

+ +!! end + +!! test +TOC with wgMaxTocLevel=3 and two level four headings (bug 6204) +!! options +wgMaxTocLevel=3 +!! input +==Section 1== +===Section 1.1=== +====Section 1.1.1==== +====Section 1.1.1.1==== +==Section 2== +!! result +

Contents

+ +
+

[edit] Section 1

+

[edit] Section 1.1

+

[edit] Section 1.1.1

+

[edit] Section 1.1.1.1

+

[edit] Section 2

+ +!! end + + +!! test +Resolving duplicate section names +!! input +== Foo bar == +== Foo bar == +!! result +

[edit] Foo bar

+

[edit] Foo bar

+ +!! end + +!! test +Resolving duplicate section names with differing case (bug 10721) +!! input +== Foo bar == +== Foo Bar == +!! result +

[edit] Foo bar

+

[edit] Foo Bar

+ +!! end + +!! article +Template:sections +!! text +===Section 1=== +==Section 2== +!! endarticle + +!! test +Template with sections, __NOTOC__ +!! input +__NOTOC__ +==Section 0== +{{sections}} +==Section 4== +!! result +

[edit] Section 0

+

[edit] Section 1

+

[edit] Section 2

+

[edit] Section 4

+ +!! end + +!! test +__NOEDITSECTION__ keyword +!! input +__NOEDITSECTION__ +==Section 1== +==Section 2== +!! result +

Section 1

+

Section 2

+ +!! end + +!! test +Link inside a section heading +!! input +==Section with a [[Main Page|link]] in it== +!! result +

[edit] Section with a link in it

+ +!! end + +!! test +TOC regression (bug 12077) +!! input +__TOC__ +== title 1 == +=== title 1.1 === +== title 2 == +!! result +

Contents

+ +
+

[edit] title 1

+

[edit] title 1.1

+

[edit] title 2

+ +!! end + +!! test +BUG 1219 URL next to image (good) +!! input +http://example.com [[Image:foobar.jpg]] +!! result +

http://example.com Foobar.jpg +

+!!end + +!! test +Short headings with trailing space should match behaviour of Parser::doHeadings (bug 19910) +!! input +=== +The line above must have a trailing space! +=== +But just in case it doesn't... +!! result +

[edit] =

+

The line above must have a trailing space! +

+

[edit] =

+

But just in case it doesn't... +

+!! end + +!! test +Header with special characters (bug 25462) +!! input +The tooltips shall not show entities to the user (ie. be double escaped) + +== text > text == +section 1 + +== text < text == +section 2 + +== text & text == +section 3 + +== text ' text == +section 4 + +== text " text == +section 5 +!! result +

The tooltips shall not show entities to the user (ie. be double escaped) +

+

Contents

+ +
+

[edit] text > text

+

section 1 +

+

[edit] text < text

+

section 2 +

+

[edit] text & text

+

section 3 +

+

[edit] text ' text

+

section 4 +

+

[edit] text " text

+

section 5 +

+!! end + +!! test +BUG 1219 URL next to image (broken) +!! input +http://example.com[[Image:foobar.jpg]] +!! result +

http://example.comFoobar.jpg +

+!!end + +!! test +Bug 1186 news: in the middle of text +!! input +http://en.wikinews.org/wiki/Wikinews:Workplace +!! result +

http://en.wikinews.org/wiki/Wikinews:Workplace +

+!!end + + +!! test +Namespaced link must have a title +!! input +[[Project:]] +!! result +

[[Project:]] +

+!!end + +!! test +Namespaced link must have a title (bad fragment version) +!! input +[[Project:#fragment]] +!! result +

[[Project:#fragment]] +

+!!end + + +!! test +div with no attributes +!! input +
HTML rocks
+!! result +
HTML rocks
+ +!! end + +!! test +div with double-quoted attribute +!! input +
HTML rocks
+!! result +
HTML rocks
+ +!! end + +!! test +div with single-quoted attribute +!! input +
HTML rocks
+!! result +
HTML rocks
+ +!! end + +!! test +div with unquoted attribute +!! input +
HTML rocks
+!! result +
HTML rocks
+ +!! end + +!! test +div with illegal double attributes +!! input +
HTML rocks
+!! result +
HTML rocks
+ +!!end + +!! test +HTML multiple attributes correction +!! input +

Awesome!

+!! result +

Awesome!

+ +!!end + +!! test +Table multiple attributes correction +!! input +{| +!+ class="error" class="awesome"| status +|} +!! result + + +
status +
+ +!!end + +!! test +DIV IN UPPERCASE +!! input +
HTML ROCKS
+!! result +
HTML ROCKS
+ +!!end + + +!! test +text with amp in the middle of nowhere +!! input +Remember AT&T? +!!result +

Remember AT&T? +

+!! end + +!! test +text with character entity: eacute +!! input +I always thought é was a cute letter. +!! result +

I always thought é was a cute letter. +

+!! end + +!! test +text with undefined character entity: xacute +!! input +I always thought &xacute; was a cute letter. +!! result +

I always thought &xacute; was a cute letter. +

+!! end + + +### +### Media links +### + +!! test +Media link +!! input +[[Media:Foobar.jpg]] +!! result +

Media:Foobar.jpg +

+!! end + +!! test +Media link with text +!! input +[[Media:Foobar.jpg|A neat file to look at]] +!! result +

A neat file to look at +

+!! end + +# FIXME: this is still bad HTML tag nesting +!! test +Media link with nasty text +fixme: doBlockLevels won't wrap this in a paragraph because it contains a div +!! input +[[Media:Foobar.jpg|Safe Link
" onmouseover="alert(document.cookie)" onfoo="
]] +!! result +Safe Link<div style="display:none">" onmouseover="alert(document.cookie)" onfoo="</div> + +!! end + +!! test +Media link to nonexistent file (bug 1702) +!! input +[[Media:No such.jpg]] +!! result +

Media:No such.jpg +

+!! end + +!! test +Image link to nonexistent file (bug 1850 - good) +!! input +[[Image:No such.jpg]] +!! result +

File:No such.jpg +

+!! end + +!! test +:Image link to nonexistent file (bug 1850 - bad) +!! input +[[:Image:No such.jpg]] +!! result +

Image:No such.jpg +

+!! end + + + +!! test +Character reference normalization in link text (bug 1938) +!! input +[[Main Page|this&that]] +!! result +

this&that +

+!!end + +!! article +אַ +!! text +Test for unicode normalization + +The page's name is U+05d0 U+05b7, with non-canonical form U+FB2E +!! endarticle + +!! test +(bug 19451) Links should refer to the normalized form. +!! input +[[אַ]] +[[אַ]] +[[אÖ·]] +[[אַ]] +[[אַ]] +!! result +

+אַ +אÖ· +אַ +אַ +

+!! end + +!! test +Empty attribute crash test (bug 2067) +!! input +foo +!! result +

foo +

+!! end + +!! test +Empty attribute crash test single-quotes (bug 2067) +!! input +foo +!! result +

foo +

+!! end + +!! test +Attribute test: equals, then nothing +!! input +foo +!! result +

foo +

+!! end + +!! test +Attribute test: unquoted value +!! input +foo +!! result +

foo +

+!! end + +!! test +Attribute test: unquoted but illegal value (hash) +!! input +foo +!! result +

foo +

+!! end + +!! test +Attribute test: no value +!! input +foo +!! result +

foo +

+!! end + +!! test +Bug 2095: link with three closing brackets +!! input +[[Main Page]]] +!! result +

Main Page] +

+!! end + +!! test +Bug 2095: link with pipe and three closing brackets +!! input +[[Main Page|link]]] +!! result +

link] +

+!! end + +!! test +Bug 2095: link with pipe and three closing brackets, version 2 +!! input +[[Main Page|[http://example.com/]]] +!! result +

[http://example.com/] +

+!! end + + +### +### Safety +### + +!! article +Template:Dangerous attribute +!! text +" onmouseover="alert(document.cookie) +!! endarticle + +!! article +Template:Dangerous style attribute +!! text +border-size: expression(alert(document.cookie)) +!! endarticle + +!! article +Template:Div style +!! text +
Magic div
+!! endarticle + +!! test +Bug 2304: HTML attribute safety (safe template; regression bug 2309) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (dangerous template; 2309) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (dangerous style template; 2309) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (safe parameter; 2309) +!! input +{{div style|width: 200px}} +!! result +
Magic div
+ +!! end + +!! test +Bug 2304: HTML attribute safety (unsafe parameter; 2309) +!! input +{{div style|width: expression(alert(document.cookie))}} +!! result +
Magic div
+ +!! end + +!! test +Bug 2304: HTML attribute safety (unsafe breakout parameter; 2309) +!! input +{{div style|">}} +!! result +
<script>alert(document.cookie)</script>">Magic div
+ +!! end + +!! test +Bug 2304: HTML attribute safety (unsafe breakout parameter 2; 2309) +!! input +{{div style|" >}} +!! result +
<script>alert(document.cookie)</script>">Magic div
+ +!! end + +!! test +Bug 2304: HTML attribute safety (link) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (italics) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (bold) +!! input +
+!! result +
+ +!! end + + +!! test +Bug 2304: HTML attribute safety (ISBN) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (RFC) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (PMID) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (web link) +!! input +
+!! result +
+ +!! end + +!! test +Bug 2304: HTML attribute safety (named web link) +!! input +
+!! result +
+ +!! end + +!! test +Bug 3244: HTML attribute safety (extension; safe) +!! input +
+!! result +
+ +!! end + +!! test +Bug 3244: HTML attribute safety (extension; unsafe) +!! input +
+!! result +
+ +!! end + +!! test +Math section safety when disabled +!! input + +!! result +

<math><script>alert(document.cookies);</script></math> +

+!! end + +# More MSIE fun discovered by Tom Gilder + +!! test +MSIE CSS safety test: spurious slash +!! input +
evil
+!! result +
evil
+ +!! end + +!! test +MSIE CSS safety test: hex code +!! input +
evil
+!! result +
evil
+ +!! end + +!! test +MSIE CSS safety test: comment in url +!! input +
evil
+!! result +
evil
+ +!! end + +!! test +MSIE CSS safety test: comment in expression +!! input +
evil4
+!! result +
evil4
+ +!! end + + +!! test +Table attribute legitimate extension +!! input +{| +!+ style="color:blue"| status +|} +!! result + + +
status +
+ +!!end + +!! test +Table attribute safety +!! input +{| +!+ style="border-width:expression(0+alert(document.cookie))"| status +|} +!! result + + +
status +
+ +!! end + +!! test +CSS line continuation 1 +!! input +
+!! result +
+ +!! end + +!! test +CSS line continuation 2 +!! input +
+!! result +
+ +!! end + +!! article +Template:Identity +!! text +{{{1}}} +!! endarticle + +!! test +Expansion of multi-line templates in attribute values (bug 6255) +!! input +
-
+!! result +
-
+ +!! end + + +!! test +Expansion of multi-line templates in attribute values (bug 6255 sanity check) +!! input +
-
+!! result +
-
+ +!! end + +!! test +Expansion of multi-line templates in attribute values (bug 6255 sanity check 2) +!! input +
-
+!! result +
-
+ +!! end + +### +### Parser hooks (see maintenance/parserTestsParserHook.php for the extension) +### +!! test +Parser hook: empty input +!! input + +!! result +
+string(0) ""
+array(0) {
+}
+
+ +!! end + +!! test +Parser hook: empty input using terminated empty elements +!! input + +!! result +
+NULL
+array(0) {
+}
+
+ +!! end + +!! test +Parser hook: empty input using terminated empty elements (space before) +!! input + +!! result +
+NULL
+array(0) {
+}
+
+ +!! end + +!! test +Parser hook: basic input +!! input +input +!! result +
+string(5) "input"
+array(0) {
+}
+
+ +!! end + + +!! test +Parser hook: case insensitive +!! input +input +!! result +
+string(5) "input"
+array(0) {
+}
+
+ +!! end + + +!! test +Parser hook: case insensitive, redux +!! input +input +!! result +
+string(5) "input"
+array(0) {
+}
+
+ +!! end + +!! test +Parser hook: nested tags +!! options +noxml +!! input + +!! result +
+string(5) ""
+array(0) {
+}
+
</tag> + +!! end + +!! test +Parser hook: basic arguments +!! input + +!! result +
+string(0) ""
+array(4) {
+  ["width"]=>
+  string(3) "200"
+  ["height"]=>
+  string(3) "100"
+  ["depth"]=>
+  string(2) "50"
+  ["square"]=>
+  string(6) "square"
+}
+
+ +!! end + +!! test +Parser hook: argument containing a forward slash (bug 5344) +!! input + +!! result +
+string(0) ""
+array(1) {
+  ["filename"]=>
+  string(8) "/tmp/bla"
+}
+
+ +!! end + +!! test +Parser hook: empty input using terminated empty elements (bug 2374) +!! input +text +!! result +
+NULL
+array(1) {
+  ["foo"]=>
+  string(3) "bar"
+}
+
text + +!! end + +#
should be output literally since there is no matching tag that begins it +!! test +Parser hook: basic arguments using terminated empty elements (bug 2374) +!! input + +other stuff + +!! result +
+NULL
+array(4) {
+  ["width"]=>
+  string(3) "200"
+  ["height"]=>
+  string(3) "100"
+  ["depth"]=>
+  string(2) "50"
+  ["square"]=>
+  string(6) "square"
+}
+
+

other stuff +</tag> +

+!! end + +### +### (see maintenance/parserTestsStaticParserHook.php for the extension) +### + +!! test +Parser hook: static parser hook not inside a comment +!! input +hello, world + +!! result +

hello, world +

+!! end + + +!! test +Parser hook: static parser hook inside a comment +!! input + + +!! result +


+

+!! end + +# Nested template calls; this case was broken by Parser.php rev 1.506, +# since reverted. + +!! article +Template:One-parameter +!! text +(My parameter is: {{{1}}}) +!! endarticle + +!! article +Template:Map-one-parameter +!! text +{{{{{1}}}|{{{2}}}}} +!! endarticle + +!! test +Nested template calls +!! input +{{Map-one-parameter|One-parameter|param}} +!! result +

(My parameter is: param) +

+!! end + + +### +### Sanitizer +### +!! test +Sanitizer: Closing of open tags +!! input +
+!! result +
+ +!! end + +!! test +Sanitizer: Closing of open but not closed tags +!! input +foo +!! result +

foo +

+!! end + +!! test +Sanitizer: Closing of closed but not open tags +!! input +
+!! result +

</s> +

+!! end + +!! test +Sanitizer: Closing of closed but not open table tags +!! input +Table not started +!! result +

Table not started</td></tr></table> +

+!! end + +!! test +Sanitizer: Escaping of spaces, multibyte characters, colons & other stuff in id="" +!! input +byte[[#æ: v|backlink]] +!! result +

bytebacklink +

+!! end + +!! test +Sanitizer: Validating the contents of the id attribute (bug 4515) +!! options +disabled +!! input +
+!! result +Something, but definitely not
... +!! end + +!! test +Sanitizer: Validating id attribute uniqueness (bug 4515, bug 6301) +!! options +disabled +!! input +

+!! result +Something need to be done. foo-2 ? +!! end + +!! test +Language converter: output gets cut off unexpectedly (bug 5757) +!! options +language=zh +!! input +this bit is safe: }- + +but if we add a conversion instance: -{zh-cn:xxx;zh-tw:yyy}- + +then we get cut off here: }- + +all additional text is vanished +!! result +

this bit is safe: }- +

but if we add a conversion instance: xxx +

then we get cut off here: }- +

all additional text is vanished +

+!! end + +!! test +Self closed html pairs (bug 5487) +!! options +!! input +
Centered text
+
In div text
+!! result +
<font id="bug" />Centered text
+
<font id="bug2" />In div text
+ +!! end + +# +# +# + +!! test +Punctuation: nbsp before exclamation +!! input +C'est grave ! +!! result +

C'est grave ! +

+!! end + +!! test +Punctuation: CSS !important (bug 11874) +!! input +
important
+!! result +
important
+ +!!end + +!! test +Punctuation: CSS ! important (bug 11874; with space after) +!! input +
important
+!! result +
important
+ +!!end + + +!! test +HTML bullet list, closed tags (bug 5497) +!! input +
    +
  • One
  • +
  • Two
  • +
+!! result +
    +
  • One
  • +
  • Two
  • +
+ +!! end + +!! test +HTML bullet list, unclosed tags (bug 5497) +!! options +disabled +!! input +
    +
  • One +
  • Two +
+!! result +
    +
  • One +
  • Two +
+ +!! end + +!! test +HTML ordered list, closed tags (bug 5497) +!! input +
    +
  1. One
  2. +
  3. Two
  4. +
+!! result +
    +
  1. One
  2. +
  3. Two
  4. +
+ +!! end + +!! test +HTML ordered list, unclosed tags (bug 5497) +!! options +disabled +!! input +
    +
  1. One +
  2. Two +
+!! result +
    +
  1. One +
  2. Two +
+ +!! end + +!! test +HTML nested bullet list, closed tags (bug 5497) +!! input +
    +
  • One
  • +
  • Two: +
      +
    • Sub-one
    • +
    • Sub-two
    • +
    +
  • +
+!! result +
    +
  • One
  • +
  • Two: +
      +
    • Sub-one
    • +
    • Sub-two
    • +
    +
  • +
+ +!! end + +!! test +HTML nested bullet list, open tags (bug 5497) +!! options +disabled +!! input +
    +
  • One +
  • Two: +
      +
    • Sub-one +
    • Sub-two +
    +
+!! result +
    +
  • One +
  • Two: +
      +
    • Sub-one +
    • Sub-two +
    +
+ +!! end + +!! test +HTML nested ordered list, closed tags (bug 5497) +!! input +
    +
  1. One
  2. +
  3. Two: +
      +
    1. Sub-one
    2. +
    3. Sub-two
    4. +
    +
  4. +
+!! result +
    +
  1. One
  2. +
  3. Two: +
      +
    1. Sub-one
    2. +
    3. Sub-two
    4. +
    +
  4. +
+ +!! end + +!! test +HTML nested ordered list, open tags (bug 5497) +!! options +disabled +!! input +
    +
  1. One +
  2. Two: +
      +
    1. Sub-one +
    2. Sub-two +
    +
+!! result +
    +
  1. One +
  2. Two: +
      +
    1. Sub-one +
    2. Sub-two +
    +
+ +!! end + +!! test +HTML ordered list item with parameters oddity +!! input +
  1. One
+!! result +
  1. One
+ +!! end + +!!test +bug 5918: autonumbering +!! input +[http://first/] [http://second] [ftp://ftp] + +ftp://inlineftp + +[mailto:enclosed@mail.tld With target] + +[mailto:enclosed@mail.tld] + +mailto:inline@mail.tld +!! result +

[1] [2] [3] +

ftp://inlineftp +

With target +

[4] +

mailto:inline@mail.tld +

+!! end + + +# +# Security and HTML correctness +# From Nick Jenkins' fuzz testing +# + +!! test +Fuzz testing: Parser13 +!! input +{| +| http://a| +!! result + + + + +
+
+ +!! end + +!! test +Fuzz testing: Parser14 +!! input +== onmouseover= == +http://__TOC__ +!! result +

[edit] onmouseover=

+http://

Contents

+ +
+ +!! end + +!! test +Fuzz testing: Parser14-table +!! input +==a== +{| STYLE=__TOC__ +!! result +

[edit] a

+ + +
+ +!! end + +# Known to produce bogus xml (extra ) +!! test +Fuzz testing: Parser16 +!! options +noxml +!! input +{| +!https://|||||| +!! result + + + + + + +
https:// + +
+ +!! end + +!! test +Fuzz testing: Parser21 +!! input +{| +! irc://{{ftp://a" onmouseover="alert('hello world');" +| +!! result + + + + + +
irc://{{ftp://a" onmouseover="alert('hello world');" + +
+ +!! end + +!! test +Fuzz testing: Parser22 +!! input +http://===r:::https://b + +{| +!!result +

http://===r:::https://b +

+ + +
+ +!! end + +# Known to produce bad XML for now +!! test +Fuzz testing: Parser24 +!! options +noxml +!! input +{| +{{{| +}}}} > +
+ +MOVE YOUR MOUSE CURSOR OVER THIS TEXT +| +!! result + +{{{| +}}}} > +
+ +MOVE YOUR MOUSE CURSOR OVER THIS TEXT +
+ + +
+
+ +!! end + +# Note: the current result listed for this is not what the original one was, +# but the original bug was JavaScript injection, which is fixed in any case. +# It's not clear that the original result listed was any more correct than the +# current one. Original result: +#

{{{| +#

+#
  • +# }}}blah" onmouseover="alert('hello world');" align="left"MOVE MOUSE CURSOR OVER HERE +!!test +Fuzz testing: Parser25 (bug 6055) +!! input +{{{ +| +
  • +}}}blah" onmouseover="alert('hello world');" align="left"'''MOVE MOUSE CURSOR OVER HERE +!! result +

    <LI CLASS=blah" onmouseover="alert('hello world');" align="left"MOVE MOUSE CURSOR OVER HERE +

    +!! end + +!!test +Fuzz testing: URL adjacent extension (with space, clean) +!! options +!! input +http://example.com junk +!! result +

    http://example.com junk +

    +!!end + +!!test +Fuzz testing: URL adjacent extension (no space, dirty; nowiki) +!! options +!! input +http://example.comjunk +!! result +

    http://example.comjunk +

    +!!end + +!!test +Fuzz testing: URL adjacent extension (no space, dirty; pre) +!! options +!! input +http://example.com
    junk
    +!! result +http://example.com
    junk
    + +!!end + +!!test +Fuzz testing: image with bogus manual thumbnail +!!input +[[Image:foobar.jpg|thumbnail= ]] +!!result +
    Error creating thumbnail:
    + +!!end + +!! test +Fuzz testing: encoded newline in generated HTML replacements (bug 6577) +!! input +
    
    +!! result
    +
    
    +
    +!! end
    +
    +!! test
    +Parsing optional HTML elements (Bug 6171)
    +!! options
    +!! input
    +
    +  
    +    
    +    
    +  
    +
    Some tabular data More tabular data ... + And yet som tabular data
    +!! result + + + + + +
    Some tabular data More tabular data ... + And yet som tabular data
    + +!! end + +!! test +Correct handling of , (Bug 6171) +!! options +!! input + + + + + + +
    Some tabular data More tabular data ... And yet som tabular data
    +!! result + + + + + + +
    Some tabular data More tabular data ... And yet som tabular data
    + +!! end + + +!! test +Parsing crashing regression (fr:JavaScript) +!! input + +!! result +

    </body></x> +

    +!! end + +!! test +Inline wiki vs wiki block nesting +!! input +'''Bold paragraph + +New wiki paragraph +!! result +

    Bold paragraph +

    New wiki paragraph +

    +!! end + +!! test +Inline HTML vs wiki block nesting +!! options +disabled +!! input +Bold paragraph + +New wiki paragraph +!! result +

    Bold paragraph +

    New wiki paragraph +

    +!! end + +# Original result was this: +#

    boldboldbolditalics +#

    +# While that might be marginally more intuitive, maybe, the six-apostrophe +# construct is clearly pathological and the result stated here (which is what +# the parser actually does) is about as reasonable as anything. +!!test +Mixing markup for italics and bold +!! options +!! input +'''bold''''''bold''bolditalics''''' +!! result +

    'bold'boldbolditalics +

    +!! end + + +!! article +Xyzzyx +!! text +Article for special page transclusion test +!! endarticle + +!! test +Special page transclusion +!! options +!! input +{{Special:Prefixindex/Xyzzyx}} +!! result +


    +

    +
    Xyzzyx
    + +!! end + +!! test +Special page transclusion twice (bug 5021) +!! options +!! input +{{Special:Prefixindex/Xyzzyx}} +{{Special:Prefixindex/Xyzzyx}} +!! result +


    +

    +
    Xyzzyx
    +


    +

    +
    Xyzzyx
    + +!! end + +!! test +Transclusion of default MediaWiki message +!! input +{{MediaWiki:Mainpage}} +!!result +

    Main Page +

    +!! end + +!! test +Transclusion of nonexistent MediaWiki message +!! input +{{MediaWiki:Mainpagexxx}} +!!result +

    MediaWiki:Mainpagexxx +

    +!! end + +!! test +Transclusion of MediaWiki message with underscore +!! input +{{MediaWiki:history_short}} +!! result +

    History +

    +!! end + +!! test +Transclusion of MediaWiki message with space +!! input +{{MediaWiki:history short}} +!! result +

    History +

    +!! end + +!! test +Invalid header with following text +!! input += x = y +!! result +

    = x = y +

    +!! end + + +!! test +Section extraction test (section 0) +!! options +section=0 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +!! end + +!! test +Section extraction test (section 1) +!! options +section=1 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +==a== +===aa=== +====aaa==== +!! end + +!! test +Section extraction test (section 2) +!! options +section=2 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +===aa=== +====aaa==== +!! end + +!! test +Section extraction test (section 3) +!! options +section=3 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +====aaa==== +!! end + +!! test +Section extraction test (section 4) +!! options +section=4 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +==b== +===ba=== +===bb=== +====bba==== +===bc=== +!! end + +!! test +Section extraction test (section 5) +!! options +section=5 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +===ba=== +!! end + +!! test +Section extraction test (section 6) +!! options +section=6 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +===bb=== +====bba==== +!! end + +!! test +Section extraction test (section 7) +!! options +section=7 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +====bba==== +!! end + +!! test +Section extraction test (section 8) +!! options +section=8 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +===bc=== +!! end + +!! test +Section extraction test (section 9) +!! options +section=9 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +==c== +===ca=== +!! end + +!! test +Section extraction test (section 10) +!! options +section=10 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +===ca=== +!! end + +!! test +Section extraction test (nonexistent section 11) +!! options +section=11 +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +!! end + +!! test +Section extraction test with bogus heading (section 1) +!! options +section=1 +!! input +==a== +==bogus== not a legal section +==b== +!! result +==a== +==bogus== not a legal section +!! end + +!! test +Section extraction test with bogus heading (section 2) +!! options +section=2 +!! input +==a== +==bogus== not a legal section +==b== +!! result +==b== +!! end + +!! test +Section extraction test with comment after heading (section 1) +!! options +section=1 +!! input +==a== +==b== +==c== +!! result +==a== +!! end + +!! test +Section extraction test with comment after heading (section 2) +!! options +section=2 +!! input +==a== +==b== +==c== +!! result +==b== +!! end + +!! test +Section extraction test with bogus heading (section 1) +!! options +section=1 +!! input +==a== +==bogus== not a legal section +==b== +!! result +==a== +==bogus== not a legal section +!! end + +!! test +Section extraction test with bogus heading (section 2) +!! options +section=2 +!! input +==a== +==bogus== not a legal section +==b== +!! result +==b== +!! end + + +# Formerly testing for bug 2587, now resolved by the use of unmarked sections +# instead of respecting commented sections +!! test +Section extraction prefixed by comment (section 1) +!! options +section=1 +!! input +==sec1== +==sec2== +!!result +==sec2== +!!end + +!! test +Section extraction prefixed by comment (section 2) +!! options +section=2 +!! input +==sec1== +==sec2== +!!result + +!!end + + +# Formerly testing for bug 2607, now resolved by the use of unmarked sections +# instead of respecting HTML-style headings +!! test +Section extraction, mixed wiki and html (section 1) +!! options +section=1 +!! input +

    unmarked

    +unmarked +==1== +one +==2== +two +!! result +==1== +one +!! end + +!! test +Section extraction, mixed wiki and html (section 2) +!! options +section=2 +!! input +

    unmarked

    +unmarked +==1== +one +==2== +two +!! result +==2== +two +!! end + + +# Formerly testing for bug 3342 +!! test +Section extraction, heading surrounded by +!! options +section=1 +!! input +==unmarked== +==marked== +!! result +==marked== +!!end + +# Test behaviour of bug 19910 +!! test +Sectiion with all-equals +!! options +section=2 +!! input +=== +The line above must have a trailing space +=== +But just in case it doesn't... +!! result +=== +But just in case it doesn't... +!! end + +!! test +Section replacement test (section 0) +!! options +replace=0,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +xxx + +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 1) +!! options +replace=1,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +xxx + +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 2) +!! options +replace=2,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +xxx + +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 3) +!! options +replace=3,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +xxx + +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 4) +!! options +replace=4,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +xxx + +==c== +===ca=== +!! end + +!! test +Section replacement test (section 5) +!! options +replace=5,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +==b== +xxx + +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 6) +!! options +replace=6,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +xxx + +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 7) +!! options +replace=7,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +xxx + +===bc=== +==c== +===ca=== +!! end + +!! test +Section replacement test (section 8) +!! options +replace=8,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +xxx + +==c== +===ca=== +!!end + +!! test +Section replacement test (section 9) +!! options +replace=9,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +xxx +!! end + +!! test +Section replacement test (section 10) +!! options +replace=10,"xxx" +!! input +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +===ca=== +!! result +start +==a== +===aa=== +====aaa==== +==b== +===ba=== +===bb=== +====bba==== +===bc=== +==c== +xxx +!! end + +!! test +Section replacement test with initial whitespace (bug 13728) +!! options +replace=2,"xxx" +!! input + Preformatted initial line +==a== +===a=== +!! result + Preformatted initial line +==a== +xxx +!! end + + +!! test +Section extraction, heading followed by pre with 20 spaces (bug 6398) +!! options +section=1 +!! input +==a== + a +!! result +==a== + a +!! end + +!! test +Section extraction, heading followed by pre with 19 spaces (bug 6398 sanity check) +!! options +section=1 +!! input +==a== + a +!! result +==a== + a +!! end + + +!! test +Section extraction,
     around bogus header (bug 10309)
    +!! options
    +noxml section=2
    +!! input
    +== Section One ==
    +
    +=======
    +
    + +== Section Two == +stuff +!! result +== Section Two == +stuff +!! end + +!! test +Section replacement,
     around bogus header (bug 10309)
    +!! options
    +noxml replace=2,"xxx"
    +!! input
    +== Section One ==
    +
    +=======
    +
    + +== Section Two == +stuff +!! result +== Section One == +
    +=======
    +
    + +xxx +!! end + + + +!! test +Handling of in URLs +!! input +**irc:// a +!! result + + +!!end + +!! test +5 quotes, code coverage +1 line +!! input +''''' +!! result +!! end + +!! test +Special:Search page linking. +!! input +{{Special:search}} +!! result +

    Special:Search +

    +!! end + +!! test +Say the magic word +!! input +* {{PAGENAME}} +* {{BASEPAGENAME}} +* {{SUBPAGENAME}} +* {{SUBPAGENAMEE}} +* {{BASEPAGENAME}} +* {{BASEPAGENAMEE}} +* {{TALKPAGENAME}} +* {{TALKPAGENAMEE}} +* {{SUBJECTPAGENAME}} +* {{SUBJECTPAGENAMEE}} +* {{NAMESPACEE}} +* {{NAMESPACE}} +* {{TALKSPACE}} +* {{TALKSPACEE}} +* {{SUBJECTSPACE}} +* {{SUBJECTSPACEE}} +* {{Dynamic|{{NUMBEROFUSERS}}|{{NUMBEROFPAGES}}|{{CURRENTVERSION}}|{{CONTENTLANGUAGE}}|{{DIRECTIONMARK}}|{{CURRENTTIMESTAMP}}|{{NUMBEROFARTICLES}}}} +!! result +
    • Parser test +
    • Parser test +
    • Parser test +
    • Parser_test +
    • Parser test +
    • Parser_test +
    • Talk:Parser test +
    • Talk:Parser_test +
    • Parser test +
    • Parser_test +
    • +
    • +
    • Talk +
    • Talk +
    • +
    • +
    • Template:Dynamic +
    + +!! end +### Note: Above tests excludes the "{{NUMBEROFADMINS}}" magic word because it generates a MySQL error when included. + +!! test +Gallery +!! input + +image1.png | +image2.gif||||| + +image3| +image4 |300px| centre + image5.svg| http:///////// +[[x|xx]]]] +* image6 + +!! result + + +!! end + +!! test +Gallery (with options) +!! input + +File:Nonexistant.jpg|caption +File:Nonexistant.jpg +image:foobar.jpg|some '''caption''' [[Main Page]] +image:foobar.jpg + +!! result + + +!! end + +!! test +gallery (with showfilename option) +!! input + +File:Nonexistant.jpg|caption +File:Nonexistant.jpg +image:foobar.jpg|some '''caption''' [[Main Page]] +File:Foobar.jpg + +!! result + + +!! end + +!! test +HTML Hex character encoding (spells the word "JavaScript") +!! input +JavaScript +!! result +

    JavaScript +

    +!! end + +!! test +__FORCETOC__ override +!! input +__NEWSECTIONLINK__ +__FORCETOC__ +!! result +


    +

    +!! end + +!! test +ISBN code coverage +!! input +ISBN 978-0-1234-56 789 +!! result +

    ISBN 978-0-1234-56 789 +

    +!! end + +!! test +ISBN followed by 5 spaces +!! input +ISBN +!! result +

    ISBN +

    +!! end + +!! test +Double ISBN +!! input +ISBN ISBN 1234567890 +!! result +

    ISBN ISBN 1234567890 +

    +!! end + +!! test +Bug 22905: followed by ISBN followed by +!! input +(fr) ISBN 2753300917 [http://www.example.com example.com] +!! result +

    (fr) ISBN 2753300917 example.com +

    +!! end + +!! test +Double RFC +!! input +RFC RFC 1234 +!! result +

    RFC RFC 1234 +

    +!! end + +!! test +Double RFC with a wiki link +!! input +RFC [[RFC 1234]] +!! result +

    RFC RFC 1234 +

    +!! end + +!! test +RFC code coverage +!! input +RFC 983 987 +!! result +

    RFC 983 987 +

    +!! end + +!! test +Centre-aligned image +!! input +[[Image:foobar.jpg|centre]] +!! result +
    Foobar.jpg
    + +!!end + +!! test +None-aligned image +!! input +[[Image:foobar.jpg|none]] +!! result +
    Foobar.jpg
    + +!!end + +!! test +Width + Height sized image (using px) (height is ignored) +!! input +[[Image:foobar.jpg|640x480px]] +!! result +

    Foobar.jpg +

    +!!end + +!! test +Width-sized image (using px, no following whitespace) +!! input +[[Image:foobar.jpg|640px]] +!! result +

    Foobar.jpg +

    +!!end + +!! test +Width-sized image (using px, with following whitespace - test regression from r39467) +!! input +[[Image:foobar.jpg|640px ]] +!! result +

    Foobar.jpg +

    +!!end + +!! test +Width-sized image (using px, with preceding whitespace - test regression from r39467) +!! input +[[Image:foobar.jpg| 640px]] +!! result +

    Foobar.jpg +

    +!!end + +!! test +Another italics / bold test +!! input + ''' ''x' +!! result +
    ' x'
    +
    +!!end + +# Note the results may be incorrect, as parserTest output included this: +# XML error: Mismatched tag at byte 6120: +# ...
    +
    +
    +
    +
    +
    +
    + +!!end + + +# Images with the "|" character in external URLs in comment tags; Eats half the comment, leaves unmatched "" tag. +!! test +Images with the "|" character in the comment +!! input +[[image:Foobar.jpg|thumb|An [http://test/?param1=|left|¶m2=|x external] URL]] +!! result +
    An external URL
    + +!!end + +!! test +[Before] HTML without raw HTML enabled ($wgRawHtml==false) +!! input + +!! result +

    <html><script>alert(1);</script></html> +

    +!! end + +!! test +HTML with raw HTML ($wgRawHtml==true) +!! options +rawhtml +!! input + +!! result +

    +

    +!! end + +!! test +Parents of subpages, one level up +!! options +subpage title=[[Subpage test/L1/L2/L3]] +!! input +[[../|L2]] +!! result +

    L2 +

    +!! end + + +!! test +Parents of subpages, one level up, not named +!! options +subpage title=[[Subpage test/L1/L2/L3]] +!! input +[[../]] +!! result +

    Subpage test/L1/L2 +

    +!! end + + + +!! test +Parents of subpages, two levels up +!! options +subpage title=[[Subpage test/L1/L2/L3]] +!! input +[[../../|L1]]2 + +[[../../|L1]]l +!! result +

    L12 +

    L1l +

    +!! end + +!! test +Parents of subpages, two levels up, without trailing slash or name. +!! options +subpage title=[[Subpage test/L1/L2/L3]] +!! input +[[../..]] +!! result +

    [[../..]] +

    +!! end + +!! test +Parents of subpages, two levels up, with lots of extra trailing slashes. +!! options +subpage title=[[Subpage test/L1/L2/L3]] +!! input +[[../../////]] +!! result +

    /// +

    +!! end + +!! test +Definition list code coverage +!! input +; title : def +; title : def +;title: def +!! result +
    title  
    def +
    title 
    def +
    title
    def +
    + +!! end + +!! test +Don't fall for the self-closing div +!! input +
    hello world
    +!! result +
    hello world
    + +!! end + +!! test +MSGNW magic word +!! input +{{MSGNW:msg}} +!! result +

    [[:Template:Msg]] +

    +!! end + +!! test +RAW magic word +!! input +{{RAW:QUERTY}} +!! result +

    Template:QUERTY +

    +!! end + +# This isn't needed for XHTML conformance, but would be handy as a fallback security measure +!! test +Always escape literal '>' in output, not just after '<' +!! input +><> +!! result +

    ><> +

    +!! end + +!! test +Template caching +!! input +{{Test}} +{{Test}} +!! result +

    This is a test template +This is a test template +

    +!! end + + +!! article +MediaWiki:Fake +!! text +==header== +!! endarticle + +!! test +Inclusion of !userCanEdit() content +!! input +{{MediaWiki:Fake}} +!! result +

    [edit] header

    + +!! end + + +!! test +Out-of-order TOC heading levels +!! input +==2== +======6====== +===3=== +=1= +=====5===== +==2== +!! result +

    Contents

    + +
    +

    [edit] 2

    +
    [edit] 6
    +

    [edit] 3

    +

    [edit] 1

    +
    [edit] 5
    +

    [edit] 2

    + +!! end + + +!! test +ISBN with a dummy number +!! input +ISBN --- +!! result +

    ISBN --- +

    +!! end + + +!! test +ISBN with space-delimited number +!! input +ISBN 92 9017 032 8 +!! result +

    ISBN 92 9017 032 8 +

    +!! end + + +!! test +ISBN with multiple spaces, no number +!! input +ISBN foo +!! result +

    ISBN foo +

    +!! end + + +!! test +ISBN length +!! input +ISBN 123456789 + +ISBN 1234567890 + +ISBN 12345678901 +!! result +

    ISBN 123456789 +

    ISBN 1234567890 +

    ISBN 12345678901 +

    +!! end + + +!! test +ISBN with trailing year (bug 8110) +!! input +ISBN 1-234-56789-0 - 2006 + +ISBN 1 234 56789 0 - 2006 +!! result +

    ISBN 1-234-56789-0 - 2006 +

    ISBN 1 234 56789 0 - 2006 +

    +!! end + + +!! test +anchorencode +!! input +{{anchorencode:foo bar©#%n}} +!! result +

    foo_bar.C2.A9.23.25n +

    +!! end + +!! test +anchorencode trims spaces +!! input +{{anchorencode: __pretty__please__}} +!! result +

    pretty_please +

    +!! end + +!! test +anchorencode deals with links +!! input +{{anchorencode: [[hello|world]] [[hi]]}} +!! result +

    world_hi +

    +!! end + +!! test +anchorencode deals with templates +!! input +{{anchorencode: {{Foo}} }} +!! result +

    FOO +

    +!! end + +!! test +anchorencode encodes like the TOC generator: (bug 18431) +!! input +=== _ +:.3A%3A&&]] === +{{anchorencode: _ +:.3A%3A&&]] }} +__NOEDITSECTION__ +!! result +

    _ +:.3A%3A&&]]

    +

    .2B:.3A.253A.26.26.5D.5D +

    +!! end + +# Expected output in the following test is not necessarily expected (there +# should probably be

    tags inside the

    in the output) -- it's +# only testing for well-formedness. +!! test +Bug 6200: blockquotes and paragraph formatting +!! input +
    +foo +
    + +bar + + baz +!! result +
    +foo +
    +

    bar +

    +
    baz
    +
    +!! end + +!! test +Bug 8293: Use of center tag ruins paragraph formatting +!! input +
    +foo +
    + +bar + + baz +!! result +
    +

    foo +

    +
    +

    bar +

    +
    baz
    +
    +!! end + + +### +### Language variants related tests +### +!! test +Self-link in language variants +!! options +title=[[Dunav]] language=sr +!! input +Both [[Dunav]] and [[Дунав]] are names for this river. +!! result +

    Both Dunav and Дунав are names for this river. +

    +!!end + + +!! test +Link to pages in language variants +!! options +language=sr +!! input +Main Page can be written as [[Маин Паге]] +!! result +

    Main Page can be written as Маин Паге +

    +!!end + + +!! test +Multiple links to pages in language variants +!! options +language=sr +!! input +[[Main Page]] can be written as [[Маин Паге]] same as [[Маин Паге]]. +!! result +

    Main Page can be written as Маин Паге same as Маин Паге. +

    +!!end + + +!! test +Simple template in language variants +!! options +language=sr +!! input +{{тест}} +!! result +

    This is a test template +

    +!! end + + +!! test +Template with explicit namespace in language variants +!! options +language=sr +!! input +{{Template:тест}} +!! result +

    This is a test template +

    +!! end + + +!! test +Basic test for template parameter in language variants +!! options +language=sr +!! input +{{парамтест|param=foo}} +!! result +

    This is a test template with parameter foo +

    +!! end + + +!! test +Simple category in language variants +!! options +language=sr cat +!! input +[[Category:МедиаWики Усер'с Гуиде]] +!! result +MediaWiki User's Guide +!! end + + +!! test +Stripping -{}- tags (language variants) +!! options +language=sr +!! input +Latin proverb: -{Ne nuntium necare}- +!! result +

    Latin proverb: Ne nuntium necare +

    +!! end + + +!! test +Prevent conversion with -{}- tags (language variants) +!! options +language=sr variant=sr-ec +!! input +Latinski: -{Ne nuntium necare}- +!! result +

    Латински: Ne nuntium necare +

    +!! end + + +!! test +Prevent conversion of text with -{}- tags (language variants) +!! options +language=sr variant=sr-ec +!! input +Latinski: -{Ne nuntium necare}- +!! result +

    Латински: Ne nuntium necare +

    +!! end + + +!! test +Prevent conversion of links with -{}- tags (language variants) +!! options +language=sr variant=sr-ec +!! input +-{[[Main Page]]}- +!! result +

    Main Page +

    +!! end + + +!! test +-{}- tags within headlines (within html for parserConvert()) +!! options +language=sr variant=sr-ec +!! input +== -{Naslov}- == +!! result +

    [уреди] Naslov

    + +!! end + + +!! test +Explicit definition of language variant alternatives +!! options +language=zh variant=zh-tw +!! input +-{zh:China;zh-tw:Taiwan}-, not China +!! result +

    Taiwan, not China +

    +!! end + + +!! test +Explicit session-wise language variant mapping (A flag and - flag) +!! options +language=zh variant=zh-tw +!! input +Taiwan is not China. +But -{A|zh:China;zh-tw:Taiwan}- is China, +(This-{-|zh:China;zh-tw:Taiwan}- should be stripped!) +and -{China}- is China. +!! result +

    Taiwan is not China. +But Taiwan is Taiwan, +(This should be stripped!) +and China is China. +

    +!! end + +!! test +Explicit session-wise language variant mapping (H flag for hide) +!! options +language=zh variant=zh-tw +!! input +(This-{H|zh:China;zh-tw:Taiwan}- should be stripped!) +Taiwan is China. +!! result +

    (This should be stripped!) +Taiwan is Taiwan. +

    +!! end + +!! test +Adding explicit conversion rule for title (T flag) +!! options +language=zh variant=zh-tw showtitle +!! input +Should be stripped-{T|zh:China;zh-tw:Taiwan}-! +!! result +Taiwan +

    Should be stripped! +

    +!! end + +!! test +Testing that changing the language variant here in the tests actually works +!! options +language=zh variant=zh showtitle +!! input +Should be stripped-{T|zh:China;zh-tw:Taiwan}-! +!! result +China +

    Should be stripped! +

    +!! end + +!! test +Bug 24072: more test on conversion rule for title +!! options +language=zh variant=zh-tw showtitle +!! input +This should be stripped-{T|zh:China;zh-tw:Taiwan}-! +This won't take interferes with the title rule-{H|zh:Beijing;zh-tw:Taipei}-. +!! result +Taiwan +

    This should be stripped! +This won't take interferes with the title rule. +

    +!! end + +!! test +Raw output of variant escape tags (R flag) +!! options +language=zh variant=zh-tw +!! input +Raw: -{R|zh:China;zh-tw:Taiwan}- +!! result +

    Raw: zh:China;zh-tw:Taiwan +

    +!! end + +!! test +Nested using of manual convert syntax +!! options +language=zh variant=zh-hk +!! input +Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiwan;zh-hk:H-{ong}- K-{}-ong;}-;}-! +!! result +

    Nested: Hello Hong Kong! +

    +!! end + +!! test +Do not convert roman numbers to language variants +!! options +language=sr variant=sr-ec +!! input +Fridrih IV je car. +!! result +

    Фридрих IV је цар. +

    +!! end + +!! test +Unclosed language converter markup "-{" +!! options +language=sr +!! input +-{T|hello +!! result +

    -{T|hello +

    +!! end + +!! test +Don't convert raw rule "-{R|=>}-" to "=>" +!! options +language=sr +!! input +-{R|=>}- +!! result +

    => +

    +!!end + +!!article +Template:Bullet +!!text +* Bar +!!endarticle + +!! test +Bug 529: Uncovered bullet +!! input +* Foo {{bullet}} +!! result +
    • Foo +
    • Bar +
    + +!! end + +!! test +Bug 529: Uncovered table already at line-start +!! input +x + +{{table}} +y +!! result +

    x +

    + + + + + + +
    1 2 +
    3 4 +
    +

    y +

    +!! end + +!! test +Bug 529: Uncovered bullet in parser function result +!! input +* Foo {{lc:{{bullet}} }} +!! result +
    • Foo +
    • bar +
    + +!! end + +!! test +Bug 5678: Double-parsed template argument +!! input +{{lc:{{{1}}}|hello}} +!! result +

    {{{1}}} +

    +!! end + +!! test +Bug 5678: Double-parsed template invocation +!! input +{{lc:{{paramtest {{!}} param = hello }} }} +!! result +

    {{paramtest | param = hello }} +

    +!! end + +!! test +Case insensitivity of parser functions for non-ASCII characters (bug 8143) +!! options +language=cs +title=[[Main Page]] +!! input +{{PRVNÍVELKÉ:ěščř}} +{{prvnívelké:ěščř}} +{{PRVNÍMALÉ:ěščř}} +{{prvnímalé:ěščř}} +{{MALÁ:ěščř}} +{{malá:ěščř}} +{{VELKÁ:ěščř}} +{{velká:ěščř}} +!! result +

    Ěščř +Ěščř +ěščř +ěščř +ěščř +ěščř +ĚŠČŘ +ĚŠČŘ +

    +!! end + +!! test +Morwen/13: Unclosed link followed by heading +!! input +[[link +==heading== +!! result +

    [[link +

    +

    [edit] heading

    + +!! end + +!! test +HHP2.1: Heuristics for headings in preprocessor parenthetical structures +!! input +{{foo| +=heading= +!! result +

    {{foo| +

    +

    heading

    + +!! end + +!! test +HHP2.2: Heuristics for headings in preprocessor parenthetical structures +!! input +{{foo| +==heading== +!! result +

    {{foo| +

    +

    [edit] heading

    + +!! end + +!! test +Tildes in comments +!! options +pst +!! input + +!! result + +!! end + +!! test +Paragraphs inside divs (no extra line breaks) +!! input +
    Line one + +Line two
    +!! result +
    Line one +Line two
    + +!! end + +!! test +Paragraphs inside divs (extra line break on open) +!! input +
    +Line one + +Line two
    +!! result +
    +

    Line one +

    +Line two
    + +!! end + +!! test +Paragraphs inside divs (extra line break on close) +!! input +
    Line one + +Line two +
    +!! result +
    Line one +

    Line two +

    +
    + +!! end + +!! test +Paragraphs inside divs (extra line break on open and close) +!! input +
    +Line one + +Line two +
    +!! result +
    +

    Line one +

    Line two +

    +
    + +!! end + +!! test +Nesting tags, paragraphs on lines which begin with
    +!! options +disabled +!! input +
    A +B +!! result +
    +

    A +B +

    +!! end + +# Bug 6200:
    should behave like
    with respect to line breaks +!! test +Bug 6200: paragraphs inside blockquotes (no extra line breaks) +!! options +disabled +!! input +
    Line one + +Line two
    +!! result +
    Line one +Line two
    + +!! end + +!! test +Bug 6200: paragraphs inside blockquotes (extra line break on open) +!! options +disabled +!! input +
    +Line one + +Line two
    +!! result +
    +

    Line one +

    +Line two
    + +!! end + +!! test +Bug 6200: paragraphs inside blockquotes (extra line break on close) +!! options +disabled +!! input +
    Line one + +Line two +
    +!! result +
    Line one +

    Line two +

    +
    + +!! end + +!! test +Bug 6200: paragraphs inside blockquotes (extra line break on open and close) +!! options +disabled +!! input +
    +Line one + +Line two +
    +!! result +
    +

    Line one +

    Line two +

    +
    + +!! end + +!! test +Paragraphs inside blockquotes/divs (no extra line breaks) +!! input +
    Line one + +Line two
    +!! result +
    Line one +Line two
    + +!! end + +!! test +Paragraphs inside blockquotes/divs (extra line break on open) +!! input +
    +Line one + +Line two
    +!! result +
    +

    Line one +

    +Line two
    + +!! end + +!! test +Paragraphs inside blockquotes/divs (extra line break on close) +!! input +
    Line one + +Line two +
    +!! result +
    Line one +

    Line two +

    +
    + +!! end + +!! test +Paragraphs inside blockquotes/divs (extra line break on open and close) +!! input +
    +Line one + +Line two +
    +!! result +
    +

    Line one +

    Line two +

    +
    + +!! end + +!! test +Interwiki links trounced by replaceExternalLinks after early LinkHolderArray expansion +!! options +wgLinkHolderBatchSize=0 +!! input +[[meatball:1]] +[[meatball:2]] +[[meatball:3]] +!! result +

    meatball:1 +meatball:2 +meatball:3 +

    +!! end + +!! test +Free external link invading image caption +!! input +[[Image:Foobar.jpg|thumb|http://x|hello]] +!! result +
    hello
    + +!! end + +!! test +Bug 15196: localised external link numbers +!! options +language=fa +!! input +[http://en.wikipedia.org/] +!! result +

    [Û±] +

    +!! end + +!! test +Multibyte character in padleft +!! input +{{padleft:-Hello|7|Æ}} +!! result +

    Æ-Hello +

    +!! end + +!! test +Multibyte character in padright +!! input +{{padright:Hello-|7|Æ}} +!! result +

    Hello-Æ +

    +!! end + +!! test +Formatted date +!! config +wgUseDynamicDates=1 +!! input +[[2009-03-24]] +!! result +

    2009-03-24 +

    +!!end + +!!test +formatdate parser function +!!input +{{#formatdate:2009-03-24}} +!! result +

    2009-03-24 +

    +!! end + +!!test +formatdate parser function, with default format +!!input +{{#formatdate:2009-03-24|mdy}} +!! result +

    March 24, 2009 +

    +!! end + +!! test +Linked date with autoformatting disabled +!! config +wgUseDynamicDates=false +!! input +[[2009-03-24]] +!! result +

    2009-03-24 +

    +!! end + +!! test +Spacing of numbers in formatted dates +!! input +{{#formatdate:January 15}} +!! result +

    January 15 +

    +!! end + +!! test +Spacing of numbers in formatted dates (linked) +!! config +wgUseDynamicDates=true +!! input +[[January 15]] +!! result +

    January 15 +

    +!! end + +# +# +# + +# +# Edit comments +# + +!! test +Edit comment with link +!! options +comment +!! input +I like the [[Main Page]] a lot +!! result +I like the Main Page a lot +!!end + +!! test +Edit comment with link and link text +!! options +comment +!! input +I like the [[Main Page|best pages]] a lot +!! result +I like the best pages a lot +!!end + +!! test +Edit comment with link and link text with suffix +!! options +comment +!! input +I like the [[Main Page|best page]]s a lot +!! result +I like the best pages a lot +!!end + +!! test +Edit comment with section link (non-local, eg in history list) +!! options +comment title=[[Main Page]] +!! input +/* External links */ removed bogus entries +!! result +→External links: removed bogus entries +!!end + +!! test +Edit comment with section link (local, eg in diff view) +!! options +comment local title=[[Main Page]] +!! input +/* External links */ removed bogus entries +!! result +→External links: removed bogus entries +!!end + +!! test +Edit comment with subpage link (bug 14080) +!! options +comment +subpage +title=[[Subpage test]] +!! input +Poked at a [[/subpage]] here... +!! result +Poked at a /subpage here... +!!end + +!! test +Edit comment with subpage link and link text (bug 14080) +!! options +comment +subpage +title=[[Subpage test]] +!! input +Poked at a [[/subpage|neat little page]] here... +!! result +Poked at a neat little page here... +!!end + +!! test +Edit comment with bogus subpage link in non-subpage NS (bug 14080) +!! options +comment +title=[[Subpage test]] +!! input +Poked at a [[/subpage]] here... +!! result +Poked at a /subpage here... +!!end + +!! test +Edit comment with bare anchor link (local, as on diff) +!! options +comment +local +title=[[Main Page]] +!!input +[[#section]] +!! result +#section +!! end + +!! test +Edit comment with bare anchor link (non-local, as on history) +!! options +comment +title=[[Main Page]] +!!input +[[#section]] +!! result +#section +!! end + +!! test +Space normalisation on autocomment (bug 22784) +!! options +comment +title=[[Main Page]] +!!input +/* __hello__world__ */ +!! result +→__hello__world__ +!! end + +!! test +Bad images - basic functionality +!! input +[[File:Bad.jpg]] +!! result +!! end + +!! test +Bad images - bug 16039: text after bad image disappears +!! input +Foo bar +[[File:Bad.jpg]] +Bar foo +!! result +

    Foo bar +

    Bar foo +

    +!! end + +!! test +Verify that displaytitle works (bug #22501) no displaytitle +!! options +showtitle +!! config +wgAllowDisplayTitle=true +wgRestrictDisplayTitle=false +!! input +this is not the the title +!! result +Parser test +

    this is not the the title +

    +!! end + +!! test +Verify that displaytitle works (bug #22501) RestrictDisplayTitle=false +!! options +showtitle +title=[[Screen]] +!! config +wgAllowDisplayTitle=true +wgRestrictDisplayTitle=false +!! input +this is not the the title +{{DISPLAYTITLE:whatever}} +!! result +whatever +

    this is not the the title +

    +!! end + +!! test +Verify that displaytitle works (bug #22501) RestrictDisplayTitle=true mismatch +!! options +showtitle +title=[[Screen]] +!! config +wgAllowDisplayTitle=true +wgRestrictDisplayTitle=true +!! input +this is not the the title +{{DISPLAYTITLE:whatever}} +!! result +Screen +

    this is not the the title +

    +!! end + +!! test +Verify that displaytitle works (bug #22501) RestrictDisplayTitle=true matching +!! options +showtitle +title=[[Screen]] +!! config +wgAllowDisplayTitle=true +wgRestrictDisplayTitle=true +!! input +this is not the the title +{{DISPLAYTITLE:screen}} +!! result +screen +

    this is not the the title +

    +!! end + +!! test +Verify that displaytitle works (bug #22501) AllowDisplayTitle=false +!! options +showtitle +title=[[Screen]] +!! config +wgAllowDisplayTitle=false +!! input +this is not the the title +{{DISPLAYTITLE:screen}} +!! result +Screen +

    this is not the the title +Template:DISPLAYTITLE:screen +

    +!! end + +!! test +Verify that displaytitle works (bug #22501) AllowDisplayTitle=false no DISPLAYTITLE +!! options +showtitle +title=[[Screen]] +!! config +wgAllowDisplayTitle=false +!! input +this is not the the title +!! result +Screen +

    this is not the the title +

    +!! end + +!! test +preload: check and +!! options +preload +!! input +Hello cruelkind world. +!! result +Hello kind world. +!! end + +!! test +preload: check +!! options +preload +!! input +Goodbye Hello world +!! result +Hello world +!! end + +!! test +preload: can pass tags through if we want to +!! options +preload +!! input +<includeonly>Hello world</includeonly> +!! result +Hello world +!! end + +!! test +preload: check that it doesn't try to do tricks +!! options +preload +!! input +* ''{{world}}'' {{subst:How are you}}{{ {{{|safesubst:}}} #if:1|2|3}} +!! result +* ''{{world}}'' {{subst:How are you}}{{ {{{|safesubst:}}} #if:1|2|3}} +!! end + +!! test +Play a bit with r67090 and bug 3158 +!! options +disabled +!! input +
     
    +
     
    +
     
    +
     
    +!! result +
     
    +
     
    +
     
    +
     
    + +!! end + +!! test +HTML5 data attributes +!! input +Baz +

    Quuz

    +!! result +

    Baz +

    +

    Quuz

    + +!! end + + +TODO: +more images +more tables +math +character entities +and much more +Try for 100% code coverage diff --git a/tests/parser/parserTestsParserHook.php b/tests/parser/parserTestsParserHook.php new file mode 100644 index 0000000000..936d0611f9 --- /dev/null +++ b/tests/parser/parserTestsParserHook.php @@ -0,0 +1,46 @@ + + */ + +class ParserTestParserHook { + + static function setup( &$parser ) { + $parser->setHook( 'tag', array( __CLASS__, 'hook' ) ); + + return true; + } + + static function hook( $in, $argv ) { + ob_start(); + var_dump( + $in, + $argv + ); + $ret = ob_get_clean(); + + return "
    \n$ret
    "; + } +} diff --git a/tests/parser/parserTestsStaticParserHook.php b/tests/parser/parserTestsStaticParserHook.php new file mode 100644 index 0000000000..a7ce5a4e85 --- /dev/null +++ b/tests/parser/parserTestsStaticParserHook.php @@ -0,0 +1,58 @@ + + */ + +class ParserTestStaticParserHook { + static function setup( &$parser ) { + $parser->setHook( 'statictag', array( __CLASS__, 'hook' ) ); + + return true; + } + + static function hook( $in, $argv, $parser ) { + if ( ! count( $argv ) ) { + $parser->static_tag_buf = $in; + return ''; + } else if ( count( $argv ) === 1 && isset( $argv['action'] ) + && $argv['action'] === 'flush' && $in === null ) + { + // Clear the buffer, we probably don't need to + if ( isset( $parser->static_tag_buf ) ) { + $tmp = $parser->static_tag_buf; + } else { + $tmp = ''; + } + $parser->static_tag_buf = null; + return $tmp; + } else + // wtf? + return + "\nCall this extension as string or as" . + " , not in any other way.\n" . + "text: " . var_export( $in, true ) . "\n" . + "argv: " . var_export( $argv, true ) . "\n"; + } +} diff --git a/tests/phpunit/Makefile b/tests/phpunit/Makefile new file mode 100644 index 0000000000..26c39f321f --- /dev/null +++ b/tests/phpunit/Makefile @@ -0,0 +1,76 @@ +.PHONY: help test phpunit install coverage warning destructive parser noparser safe databaseless list-groups +.DEFAULT: warning + +SHELL = /bin/sh +CONFIG_FILE = $(shell pwd)/suite.xml +PHP = php +PU = ${PHP} phpunit.php --configuration ${CONFIG_FILE} ${FLAGS} + +all test: warning + +warning: + # Use 'make help' to get usage + @echo "WARNING -- some tests are DESTRUCTIVE and will alter your wiki." + @echo "DO NOT RUN THESE TESTS on a production wiki." + @echo "" + @echo "Until the default suites are made non-destructive, you can run" + @echo "the destructive tests like so:" + @echo " make destructive" + @echo "" + @echo "Some tests are expected to be safe, you can run them with" + @echo " make safe" + @echo "" + @echo "You are recommended to run them with read-only credentials, though." + @echo "" + @echo "If you don't have a database running, you can still run" + @echo " make databaseless" + @echo "" + +destructive: phpunit + +phpunit: + ${PU} + +install: + php install-phpunit.sh + +tap: + ${PU} --tap + +coverage: + ${PU} --coverage-html ../../../docs/code-coverage + +parser: + ${PU} --group Parser + +noparser: + ${PU} --exclude-group Parser,Broken + +safe: + ${PU} --exclude-group Broken,Destructive + +databaseless: + ${PU} --exclude-group Broken,Destructive,Database + +list-groups: + ${PU} --list-groups + +help: + # Usage: + # make [OPTION=value] + # + # Targets: + # phpunit (default) Run all the tests with phpunit + # install Install PHPUnit from phpunit.de + # tap Run the tests individually through Test::Harness's prove(1) + # help You're looking at it! + # coverage Run the tests and generates an HTML code coverage report + # You will need the Xdebug PHP extension for the later. + # [no]parser Skip or only run Parser tests + # + # list-groups List availabe Tests groups. + # + # Options: + # CONFIG_FILE Path to a PHPUnit configuration file (default: suite.xml) + # FLAGS Additional flags to pass to PHPUnit + # PHP Path to php diff --git a/tests/phpunit/README b/tests/phpunit/README new file mode 100644 index 0000000000..ce78270dc0 --- /dev/null +++ b/tests/phpunit/README @@ -0,0 +1,35 @@ +== MediaWiki PHPUnit Tests == + +Some quickie unit tests done with the PHPUnit testing framework. To run the +test suite, run 'make test' in this (maintenance/tests/phpunit) directory. + +=== WARNING === + +The current versions of some of these tests are DESTRUCTIVE AND WILL ALTER +YOUR WIKI'S CONTENTS. DO NOT RUN ON A PRODUCTION SYSTEM OR ONE WHERE YOU +NEED TO RETAIN YOUR DATA. + +=== Installation === + +PHPUnit is no longer maintained by PEAR. To get the current version of +PHPUnit, first uninstall any old version of PHPUnit or PHPUnit2 from PEAR, +then install the current version from phpunit.de like this: + + pear channel-discover pear.phpunit.de + pear install phpunit/PHPUnit + +You also may wish to install this via your normal package mechanism: + + aptitude install phpunit +- or - + yum install phpunit + +=== Notes === + +* Label currently broken tests in the group Broken and they will not be run +by phpunit. You can add them to the group by putting the following comment at +the top of the file: + /** + * @group Broken + */ +* Need to fix some broken tests diff --git a/tests/phpunit/TODO b/tests/phpunit/TODO new file mode 100644 index 0000000000..76f38c1dcc --- /dev/null +++ b/tests/phpunit/TODO @@ -0,0 +1,15 @@ +== Things To Do == + +* DEFAULT TESTS NEED TO MADE NON-DESTRUCTIVE. Any destructive tests which alter the contents of the live wiki need to +be protected with an explicit confirmation so people exploring their system don't accidentally destroy their main page +or register user accounts with default passwords. + +* Most of the tests are named poorly; naming should describe a use case in story-like language, not simply identify the +unit under test. An example would be the difference between testCalculate and testAddingIntegersTogetherWorks. +* Many of the tests make multiple assertions, and are thus not unitary tests. By using data-providers and more use-case +oriented test selection nearly all of these cases can be easily resolved. +* Some of the test files are either incorrectly named or in the wrong folder. Tests should be organized in a mirrored +structure to the source they are testing, and named the same, with the exception of the word "Test" at the end. +* Shared set-up code or base classes are present, but usually named improperly or appear to be poorly factored. Support +code should share as much of the same naming as the code it's supporting, and test and test-case depenencies should be +considered to resolve other shared needs. diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 0000000000..81beb2bc4a --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,65 @@ +" ) ) { + echo <<suite = $suite; + } + } + + function __call( $func, $args ) { + if ( method_exists( $this->suite, $func ) ) { + return call_user_func_array( array( $this->suite, $func ), $args); + } else { + throw new MWException( "Called non-existant $func method on " + . get_class( $this ) ); + } + } +} + diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php new file mode 100644 index 0000000000..987a0f5be7 --- /dev/null +++ b/tests/phpunit/includes/CdbTest.php @@ -0,0 +1,84 @@ +markTestIncomplete( 'This test requires native CDB support to be present.' ); + } + } + + public function testCdb() { + $dir = wfTempDir(); + if ( !is_writable( $dir ) ) { + $this->markTestSkipped( "Temp dir isn't writable" ); + } + + $w1 = new CdbWriter_PHP( "$dir/php.cdb" ); + $w2 = new CdbWriter_DBA( "$dir/dba.cdb" ); + + $data = array(); + for ( $i = 0; $i < 1000; $i++ ) { + $key = $this->randomString(); + $value = $this->randomString(); + $w1->set( $key, $value ); + $w2->set( $key, $value ); + + if ( !isset( $data[$key] ) ) { + $data[$key] = $value; + } + } + + $w1->close(); + $w2->close(); + + $this->assertEquals( + md5_file( "$dir/dba.cdb" ), + md5_file( "$dir/php.cdb" ), + 'same hash' + ); + + $r1 = new CdbReader_PHP( "$dir/php.cdb" ); + $r2 = new CdbReader_DBA( "$dir/dba.cdb" ); + + foreach ( $data as $key => $value ) { + if ( $key === '' ) { + // Known bug + continue; + } + $v1 = $r1->get( $key ); + $v2 = $r2->get( $key ); + + $v1 = $v1 === false ? '(not found)' : $v1; + $v2 = $v2 === false ? '(not found)' : $v2; + + # cdbAssert( 'Mismatch', $key, $v1, $v2 ); + $this->cdbAssert( "PHP error", $key, $v1, $value ); + $this->cdbAssert( "DBA error", $key, $v2, $value ); + } + + unlink( "$dir/dba.cdb" ); + unlink( "$dir/php.cdb" ); + } + + private function randomString() { + $len = mt_rand( 0, 10 ); + $s = ''; + for ( $j = 0; $j < $len; $j++ ) { + $s .= chr( mt_rand( 0, 255 ) ); + } + return $s; + } + + private function cdbAssert( $msg, $key, $v1, $v2 ) { + $this->assertEquals( + $v2, + $v1, + $msg . ', k=' . bin2hex( $key ) + ); + } +} diff --git a/tests/phpunit/includes/ExternalStoreTest.php b/tests/phpunit/includes/ExternalStoreTest.php new file mode 100644 index 0000000000..3c15f69755 --- /dev/null +++ b/tests/phpunit/includes/ExternalStoreTest.php @@ -0,0 +1,32 @@ +saved_wgExternalStores = $wgExternalStores ; + } + + function tearDown() { + global $wgExternalStores; + $wgExternalStores = $this->saved_wgExternalStores ; + } + + function testExternalStoreDoesNotFetchIncorrectURL() { + global $wgExternalStores; + $wgExternalStores = true; + + # Assertions for r68900 + $this->assertFalse( + ExternalStore::fetchFromURL( 'http://' ) ); + $this->assertFalse( + ExternalStore::fetchFromURL( 'ftp.wikimedia.org' ) ); + $this->assertFalse( + ExternalStore::fetchFromURL( '/super.txt' ) ); + } +} + diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php new file mode 100644 index 0000000000..f879011353 --- /dev/null +++ b/tests/phpunit/includes/ExtraParserTest.php @@ -0,0 +1,31 @@ +assertEquals( "

    $longLine

    ", + $parser->parse( $longLine, $t, $options )->getText() ); + } + } diff --git a/tests/phpunit/includes/GlobalTest.php b/tests/phpunit/includes/GlobalTest.php new file mode 100644 index 0000000000..d9116dc0ce --- /dev/null +++ b/tests/phpunit/includes/GlobalTest.php @@ -0,0 +1,380 @@ +originals['wgReadOnlyFile'] = $wgReadOnlyFile; + $wgReadOnlyFile = tempnam( wfTempDir(), "mwtest_readonly" ); + unlink( $wgReadOnlyFile ); + $wgContLang = $wgLang = Language::factory( 'en' ); + } + + function tearDown() { + global $wgReadOnlyFile; + if ( file_exists( $wgReadOnlyFile ) ) { + unlink( $wgReadOnlyFile ); + } + $wgReadOnlyFile = $this->originals['wgReadOnlyFile']; + } + + function testRandom() { + # This could hypothetically fail, but it shouldn't ;) + $this->assertFalse( + wfRandom() == wfRandom() ); + } + + function testUrlencode() { + $this->assertEquals( + "%E7%89%B9%E5%88%A5:Contributions/Foobar", + wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) ); + } + + function testReadOnlyEmpty() { + global $wgReadOnly; + $wgReadOnly = null; + + $this->assertFalse( wfReadOnly() ); + $this->assertFalse( wfReadOnly() ); + } + + function testReadOnlySet() { + global $wgReadOnly, $wgReadOnlyFile; + + $f = fopen( $wgReadOnlyFile, "wt" ); + fwrite( $f, 'Message' ); + fclose( $f ); + $wgReadOnly = null; + + $this->assertTrue( wfReadOnly() ); + $this->assertTrue( wfReadOnly() ); + + unlink( $wgReadOnlyFile ); + $wgReadOnly = null; + + $this->assertFalse( wfReadOnly() ); + $this->assertFalse( wfReadOnly() ); + } + + function testQuotedPrintable() { + $this->assertEquals( + "=?UTF-8?Q?=C4=88u=20legebla=3F?=", + UserMailer::quotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) ); + } + + function testTime() { + $start = wfTime(); + $this->assertType( 'float', $start ); + $end = wfTime(); + $this->assertTrue( $end > $start, "Time is running backwards!" ); + } + + function testArrayToCGI() { + $this->assertEquals( + "baz=AT%26T&foo=bar", + wfArrayToCGI( + array( 'baz' => 'AT&T', 'ignore' => '' ), + array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) ); + } + + function testMimeTypeMatch() { + $this->assertEquals( + 'text/html', + mimeTypeMatch( 'text/html', + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.7, + 'text/plain' => 0.3 ) ) ); + $this->assertEquals( + 'text/*', + mimeTypeMatch( 'text/html', + array( 'image/*' => 1.0, + 'text/*' => 0.5 ) ) ); + $this->assertEquals( + '*/*', + mimeTypeMatch( 'text/html', + array( '*/*' => 1.0 ) ) ); + $this->assertNull( + mimeTypeMatch( 'text/html', + array( 'image/png' => 1.0, + 'image/svg+xml' => 0.5 ) ) ); + } + + function testNegotiateType() { + $this->assertEquals( + 'text/html', + wfNegotiateType( + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.7, + 'text/plain' => 0.5, + 'text/*' => 0.2 ), + array( 'text/html' => 1.0 ) ) ); + $this->assertEquals( + 'application/xhtml+xml', + wfNegotiateType( + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.7, + 'text/plain' => 0.5, + 'text/*' => 0.2 ), + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.5 ) ) ); + $this->assertEquals( + 'text/html', + wfNegotiateType( + array( 'text/html' => 1.0, + 'text/plain' => 0.5, + 'text/*' => 0.5, + 'application/xhtml+xml' => 0.2 ), + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.5 ) ) ); + $this->assertEquals( + 'text/html', + wfNegotiateType( + array( 'text/*' => 1.0, + 'image/*' => 0.7, + '*/*' => 0.3 ), + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.5 ) ) ); + $this->assertNull( + wfNegotiateType( + array( 'text/*' => 1.0 ), + array( 'application/xhtml+xml' => 1.0 ) ) ); + } + + function testTimestamp() { + $t = gmmktime( 12, 34, 56, 1, 15, 2001 ); + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, $t ), + 'TS_UNIX to TS_MW' ); + $this->assertEquals( + '19690115123456', + wfTimestamp( TS_MW, -30281104 ), + 'Negative TS_UNIX to TS_MW' ); + $this->assertEquals( + 979562096, + wfTimestamp( TS_UNIX, $t ), + 'TS_UNIX to TS_UNIX' ); + $this->assertEquals( + '2001-01-15 12:34:56', + wfTimestamp( TS_DB, $t ), + 'TS_UNIX to TS_DB' ); + $this->assertEquals( + '20010115T123456Z', + wfTimestamp( TS_ISO_8601_BASIC, $t ), + 'TS_ISO_8601_BASIC to TS_DB' ); + + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, '20010115123456' ), + 'TS_MW to TS_MW' ); + $this->assertEquals( + 979562096, + wfTimestamp( TS_UNIX, '20010115123456' ), + 'TS_MW to TS_UNIX' ); + $this->assertEquals( + '2001-01-15 12:34:56', + wfTimestamp( TS_DB, '20010115123456' ), + 'TS_MW to TS_DB' ); + $this->assertEquals( + '20010115T123456Z', + wfTimestamp( TS_ISO_8601_BASIC, '20010115123456' ), + 'TS_MW to TS_ISO_8601_BASIC' ); + + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, '2001-01-15 12:34:56' ), + 'TS_DB to TS_MW' ); + $this->assertEquals( + 979562096, + wfTimestamp( TS_UNIX, '2001-01-15 12:34:56' ), + 'TS_DB to TS_UNIX' ); + $this->assertEquals( + '2001-01-15 12:34:56', + wfTimestamp( TS_DB, '2001-01-15 12:34:56' ), + 'TS_DB to TS_DB' ); + $this->assertEquals( + '20010115T123456Z', + wfTimestamp( TS_ISO_8601_BASIC, '2001-01-15 12:34:56' ), + 'TS_DB to TS_ISO_8601_BASIC' ); + + # rfc2822 section 3.3 + + $this->assertEquals( + 'Mon, 15 Jan 2001 12:34:56 GMT', + wfTimestamp( TS_RFC2822, '20010115123456' ), + 'TS_MW to TS_RFC2822' ); + + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, 'Mon, 15 Jan 2001 12:34:56 GMT' ), + 'TS_RFC2822 to TS_MW' ); + + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, ' Mon, 15 Jan 2001 12:34:56 GMT' ), + 'TS_RFC2822 with leading space to TS_MW' ); + + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, '15 Jan 2001 12:34:56 GMT' ), + 'TS_RFC2822 without optional day-of-week to TS_MW' ); + + # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space + # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2 + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, 'Mon, 15 Jan 2001 12:34:56 GMT' ), + 'TS_RFC2822 to TS_MW' ); + + # WSP = SP / HTAB ; rfc2234 + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, "Mon, 15 Jan\x092001 12:34:56 GMT" ), + 'TS_RFC2822 with HTAB to TS_MW' ); + + $this->assertEquals( + '20010115123456', + wfTimestamp( TS_MW, "Mon, 15 Jan\x09 \x09 2001 12:34:56 GMT" ), + 'TS_RFC2822 with HTAB and SP to TS_MW' ); + + $this->assertEquals( + '19941106084937', + wfTimestamp( TS_MW, "Sun, 6 Nov 94 08:49:37 GMT" ), + 'TS_RFC2822 with obsolete year to TS_MW' ); + } + + /** + * This test checks wfTimestamp() with values outside. + * It needs PHP 64 bits or PHP > 5.1. + * See r74778 and bug 25451 + */ + function testOldTimestamps() { + $this->assertEquals( 'Fri, 13 Dec 1901 20:45:54 GMT', + wfTimestamp( TS_RFC2822, '19011213204554' ), + 'Earliest time according to php documentation' ); + + $this->assertEquals( 'Tue, 19 Jan 2038 03:14:07 GMT', + wfTimestamp( TS_RFC2822, '20380119031407' ), + 'Latest 32 bit time' ); + + $this->assertEquals( '-2147483648', + wfTimestamp( TS_UNIX, '19011213204552' ), + 'Earliest 32 bit unix time' ); + + $this->assertEquals( '2147483647', + wfTimestamp( TS_UNIX, '20380119031407' ), + 'Latest 32 bit unix time' ); + + $this->assertEquals( 'Fri, 13 Dec 1901 20:45:52 GMT', + wfTimestamp( TS_RFC2822, '19011213204552' ), + 'Earliest 32 bit time' ); + + $this->assertEquals( 'Fri, 13 Dec 1901 20:45:51 GMT', + wfTimestamp( TS_RFC2822, '19011213204551' ), + 'Earliest 32 bit time - 1' ); + + $this->assertEquals( 'Tue, 19 Jan 2038 03:14:08 GMT', + wfTimestamp( TS_RFC2822, '20380119031408' ), + 'Latest 32 bit time + 1' ); + + $this->assertEquals( '19011212000000', + wfTimestamp(TS_MW, '19011212000000'), + 'Convert to itself r74778#c10645' ); + + $this->assertEquals( '-2147483649', + wfTimestamp( TS_UNIX, '19011213204551' ), + 'Earliest 32 bit unix time - 1' ); + + $this->assertEquals( '2147483648', + wfTimestamp( TS_UNIX, '20380119031408' ), + 'Latest 32 bit unix time + 1' ); + + $this->assertEquals( '19011213204551', + wfTimestamp( TS_MW, '-2147483649' ), + '1901 negative unix time to MediaWiki' ); + + $this->assertEquals( '18010115123456', + wfTimestamp( TS_MW, '-5331871504' ), + '1801 negative unix time to MediaWiki' ); + + $this->assertEquals( 'Tue, 09 Aug 0117 12:34:56 GMT', + wfTimestamp( TS_RFC2822, '0117-08-09 12:34:56'), + 'Death of Roman Emperor [[Trajan]]'); + + /* FIXME: 00 to 101 years are taken as being in [1970-2069] */ + + $this->assertEquals( 'Sun, 01 Jan 0101 00:00:00 GMT', + wfTimestamp( TS_RFC2822, '-58979923200'), + '1/1/101'); + + $this->assertEquals( 'Mon, 01 Jan 0001 00:00:00 GMT', + wfTimestamp( TS_RFC2822, '-62135596800'), + 'Year 1'); + + /* It is not clear if we should generate a year 0 or not + * We are completely off RFC2822 requirement of year being + * 1900 or later. + */ + $this->assertEquals( 'Wed, 18 Oct 0000 00:00:00 GMT', + wfTimestamp( TS_RFC2822, '-62142076800'), + 'ISO 8601:2004 [[year 0]], also called [[1 BC]]'); + } + + function testHttpDate() { + # The Resource Loader uses wfTimestamp() to convert timestamps + # from If-Modified-Since header. + # Thus it must be able to parse all rfc2616 date formats + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1 + + $this->assertEquals( + '19941106084937', + wfTimestamp( TS_MW, 'Sun, 06 Nov 1994 08:49:37 GMT' ), + 'RFC 822 date' ); + + $this->assertEquals( + '19941106084937', + wfTimestamp( TS_MW, 'Sunday, 06-Nov-94 08:49:37 GMT' ), + 'RFC 850 date' ); + + $this->assertEquals( + '19941106084937', + wfTimestamp( TS_MW, 'Sun Nov 6 08:49:37 1994' ), + "ANSI C's asctime() format" ); + + // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171 + $this->assertEquals( + '20101122141242', + wfTimestamp( TS_MW, 'Mon, 22 Nov 2010 14:12:42 GMT; length=52626' ), + "Netscape extension to HTTP/1.0" ); + + } + + function testBasename() { + $sets = array( + '' => '', + '/' => '', + '\\' => '', + '//' => '', + '\\\\' => '', + 'a' => 'a', + 'aaaa' => 'aaaa', + '/a' => 'a', + '\\a' => 'a', + '/aaaa' => 'aaaa', + '\\aaaa' => 'aaaa', + '/aaaa/' => 'aaaa', + '\\aaaa\\' => 'aaaa', + '\\aaaa\\' => 'aaaa', + '/mnt/upload3/wikipedia/en/thumb/8/8b/Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg' => '93px-Zork_Grand_Inquisitor_box_cover.jpg', + 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE' => 'VIEWER.EXE', + 'Östergötland_coat_of_arms.png' => 'Östergötland_coat_of_arms.png', + ); + foreach ( $sets as $from => $to ) { + $this->assertEquals( $to, wfBaseName( $from ), + "wfBaseName('$from') => '$to'" ); + } + } + + /* TODO: many more! */ +} + + diff --git a/tests/phpunit/includes/HttpTest.php b/tests/phpunit/includes/HttpTest.php new file mode 100644 index 0000000000..a48c7bb4f4 --- /dev/null +++ b/tests/phpunit/includes/HttpTest.php @@ -0,0 +1,526 @@ + "review=test" ); + + function setUp() { + putenv( "http_proxy" ); /* Remove any proxy env var, so curl doesn't get confused */ + if ( is_array( self::$content ) ) { + return; + } + self::$has_curl = function_exists( 'curl_init' ); + self::$has_fopen = wfIniGetBool( 'allow_url_fopen' ); + + if ( !file_exists( "/usr/bin/curl" ) ) { + $this->markTestIncomplete( "This test requires the curl binary at /usr/bin/curl. If you have curl, please file a bug on this test, or, better yet, provide a patch." ); + } + + $content = tempnam( wfTempDir(), "" ); + $headers = tempnam( wfTempDir(), "" ); + if ( !$content && !$headers ) { + die( "Couldn't create temp file!" ); + } + + // This probably isn't the best test for a proxy, but it works on my system! + system( "curl -0 -o $content -s " . self::$proxy ); + $out = file_get_contents( $content ); + if ( $out ) { + self::$has_proxy = true; + } + + /* Maybe use wget instead of curl here ... just to use a different codebase? */ + foreach ( $this->test_geturl as $u ) { + system( "curl -0 -s -D $headers '$u' -o $content" ); + self::$content["GET $u"] = file_get_contents( $content ); + self::$headers["GET $u"] = file_get_contents( $headers ); + } + foreach ( $this->test_requesturl as $u ) { + system( "curl -0 -s -X POST -H 'Content-Length: 0' -D $headers '$u' -o $content" ); + self::$content["POST $u"] = file_get_contents( $content ); + self::$headers["POST $u"] = file_get_contents( $headers ); + } + foreach ( $this->test_posturl as $u => $postData ) { + system( "curl -0 -s -X POST -d '$postData' -D $headers '$u' -o $content" ); + self::$content["POST $u => $postData"] = file_get_contents( $content ); + self::$headers["POST $u => $postData"] = file_get_contents( $headers ); + } + unlink( $content ); + unlink( $headers ); + } + + + function testInstantiation() { + Http::$httpEngine = false; + + $r = MWHttpRequest::factory( "http://www.example.com/" ); + if ( self::$has_curl ) { + $this->assertThat( $r, $this->isInstanceOf( 'CurlHttpRequest' ) ); + } else { + $this->assertThat( $r, $this->isInstanceOf( 'PhpHttpRequest' ) ); + } + unset( $r ); + + if ( !self::$has_fopen ) { + $this->setExpectedException( 'MWException' ); + } + Http::$httpEngine = 'php'; + $r = MWHttpRequest::factory( "http://www.example.com/" ); + $this->assertThat( $r, $this->isInstanceOf( 'PhpHttpRequest' ) ); + unset( $r ); + + if ( !self::$has_curl ) { + $this->setExpectedException( 'MWException' ); + } + Http::$httpEngine = 'curl'; + $r = MWHttpRequest::factory( "http://www.example.com/" ); + if ( self::$has_curl ) { + $this->assertThat( $r, $this->isInstanceOf( 'CurlHttpRequest' ) ); + } + } + + function runHTTPFailureChecks() { + // Each of the following requests should result in a failure. + + $timeout = 1; + $start_time = time(); + $r = Http::get( "http://www.example.com:1/", $timeout ); + $end_time = time(); + $this->assertLessThan( $timeout + 2, $end_time - $start_time, + "Request took less than {$timeout}s via " . Http::$httpEngine ); + $this->assertEquals( $r, false, "false -- what we get on error from Http::get()" ); + + $r = Http::get( "http://www.example.com/this-file-does-not-exist", $timeout ); + $this->assertFalse( $r, "False on 404s" ); + + + $r = MWHttpRequest::factory( "http://www.example.com/this-file-does-not-exist" ); + $er = $r->execute(); + if ( $r instanceof PhpHttpRequest && version_compare( '5.2.10', phpversion(), '>' ) ) { + $this->assertRegexp( "/HTTP request failed/", $er->getWikiText() ); + } else { + $this->assertRegexp( "/404 Not Found/", $er->getWikiText() ); + } + } + + function testFailureDefault() { + Http::$httpEngine = false; + $this->runHTTPFailureChecks(); + } + + function testFailurePhp() { + if ( !self::$has_fopen ) { + $this->markTestIncomplete( "This test requires allow_url_fopen=true." ); + } + + Http::$httpEngine = "php"; + $this->runHTTPFailureChecks(); + } + + function testFailureCurl() { + if ( !self::$has_curl ) { + $this->markTestIncomplete( "This test requires curl." ); + } + + Http::$httpEngine = "curl"; + $this->runHTTPFailureChecks(); + } + + /* ./phase3/includes/Import.php:1108: $data = Http::request( $method, $url ); */ + /* ./includes/Import.php:1124: $link = Title::newFromText( "$interwiki:Special:Export/$page" ); */ + /* ./includes/Import.php:1134: return ImportStreamSource::newFromURL( $url, "POST" ); */ + function runHTTPRequests( $proxy = null ) { + $opt = array(); + + if ( $proxy ) { + $opt['proxy'] = $proxy; + } elseif ( $proxy === false ) { + $opt['noProxy'] = true; + } + + /* no postData here because the only request I could find in code so far didn't have any */ + foreach ( $this->test_requesturl as $u ) { + $r = Http::request( "POST", $u, $opt ); + $this->assertEquals( self::$content["POST $u"], "$r", "POST $u with " . Http::$httpEngine ); + } + } + + function testRequestDefault() { + Http::$httpEngine = false; + $this->runHTTPRequests(); + } + + function testRequestPhp() { + if ( !self::$has_fopen ) { + $this->markTestIncomplete( "This test requires allow_url_fopen=true." ); + } + + Http::$httpEngine = "php"; + $this->runHTTPRequests(); + } + + function testRequestCurl() { + if ( !self::$has_curl ) { + $this->markTestIncomplete( "This test requires curl." ); + } + + Http::$httpEngine = "curl"; + $this->runHTTPRequests(); + } + + function runHTTPGets( $proxy = null ) { + $opt = array(); + + if ( $proxy ) { + $opt['proxy'] = $proxy; + } elseif ( $proxy === false ) { + $opt['noProxy'] = true; + } + + foreach ( $this->test_geturl as $u ) { + $r = Http::get( $u, 30, $opt ); /* timeout of 30s */ + $this->assertEquals( self::$content["GET $u"], "$r", "Get $u with " . Http::$httpEngine ); + } + } + + function testGetDefault() { + Http::$httpEngine = false; + $this->runHTTPGets(); + } + + function testGetPhp() { + if ( !self::$has_fopen ) { + $this->markTestIncomplete( "This test requires allow_url_fopen=true." ); + } + + Http::$httpEngine = "php"; + $this->runHTTPGets(); + } + + function testGetCurl() { + if ( !self::$has_curl ) { + $this->markTestIncomplete( "This test requires curl." ); + } + + Http::$httpEngine = "curl"; + $this->runHTTPGets(); + } + + /* ./phase3/maintenance/parserTests.inc:1618: return Http::post( $url, array( 'postData' => wfArrayToCGI( $data ) ) ); */ + function runHTTPPosts( $proxy = null ) { + $opt = array(); + + if ( $proxy ) { + $opt['proxy'] = $proxy; + } elseif ( $proxy === false ) { + $opt['noProxy'] = true; + } + + foreach ( $this->test_posturl as $u => $postData ) { + $opt['postData'] = $postData; + $r = Http::post( $u, $opt ); + $this->assertEquals( self::$content["POST $u => $postData"], "$r", + "POST $u (postData=$postData) with " . Http::$httpEngine ); + } + } + + function testPostDefault() { + Http::$httpEngine = false; + $this->runHTTPPosts(); + } + + function testPostPhp() { + if ( !self::$has_fopen ) { + $this->markTestIncomplete( "This test requires allow_url_fopen=true." ); + } + + Http::$httpEngine = "php"; + $this->runHTTPPosts(); + } + + function testPostCurl() { + if ( !self::$has_curl ) { + $this->markTestIncomplete( "This test requires curl." ); + } + + Http::$httpEngine = "curl"; + $this->runHTTPPosts(); + } + + function runProxyRequests() { + if ( !self::$has_proxy ) { + $this->markTestIncomplete( "This test requires a proxy." ); + } + $this->runHTTPGets( self::$proxy ); + $this->runHTTPPosts( self::$proxy ); + $this->runHTTPRequests( self::$proxy ); + + // Set false here to do noProxy + $this->runHTTPGets( false ); + $this->runHTTPPosts( false ); + $this->runHTTPRequests( false ); + } + + function testProxyDefault() { + Http::$httpEngine = false; + $this->runProxyRequests(); + } + + function testProxyPhp() { + if ( !self::$has_fopen ) { + $this->markTestIncomplete( "This test requires allow_url_fopen=true." ); + } + + Http::$httpEngine = 'php'; + $this->runProxyRequests(); + } + + function testProxyCurl() { + if ( !self::$has_curl ) { + $this->markTestIncomplete( "This test requires curl." ); + } + + Http::$httpEngine = 'curl'; + $this->runProxyRequests(); + } + + function testIsLocalUrl() { + } + + /* ./extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.body.php:559: $user_agent = Http::userAgent(); */ + function testUserAgent() { + } + + function testIsValidUrl() { + } + + function testValidateCookieDomain() { + $this->assertFalse( Cookie::validateCookieDomain( "co.uk" ) ); + $this->assertFalse( Cookie::validateCookieDomain( ".co.uk" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "gov.uk" ) ); + $this->assertFalse( Cookie::validateCookieDomain( ".gov.uk" ) ); + $this->assertTrue( Cookie::validateCookieDomain( "supermarket.uk" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "uk" ) ); + $this->assertFalse( Cookie::validateCookieDomain( ".uk" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "127.0.0." ) ); + $this->assertFalse( Cookie::validateCookieDomain( "127." ) ); + $this->assertFalse( Cookie::validateCookieDomain( "127.0.0.1." ) ); + $this->assertTrue( Cookie::validateCookieDomain( "127.0.0.1" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "333.0.0.1" ) ); + $this->assertTrue( Cookie::validateCookieDomain( "example.com" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "example.com." ) ); + $this->assertTrue( Cookie::validateCookieDomain( ".example.com" ) ); + + $this->assertTrue( Cookie::validateCookieDomain( ".example.com", "www.example.com" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "example.com", "www.example.com" ) ); + $this->assertTrue( Cookie::validateCookieDomain( "127.0.0.1", "127.0.0.1" ) ); + $this->assertFalse( Cookie::validateCookieDomain( "127.0.0.1", "localhost" ) ); + + + } + + function testSetCooke() { + $c = new MockCookie( "name", "value", + array( + "domain" => "ac.th", + "path" => "/path/", + ) ); + $this->assertFalse( $c->canServeDomain( "ac.th" ) ); + + $c = new MockCookie( "name", "value", + array( + "domain" => "example.com", + "path" => "/path/", + ) ); + + $this->assertTrue( $c->canServeDomain( "example.com" ) ); + $this->assertFalse( $c->canServeDomain( "www.example.com" ) ); + + $c = new MockCookie( "name", "value", + array( + "domain" => ".example.com", + "path" => "/path/", + ) ); + + $this->assertFalse( $c->canServeDomain( "www.example.net" ) ); + $this->assertFalse( $c->canServeDomain( "example.com" ) ); + $this->assertTrue( $c->canServeDomain( "www.example.com" ) ); + + $this->assertFalse( $c->canServePath( "/" ) ); + $this->assertFalse( $c->canServePath( "/bogus/path/" ) ); + $this->assertFalse( $c->canServePath( "/path" ) ); + $this->assertTrue( $c->canServePath( "/path/" ) ); + + $this->assertTrue( $c->isUnExpired() ); + + $this->assertEquals( "", $c->serializeToHttpRequest( "/path/", "www.example.net" ) ); + $this->assertEquals( "", $c->serializeToHttpRequest( "/", "www.example.com" ) ); + $this->assertEquals( "name=value", $c->serializeToHttpRequest( "/path/", "www.example.com" ) ); + + $c = new MockCookie( "name", "value", + array( + "domain" => "www.example.com", + "path" => "/path/", + ) ); + $this->assertFalse( $c->canServeDomain( "example.com" ) ); + $this->assertFalse( $c->canServeDomain( "www.example.net" ) ); + $this->assertTrue( $c->canServeDomain( "www.example.com" ) ); + + $c = new MockCookie( "name", "value", + array( + "domain" => ".example.com", + "path" => "/path/", + "expires" => "-1 day", + ) ); + $this->assertFalse( $c->isUnExpired() ); + $this->assertEquals( "", $c->serializeToHttpRequest( "/path/", "www.example.com" ) ); + + $c = new MockCookie( "name", "value", + array( + "domain" => ".example.com", + "path" => "/path/", + "expires" => "+1 day", + ) ); + $this->assertTrue( $c->isUnExpired() ); + $this->assertEquals( "name=value", $c->serializeToHttpRequest( "/path/", "www.example.com" ) ); + } + + function testCookieJarSetCookie() { + $cj = new CookieJar; + $cj->setCookie( "name", "value", + array( + "domain" => ".example.com", + "path" => "/path/", + ) ); + $cj->setCookie( "name2", "value", + array( + "domain" => ".example.com", + "path" => "/path/sub", + ) ); + $cj->setCookie( "name3", "value", + array( + "domain" => ".example.com", + "path" => "/", + ) ); + $cj->setCookie( "name4", "value", + array( + "domain" => ".example.net", + "path" => "/path/", + ) ); + $cj->setCookie( "name5", "value", + array( + "domain" => ".example.net", + "path" => "/path/", + "expires" => "-1 day", + ) ); + + $this->assertEquals( "name4=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) ); + $this->assertEquals( "name3=value", $cj->serializeToHttpRequest( "/", "www.example.com" ) ); + $this->assertEquals( "name=value; name3=value", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) ); + + $cj->setCookie( "name5", "value", + array( + "domain" => ".example.net", + "path" => "/path/", + "expires" => "+1 day", + ) ); + $this->assertEquals( "name4=value; name5=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) ); + + $cj->setCookie( "name4", "value", + array( + "domain" => ".example.net", + "path" => "/path/", + "expires" => "-1 day", + ) ); + $this->assertEquals( "name5=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) ); + } + + function testParseResponseHeader() { + $cj = new CookieJar; + + $h[] = "Set-Cookie: name4=value; domain=.example.com; path=/; expires=Mon, 09-Dec-2029 13:46:00 GMT"; + $cj->parseCookieResponseHeader( $h[0], "www.example.com" ); + $this->assertEquals( "name4=value", $cj->serializeToHttpRequest( "/", "www.example.com" ) ); + + $h[] = "name4=value2; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT"; + $cj->parseCookieResponseHeader( $h[1], "www.example.com" ); + $this->assertEquals( "", $cj->serializeToHttpRequest( "/", "www.example.com" ) ); + $this->assertEquals( "name4=value2", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) ); + + $h[] = "name5=value3; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT"; + $cj->parseCookieResponseHeader( $h[2], "www.example.com" ); + $this->assertEquals( "name4=value2; name5=value3", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) ); + + $h[] = "name6=value3; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT"; + $cj->parseCookieResponseHeader( $h[3], "www.example.com" ); + $this->assertEquals( "", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) ); + + $h[] = "name6=value0; domain=.example.net; path=/path/; expires=Mon, 09-Dec-1999 13:46:00 GMT"; + $cj->parseCookieResponseHeader( $h[4], "www.example.net" ); + $this->assertEquals( "", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) ); + + $h[] = "name6=value4; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT"; + $cj->parseCookieResponseHeader( $h[5], "www.example.net" ); + $this->assertEquals( "name6=value4", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) ); + } + + function runCookieRequests() { + $r = MWHttpRequest::factory( "http://www.php.net/manual", array( 'followRedirects' => true ) ); + $r->execute(); + + $jar = $r->getCookieJar(); + $this->assertThat( $jar, $this->isInstanceOf( 'CookieJar' ) ); + + if ( $r instanceof PhpHttpRequest && version_compare( '5.1.7', phpversion(), '>' ) ) { + $this->markTestSkipped( 'Redirection fails or crashes PHP on 5.1.6 and prior' ); + } + $serialized = $jar->serializeToHttpRequest( "/search?q=test", "www.php.net" ); + $this->assertRegExp( '/\bCOUNTRY=[^=;]+/', $serialized ); + $this->assertRegExp( '/\bLAST_LANG=[^=;]+/', $serialized ); + $this->assertEquals( '', $jar->serializeToHttpRequest( "/search?q=test", "www.php.com" ) ); + } + + function testCookieRequestDefault() { + Http::$httpEngine = false; + $this->runCookieRequests(); + } + function testCookieRequestPhp() { + if ( !self::$has_fopen ) { + $this->markTestIncomplete( "This test requires allow_url_fopen=true." ); + } + + Http::$httpEngine = 'php'; + $this->runCookieRequests(); + } + function testCookieRequestCurl() { + if ( !self::$has_curl ) { + $this->markTestIncomplete( "This test requires curl." ); + } + + Http::$httpEngine = 'curl'; + $this->runCookieRequests(); + } +} diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php new file mode 100644 index 0000000000..ee092997bd --- /dev/null +++ b/tests/phpunit/includes/IPTest.php @@ -0,0 +1,298 @@ +assertFalse( IP::isIPAddress( false ), 'Boolean false is not an IP' ); + $this->assertFalse( IP::isIPAddress( true ), 'Boolean true is not an IP' ); + $this->assertFalse( IP::isIPAddress( "" ), 'Empty string is not an IP' ); + $this->assertFalse( IP::isIPAddress( 'abc' ), 'Garbage IP string' ); + $this->assertFalse( IP::isIPAddress( ':' ), 'Single ":" is not an IP' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurence' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurence, last at end' ); + $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurence, firt at beginning' ); + $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' ); + $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' ); + $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' ); + $this->assertFalse( IP::isIPAddress( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' ); + $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' ); + $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) ); + $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' ); + $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' ); + + $validIPs = array( 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac', + '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ); + foreach ( $validIPs as $ip ) { + $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" ); + } + } + + public function testisIPv6() { + $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' ); + $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' ); + $this->assertTrue( IP::isIPv6( 'fc:100::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) ); + $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' ); + $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' ); + + $this->assertFalse( IP::isIPv6( ':::' ) ); + $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' ); + $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' ); + $this->assertTrue( IP::isIPv6( '::0' ) ); + $this->assertTrue( IP::isIPv6( '::fc' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) ); + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' ); + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); + + $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' ); + $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' ); + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); + + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) ); + } + + public function testisIPv4() { + $this->assertFalse( IP::isIPv4( false ), 'Boolean false is not an IP' ); + $this->assertFalse( IP::isIPv4( true ), 'Boolean true is not an IP' ); + $this->assertFalse( IP::isIPv4( "" ), 'Empty string is not an IP' ); + $this->assertFalse( IP::isIPv4( 'abc' ) ); + $this->assertFalse( IP::isIPv4( ':' ) ); + $this->assertFalse( IP::isIPv4( '124.24.52' ), 'IPv4 not enough quads' ); + $this->assertFalse( IP::isIPv4( '24.324.52.13' ), 'IPv4 out of range' ); + $this->assertFalse( IP::isIPv4( '.24.52.13' ), 'IPv4 starts with period' ); + + $this->assertTrue( IP::isIPv4( '124.24.52.13' ) ); + $this->assertTrue( IP::isIPv4( '1.24.52.13' ) ); + $this->assertTrue( IP::isIPv4( '74.24.52.13/20', 'IPv4 range' ) ); + } + + // tests isValid() + public function testValidIPs() { + foreach ( range( 0, 255 ) as $i ) { + $a = sprintf( "%03d", $i ); + $b = sprintf( "%02d", $i ); + $c = sprintf( "%01d", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f.$f.$f.$f"; + $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv4 address" ); + } + } + foreach ( range( 0x0, 0xFFFF ) as $i ) { + $a = sprintf( "%04x", $i ); + $b = sprintf( "%03x", $i ); + $c = sprintf( "%02x", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f:$f:$f:$f:$f:$f:$f:$f"; + $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv6 address" ); + } + } + } + + // tests isValid() + public function testInvalidIPs() { + // Out of range... + foreach ( range( 256, 999 ) as $i ) { + $a = sprintf( "%03d", $i ); + $b = sprintf( "%02d", $i ); + $c = sprintf( "%01d", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f.$f.$f.$f"; + $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" ); + } + } + foreach ( range( 'g', 'z' ) as $i ) { + $a = sprintf( "%04s", $i ); + $b = sprintf( "%03s", $i ); + $c = sprintf( "%02s", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f:$f:$f:$f:$f:$f:$f:$f"; + $this->assertFalse( IP::isValid( $ip ) , "$ip is not a valid IPv6 address" ); + } + } + // Have CIDR + $ipCIDRs = array( + '212.35.31.121/32', + '212.35.31.121/18', + '212.35.31.121/24', + '::ff:d:321:5/96', + 'ff::d3:321:5/116', + 'c:ff:12:1:ea:d:321:5/120', + ); + foreach ( $ipCIDRs as $i ) { + $this->assertFalse( IP::isValid( $i ), + "$i is an invalid IP address because it is a block" ); + } + // Incomplete/garbage + $invalid = array( + 'www.xn--var-xla.net', + '216.17.184.G', + '216.17.184.1.', + '216.17.184', + '216.17.184.', + '256.17.184.1' + ); + foreach ( $invalid as $i ) { + $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" ); + } + } + + // tests isValidBlock() + public function testValidBlocks() { + $valid = array( + '116.17.184.5/32', + '0.17.184.5/30', + '16.17.184.1/24', + '30.242.52.14/1', + '10.232.52.13/8', + '30.242.52.14/0', + '::e:f:2001/96', + '::c:f:2001/128', + '::10:f:2001/70', + '::fe:f:2001/1', + '::6d:f:2001/8', + '::fe:f:2001/0', + ); + foreach ( $valid as $i ) { + $this->assertTrue( IP::isValidBlock( $i ), "$i is a valid IP block" ); + } + } + + // tests isValidBlock() + public function testInvalidBlocks() { + $invalid = array( + '116.17.184.5/33', + '0.17.184.5/130', + '16.17.184.1/-1', + '10.232.52.13/*', + '7.232.52.13/ab', + '11.232.52.13/', + '::e:f:2001/129', + '::c:f:2001/228', + '::10:f:2001/-1', + '::6d:f:2001/*', + '::86:f:2001/ab', + '::23:f:2001/', + ); + foreach ( $invalid as $i ) { + $this->assertFalse( IP::isValidBlock( $i ), "$i is not a valid IP block" ); + } + } + + // test wrapper around ip2long which might return -1 or false depending on PHP version + public function testip2longWrapper() { + // fixme : add more tests ? + $this->assertEquals( pow(2,32) - 1, IP::toUnsigned( '255.255.255.255' )); + $i = 'IN.VA.LI.D'; + $this->assertFalse( IP::toUnSigned( $i ) ); + } + + // tests isPublic() + public function testPrivateIPs() { + $private = array( 'fc::3', 'fc::ff', '::1', '10.0.0.1', '172.16.0.1', '192.168.0.1' ); + foreach ( $private as $p ) { + $this->assertFalse( IP::isPublic( $p ), "$p is not a public IP address" ); + } + } + + // Private wrapper used to test CIDR Parsing. + private function assertFalseCIDR( $CIDR, $msg='' ) { + $ff = array( false, false ); + $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg ); + } + + // Private wrapper to test network shifting using only dot notation + private function assertNet( $expected, $CIDR ) { + $parse = IP::parseCIDR( $CIDR ); + $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" ); + } + + public function testHexToQuad() { + $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '00000001' ) ); + $this->assertEquals( '255.0.0.0' , IP::hexToQuad( 'FF000000' ) ); + $this->assertEquals( '255.255.255.255', IP::hexToQuad( 'FFFFFFFF' ) ); + $this->assertEquals( '10.188.222.255' , IP::hexToQuad( '0ABCDEFF' ) ); + // hex not left-padded... + $this->assertEquals( '0.0.0.0' , IP::hexToQuad( '0' ) ); + $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '1' ) ); + $this->assertEquals( '0.0.0.255' , IP::hexToQuad( 'FF' ) ); + $this->assertEquals( '0.0.255.0' , IP::hexToQuad( 'FF00' ) ); + } + + public function testHexToOctet() { + $this->assertEquals( '0:0:0:0:0:0:0:1', + IP::hexToOctet( '00000000000000000000000000000001' ) ); + $this->assertEquals( '0:0:0:0:0:0:FF:3', + IP::hexToOctet( '00000000000000000000000000FF0003' ) ); + $this->assertEquals( '0:0:0:0:0:0:FF00:6', + IP::hexToOctet( '000000000000000000000000FF000006' ) ); + $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', + IP::hexToOctet( '000000000000000000000000FCCFFAFF' ) ); + $this->assertEquals( 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', + IP::hexToOctet( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ) ); + // hex not left-padded... + $this->assertEquals( '0:0:0:0:0:0:0:0' , IP::hexToOctet( '0' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:1' , IP::hexToOctet( '1' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:FF' , IP::hexToOctet( 'FF' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:FFD0' , IP::hexToOctet( 'FFD0' ) ); + $this->assertEquals( '0:0:0:0:0:0:FA00:0' , IP::hexToOctet( 'FA000000' ) ); + $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', IP::hexToOctet( 'FCCFFAFF' ) ); + } + + /* + * IP::parseCIDR() returns an array containing a signed IP address + * representing the network mask and the bit mask. + */ + function testCIDRParsing() { + $this->assertFalseCIDR( '192.0.2.0' , "missing mask" ); + $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" ); + + // Verify if statement + $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" ); + $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" ); + $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" ); + $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" ); + + // Check internal logic + # 0 mask always result in array(0,0) + $this->assertEquals( array( 0, 0 ), IP::parseCIDR('192.0.0.2/0') ); + $this->assertEquals( array( 0, 0 ), IP::parseCIDR('0.0.0.0/0') ); + $this->assertEquals( array( 0, 0 ), IP::parseCIDR('255.255.255.255/0') ); + + // FIXME : add more tests. + + # This part test network shifting + $this->assertNet( '192.0.0.0' , '192.0.0.2/24' ); + $this->assertNet( '192.168.5.0', '192.168.5.13/24'); + $this->assertNet( '10.0.0.160' , '10.0.0.161/28' ); + $this->assertNet( '10.0.0.0' , '10.0.0.3/28' ); + $this->assertNet( '10.0.0.0' , '10.0.0.3/30' ); + $this->assertNet( '10.0.0.4' , '10.0.0.4/30' ); + $this->assertNet( '172.17.32.0', '172.17.35.48/21' ); + $this->assertNet( '10.128.0.0' , '10.135.0.0/9' ); + $this->assertNet( '134.0.0.0' , '134.0.5.1/8' ); + } +} diff --git a/tests/phpunit/includes/ImageFunctionsTest.php b/tests/phpunit/includes/ImageFunctionsTest.php new file mode 100644 index 0000000000..4de4e63d4f --- /dev/null +++ b/tests/phpunit/includes/ImageFunctionsTest.php @@ -0,0 +1,48 @@ + 50, + 'height' => 50, + 'tests' => array( + 50 => 50, + 17 => 17, + 18 => 18 ) ), + array( + 'width' => 366, + 'height' => 300, + 'tests' => array( + 50 => 61, + 17 => 21, + 18 => 22 ) ), + array( + 'width' => 300, + 'height' => 366, + 'tests' => array( + 50 => 41, + 17 => 14, + 18 => 15 ) ), + array( + 'width' => 100, + 'height' => 400, + 'tests' => array( + 50 => 12, + 17 => 4, + 18 => 4 ) ) ); + foreach ( $vals as $row ) { + extract( $row ); + foreach ( $tests as $max => $expected ) { + $y = round( $expected * $height / $width ); + $result = wfFitBoxWidth( $width, $height, $max ); + $y2 = round( $result * $height / $width ); + $this->assertEquals( $expected, + $result, + "($width, $height, $max) wanted: {$expected}x$y, got: {$result}x$y2" ); + } + } + } +} + + diff --git a/tests/phpunit/includes/LanguageConverterTest.php b/tests/phpunit/includes/LanguageConverterTest.php new file mode 100644 index 0000000000..944195f4db --- /dev/null +++ b/tests/phpunit/includes/LanguageConverterTest.php @@ -0,0 +1,128 @@ +lang = new LanguageToTest(); + $this->lc = new TestConverter( $this->lang, 'tg', + array( 'tg', 'tg-latn' ) ); + } + + function tearDown() { + global $wgMemc, $wgContLang; + unset( $wgMemc ); + unset( $this->lc ); + unset( $this->lang ); + $wgContLang = Language::factory( 'en' ); + } + + function testGetPreferredVariantDefaults() { + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantHeaders() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'tg-latn' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantHeaderWeight() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'tg;q=1' ); + + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantHeaderWeight2() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'tg-latn;q=1' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantHeaderMulti() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'en, tg-latn;q=1' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantUserOption() { + global $wgUser; + + $wgUser = new User; + $wgUser->setId( 1 ); + $wgUser->mDataLoaded = true; + $wgUser->mOptionsLoaded = true; + $wgUser->setOption( 'variant', 'tg-latn' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantHeaderUserVsUrl() { + global $wgRequest, $wgUser, $wgContLang; + + $wgContLang = Language::factory( 'tg-latn' ); + $wgRequest->setVal( 'variant', 'tg' ); + $wgUser = User::newFromId( "admin" ); + $wgUser->setId( 1 ); + $wgUser->mDataLoaded = true; + $wgUser->mOptionsLoaded = true; + $wgUser->setOption( 'variant', 'tg-latn' ); // The user's data is ignored + // because the variant is set in the URL. + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } + + + function testGetPreferredVariantDefaultLanguageVariant() { + global $wgDefaultLanguageVariant; + + $wgDefaultLanguageVariant = 'tg-latn'; + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + function testGetPreferredVariantDefaultLanguageVsUrlVariant() { + global $wgDefaultLanguageVariant, $wgRequest, $wgContLang; + + $wgContLang = Language::factory( 'tg-latn' ); + $wgDefaultLanguageVariant = 'tg'; + $wgRequest->setVal( 'variant', null ); + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } +} + +/** + * Test converter (from Tajiki to latin orthography) + */ +class TestConverter extends LanguageConverter { + private $table = array( + 'б' => 'b', + 'в' => 'v', + 'г' => 'g', + ); + + function loadDefaultTables() { + $this->mTables = array( + 'tg-latn' => new ReplacementArray( $this->table ), + 'tg' => new ReplacementArray() + ); + } + +} + +class LanguageToTest extends Language { + function __construct() { + parent::__construct(); + $variants = array( 'tg', 'tg-latn' ); + $this->mConverter = new TestConverter( $this, 'tg', $variants ); + } +} diff --git a/tests/phpunit/includes/LicensesTest.php b/tests/phpunit/includes/LicensesTest.php new file mode 100644 index 0000000000..0008a7772d --- /dev/null +++ b/tests/phpunit/includes/LicensesTest.php @@ -0,0 +1,14 @@ + $str ) ); + $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) ); + } +} diff --git a/tests/phpunit/includes/LocalFileTest.php b/tests/phpunit/includes/LocalFileTest.php new file mode 100644 index 0000000000..32aa51579c --- /dev/null +++ b/tests/phpunit/includes/LocalFileTest.php @@ -0,0 +1,99 @@ + 'test', + 'directory' => '/testdir', + 'url' => '/testurl', + 'hashLevels' => 2, + 'transformVia404' => false, + ); + $this->repo_hl0 = new LocalRepo( array( 'hashLevels' => 0 ) + $info ); + $this->repo_hl2 = new LocalRepo( array( 'hashLevels' => 2 ) + $info ); + $this->repo_lc = new LocalRepo( array( 'initialCapital' => false ) + $info ); + $this->file_hl0 = $this->repo_hl0->newFile( 'test!' ); + $this->file_hl2 = $this->repo_hl2->newFile( 'test!' ); + $this->file_lc = $this->repo_lc->newFile( 'test!' ); + } + + function testGetHashPath() { + $this->assertEquals( '', $this->file_hl0->getHashPath() ); + $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() ); + $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() ); + } + + function testGetRel() { + $this->assertEquals( 'Test!', $this->file_hl0->getRel() ); + $this->assertEquals( 'a/a2/Test!', $this->file_hl2->getRel() ); + $this->assertEquals( 'c/c4/test!', $this->file_lc->getRel() ); + } + + function testGetUrlRel() { + $this->assertEquals( 'Test%21', $this->file_hl0->getUrlRel() ); + $this->assertEquals( 'a/a2/Test%21', $this->file_hl2->getUrlRel() ); + $this->assertEquals( 'c/c4/test%21', $this->file_lc->getUrlRel() ); + } + + function testGetArchivePath() { + $this->assertEquals( '/testdir/archive', $this->file_hl0->getArchivePath() ); + $this->assertEquals( '/testdir/archive/a/a2', $this->file_hl2->getArchivePath() ); + $this->assertEquals( '/testdir/archive/!', $this->file_hl0->getArchivePath( '!' ) ); + $this->assertEquals( '/testdir/archive/a/a2/!', $this->file_hl2->getArchivePath( '!' ) ); + } + + function testGetThumbPath() { + $this->assertEquals( '/testdir/thumb/Test!', $this->file_hl0->getThumbPath() ); + $this->assertEquals( '/testdir/thumb/a/a2/Test!', $this->file_hl2->getThumbPath() ); + $this->assertEquals( '/testdir/thumb/Test!/x', $this->file_hl0->getThumbPath( 'x' ) ); + $this->assertEquals( '/testdir/thumb/a/a2/Test!/x', $this->file_hl2->getThumbPath( 'x' ) ); + } + + function testGetArchiveUrl() { + $this->assertEquals( '/testurl/archive', $this->file_hl0->getArchiveUrl() ); + $this->assertEquals( '/testurl/archive/a/a2', $this->file_hl2->getArchiveUrl() ); + $this->assertEquals( '/testurl/archive/%21', $this->file_hl0->getArchiveUrl( '!' ) ); + $this->assertEquals( '/testurl/archive/a/a2/%21', $this->file_hl2->getArchiveUrl( '!' ) ); + } + + function testGetThumbUrl() { + $this->assertEquals( '/testurl/thumb/Test%21', $this->file_hl0->getThumbUrl() ); + $this->assertEquals( '/testurl/thumb/a/a2/Test%21', $this->file_hl2->getThumbUrl() ); + $this->assertEquals( '/testurl/thumb/Test%21/x', $this->file_hl0->getThumbUrl( 'x' ) ); + $this->assertEquals( '/testurl/thumb/a/a2/Test%21/x', $this->file_hl2->getThumbUrl( 'x' ) ); + } + + function testGetArchiveVirtualUrl() { + $this->assertEquals( 'mwrepo://test/public/archive', $this->file_hl0->getArchiveVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) ); + } + + function testGetThumbVirtualUrl() { + $this->assertEquals( 'mwrepo://test/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) ); + } + + function testGetUrl() { + $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() ); + $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() ); + } + + function testWfLocalFile() { + $file = wfLocalFile( "File:Some_file_that_probably_doesn't exist.png" ); + $this->assertThat( $file, $this->isInstanceOf( 'LocalFile' ), 'wfLocalFile() returns LocalFile for valid Titles' ); + } +} + + diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php new file mode 100644 index 0000000000..22d736369b --- /dev/null +++ b/tests/phpunit/includes/MessageTest.php @@ -0,0 +1,50 @@ +assertTrue( wfMessage( 'mainpage' )->exists() ); + $this->assertTrue( wfMessage( 'mainpage' )->params( array() )->exists() ); + $this->assertTrue( wfMessage( 'mainpage' )->rawParams( 'foo', 123 )->exists() ); + $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->exists() ); + $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->params( array() )->exists() ); + $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->rawParams( 'foo', 123 )->exists() ); + } + + function testKey() { + $this->assertType( 'Message', wfMessage( 'mainpage' ) ); + $this->assertType( 'Message', wfMessage( 'i-dont-exist-evar' ) ); + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->text() ); + } + + function testInLanguage() { + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( 'en' )->text() ); + $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( 'ru' )->text() ); + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'en' ) )->text() ); + $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'ru' ) )->text() ); + } + + function testMessagePararms() { + $this->assertEquals( 'Return to $1.', wfMessage( 'returnto' )->text() ); + $this->assertEquals( 'Return to $1.', wfMessage( 'returnto', array() )->text() ); + $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', 'foo', 'bar' )->text() ); + $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', array( 'foo', 'bar' ) )->text() ); + } + + /** + * @expectedException MWException + */ + function testInLanguageThrows() { + wfMessage( 'foo' )->inLanguage( 123 ); + } +} diff --git a/tests/phpunit/includes/ParserOptionsTest.php b/tests/phpunit/includes/ParserOptionsTest.php new file mode 100644 index 0000000000..63d98e7c93 --- /dev/null +++ b/tests/phpunit/includes/ParserOptionsTest.php @@ -0,0 +1,36 @@ +popts = new ParserOptions( $wgUser ); + $this->pcache = ParserCache::singleton(); + } + + function tearDown() { + parent::tearDown(); + } + + /** + * ParserOptions::optionsHash was not giving consistent results when $wgUseDynamicDates was set + * @group Database + */ + function testGetParserCacheKeyWithDynamicDates() { + global $wgUseDynamicDates; + $wgUseDynamicDates = true; + + $title = Title::newFromText( "Some test article" ); + $article = new Article( $title ); + + $pcacheKeyBefore = $this->pcache->getKey( $article, $this->popts ); + $this->assertNotNull( $this->popts->getDateFormat() ); + $pcacheKeyAfter = $this->pcache->getKey( $article, $this->popts ); + $this->assertEquals( $pcacheKeyBefore, $pcacheKeyAfter ); + } +} diff --git a/tests/phpunit/includes/ResourceLoaderFileModuleTest.php b/tests/phpunit/includes/ResourceLoaderFileModuleTest.php new file mode 100644 index 0000000000..5ad7d9373d --- /dev/null +++ b/tests/phpunit/includes/ResourceLoaderFileModuleTest.php @@ -0,0 +1,15 @@ +assertTrue( self::$resourceLoaderRegisterModulesHook ); + return $resourceLoader; + } + + /** + * @dataProvider provideValidModules + * @depends testCreatingNewResourceLoaderCallsRegistrationHook + * @covers ResourceLoader::register + * @covers ResourceLoader::getModule + */ + public function testRegisteredValidModulesAreAccessible( + $name, ResourceLoaderModule $module, ResourceLoader $resourceLoader + ) { + $resourceLoader->register( $name, $module ); + $this->assertEquals( $module, $resourceLoader->getModule( $name ) ); + } + + /** + * Allthough ResourceLoader::register uses type hinting to prevent arbitrary information being passed through as a + * ResourceLoaderModule object, null can still get through. + * + * @depends testCreatingNewResourceLoaderCallsRegistrationHook + * @covers ResourceLoader::register + * @covers ResourceLoader::getModule + * @expectedException MWException + */ + public function testRegisteringNullModuleThrowsAnException( ResourceLoader $resourceLoader ) { + $this->markTestIncomplete( "Broken by r77011" ); + $resourceLoader->register( 'TEST.nullModule', null ); + } +} + +/* Stubs */ + +class ResourceLoaderTestModule extends ResourceLoaderModule { } + +/* Hooks */ +global $wgHooks; +$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules'; \ No newline at end of file diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php new file mode 100644 index 0000000000..a9405b6ef7 --- /dev/null +++ b/tests/phpunit/includes/RevisionTest.php @@ -0,0 +1,114 @@ + false, + 'wgCompressRevisions' => false, + 'wgInputEncoding' => 'utf-8', + 'wgOutputEncoding' => 'utf-8' ); + foreach ( $globalSet as $var => $data ) { + $this->saveGlobals[$var] = $GLOBALS[$var]; + $GLOBALS[$var] = $data; + } + } + + function tearDown() { + foreach ( $this->saveGlobals as $var => $data ) { + $GLOBALS[$var] = $data; + } + } + + function testGetRevisionText() { + $row = new stdClass; + $row->old_flags = ''; + $row->old_text = 'This is a bunch of revision text.'; + $this->assertEquals( + 'This is a bunch of revision text.', + Revision::getRevisionText( $row ) ); + } + + function testGetRevisionTextGzip() { + $row = new stdClass; + $row->old_flags = 'gzip'; + $row->old_text = gzdeflate( 'This is a bunch of revision text.' ); + $this->assertEquals( + 'This is a bunch of revision text.', + Revision::getRevisionText( $row ) ); + } + + function testGetRevisionTextUtf8Native() { + $row = new stdClass; + $row->old_flags = 'utf-8'; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + function testGetRevisionTextUtf8Legacy() { + $row = new stdClass; + $row->old_flags = ''; + $row->old_text = "Wiki est l'\xe9cole superieur !"; + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + function testGetRevisionTextUtf8NativeGzip() { + $row = new stdClass; + $row->old_flags = 'gzip,utf-8'; + $row->old_text = gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ); + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + function testGetRevisionTextUtf8LegacyGzip() { + $row = new stdClass; + $row->old_flags = 'gzip'; + $row->old_text = gzdeflate( "Wiki est l'\xe9cole superieur !" ); + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + function testCompressRevisionTextUtf8() { + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = Revision::compressRevisionText( $row->old_text ); + $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), + "Flags should contain 'utf-8'" ); + $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ), + "Flags should not contain 'gzip'" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + $row->old_text, "Direct check" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ), "getRevisionText" ); + } + + function testCompressRevisionTextUtf8Gzip() { + $GLOBALS['wgCompressRevisions'] = true; + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = Revision::compressRevisionText( $row->old_text ); + $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), + "Flags should contain 'utf-8'" ); + $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ), + "Flags should contain 'gzip'" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + gzinflate( $row->old_text ), "Direct check" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ), "getRevisionText" ); + } +} + + diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php new file mode 100644 index 0000000000..117ac1ca49 --- /dev/null +++ b/tests/phpunit/includes/SampleTest.php @@ -0,0 +1,95 @@ +assertEquals("Text", $title->__toString(), "Title creation"); + $this->assertEquals("Text", "Text", "Automatic string conversion"); + + $title = Title::newFromText("text", NS_MEDIA); + $this->assertEquals("Media:Text", $title->__toString(), "Title creation with namespace"); + + } + + /** + * If you want to run a the same test with a variety of data. use a data provider. + * see: http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html + */ + public function provideTitles() { + return array( + array( 'Text', NS_MEDIA, 'Media:Text' ), + array( 'Text', null, 'Text' ), + array( 'text', null, 'Text' ), + array( 'Text', NS_USER, 'User:Text' ), + array( 'Photo.jpg', NS_IMAGE, 'File:Photo.jpg' ) + ); + } + + /** + * @dataProvider provideTitles + * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.dataProvider + */ + public function testCreateBasicListOfTitles($titleName, $ns, $text) { + $title = Title::newFromText($titleName, $ns); + $this->assertEquals($text, "$title", "see if '$titleName' matches '$text'"); + } + + public function testSetUpMainPageTitleForNextTest() { + $title = Title::newMainPage(); + $this->assertEquals("Main Page", "$title", "Test initial creation of a title"); + + return $title; + } + + /** + * Instead of putting a bunch of tests in a single test method, + * you should put only one or two tests in each test method. This + * way, the test method names can remain descriptive. + * + * If you want to make tests depend on data created in another + * method, you can create dependencies feed whatever you return + * from the dependant method (e.g. testInitialCreation in this + * example) as arguments to the next method (e.g. $title in + * testTitleDepends is whatever testInitialCreatiion returned.) + */ + /** + * @depends testSetUpMainPageTitleForNextTest + * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.depends + */ + public function testCheckMainPageTitleIsConsideredLocal( $title ) { + $this->assertTrue( $title->isLocal() ); + } + + /** + * @expectedException MWException object + * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.expectedException + */ + function testTitleObjectFromObject() { + $title = Title::newFromText( new Title( "test" ) ); + $this->assertEquals( "Test", $title->isLocal() ); + } +} + diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php new file mode 100644 index 0000000000..a12f9013f0 --- /dev/null +++ b/tests/phpunit/includes/SanitizerTest.php @@ -0,0 +1,72 @@ +assertEquals( + "\xc3\xa9cole", + Sanitizer::decodeCharReferences( 'école' ), + 'decode named entities' + ); + } + + function testDecodeNumericEntities() { + $this->assertEquals( + "\xc4\x88io bonas dans l'\xc3\xa9cole!", + Sanitizer::decodeCharReferences( "Ĉio bonas dans l'école!" ), + 'decode numeric entities' + ); + } + + function testDecodeMixedEntities() { + $this->assertEquals( + "\xc4\x88io bonas dans l'\xc3\xa9cole!", + Sanitizer::decodeCharReferences( "Ĉio bonas dans l'école!" ), + 'decode mixed numeric/named entities' + ); + } + + function testDecodeMixedComplexEntities() { + $this->assertEquals( + "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas Ĉio dans l'école)", + Sanitizer::decodeCharReferences( + "Ĉio bonas dans l'école! (mais pas &#x108;io dans l'&eacute;cole)" + ), + 'decode mixed complex entities' + ); + } + + function testInvalidAmpersand() { + $this->assertEquals( + 'a & b', + Sanitizer::decodeCharReferences( 'a & b' ), + 'Invalid ampersand' + ); + } + + function testInvalidEntities() { + $this->assertEquals( + '&foo;', + Sanitizer::decodeCharReferences( '&foo;' ), + 'Invalid named entity' + ); + } + + function testInvalidNumberedEntities() { + $this->assertEquals( UTF8_REPLACEMENT, Sanitizer::decodeCharReferences( "�" ), 'Invalid numbered entity' ); + } + + function testSelfClosingTag() { + $GLOBALS['wgUseTidy'] = false; + $this->assertEquals( + '
    Hello world
    ', + Sanitizer::removeHTMLtags( '
    Hello world
    ' ), + 'Self-closing closing div' + ); + } +} + diff --git a/tests/phpunit/includes/SeleniumConfigurationTest.php b/tests/phpunit/includes/SeleniumConfigurationTest.php new file mode 100644 index 0000000000..16a916ed6c --- /dev/null +++ b/tests/phpunit/includes/SeleniumConfigurationTest.php @@ -0,0 +1,228 @@ + '*firefox', + 'iexplorer' => '*iexploreproxy', + 'chrome' => '*chrome' + ); + /* + * Array of expected selenium settings from $testConfig0 + */ + private $testSettings0 = array( + 'host' => 'localhost', + 'port' => 'foobarr', + 'wikiUrl' => 'http://localhost/deployment', + 'username' => 'xxxxxxx', + 'userPassword' => '', + 'testBrowser' => 'chrome', + 'startserver' => null, + 'stopserver' => null, + 'seleniumserverexecpath' => null, + 'jUnitLogFile' => null, + 'runAgainstGrid' => null + ); + /* + * Array of expected testSuites from $testConfig0 + */ + private $testSuites0 = array( + 'SimpleSeleniumTestSuite' => 'maintenance/tests/selenium/SimpleSeleniumTestSuite.php', + 'TestSuiteName' => 'testSuitePath' + ); + + + /* + * Another sample selenium settings file contents + */ + private $testConfig1 = +' +[SeleniumSettings] +host = "localhost" +testBrowser = "firefox" +'; + /* + * Expected browsers from $testConfig1 + */ + private $testBrowsers1 = null; + /* + * Expected selenium settings from $testConfig1 + */ + private $testSettings1 = array( + 'host' => 'localhost', + 'port' => null, + 'wikiUrl' => null, + 'username' => null, + 'userPassword' => null, + 'testBrowser' => 'firefox', + 'startserver' => null, + 'stopserver' => null, + 'seleniumserverexecpath' => null, + 'jUnitLogFile' => null, + 'runAgainstGrid' => null + ); + /* + * Expected test suites from $testConfig1 + */ + private $testSuites1 = null; + + + public function setUp() { + if ( !defined( 'SELENIUMTEST' ) ) { + define( 'SELENIUMTEST', true ); + } + } + + /* + * Clean up the temporary file used to store the selenium settings. + */ + public function tearDown() { + if ( strlen( $this->tempFileName ) > 0 ) { + unlink( $this->tempFileName ); + unset( $this->tempFileName ); + } + parent::tearDown(); + } + + /** + * @expectedException MWException + * @group SeleniumFramework + */ + public function testErrorOnIncorrectConfigFile() { + $seleniumSettings; + $seleniumBrowsers; + $seleniumTestSuites; + + SeleniumConfig::getSeleniumSettings($seleniumSettings, + $seleniumBrowsers, + $seleniumTestSuites, + "Some_fake_settings_file.ini" ); + + } + + /** + * @expectedException MWException + * @group SeleniumFramework + */ + public function testErrorOnMissingConfigFile() { + $seleniumSettings; + $seleniumBrowsers; + $seleniumTestSuites; + global $wgSeleniumConfigFile; + $wgSeleniumConfigFile = ''; + SeleniumConfig::getSeleniumSettings($seleniumSettings, + $seleniumBrowsers, + $seleniumTestSuites); + } + + /** + * @group SeleniumFramework + */ + public function testUsesGlobalVarForConfigFile() { + $seleniumSettings; + $seleniumBrowsers; + $seleniumTestSuites; + global $wgSeleniumConfigFile; + $this->writeToTempFile( $this->testConfig0 ); + $wgSeleniumConfigFile = $this->tempFileName; + SeleniumConfig::getSeleniumSettings($seleniumSettings, + $seleniumBrowsers, + $seleniumTestSuites); + $this->assertEquals($seleniumSettings, $this->testSettings0 , + 'The selenium settings should have been read from the file defined in $wgSeleniumConfigFile' + ); + $this->assertEquals($seleniumBrowsers, $this->testBrowsers0, + 'The available browsers should have been read from the file defined in $wgSeleniumConfigFile' + ); + $this->assertEquals($seleniumTestSuites, $this->testSuites0, + 'The test suites should have been read from the file defined in $wgSeleniumConfigFile' + ); + } + + /** + * @group SeleniumFramework + * @dataProvider sampleConfigs + */ + public function testgetSeleniumSettings($sampleConfig, $expectedSettings, $expectedBrowsers, $expectedSuites ) { + $this->writeToTempFile( $sampleConfig ); + $seleniumSettings; + $seleniumBrowsers; + $seleniumTestSuites; + + SeleniumConfig::getSeleniumSettings($seleniumSettings, + $seleniumBrowsers, + $seleniumTestSuites, + $this->tempFileName ); + + $this->assertEquals($seleniumSettings, $expectedSettings, + "The selenium settings for the following test configuration was not retrieved correctly" . $sampleConfig + ); + $this->assertEquals($seleniumBrowsers, $expectedBrowsers, + "The available browsers for the following test configuration was not retrieved correctly" . $sampleConfig + ); + $this->assertEquals($seleniumTestSuites, $expectedSuites, + "The test suites for the following test configuration was not retrieved correctly" . $sampleConfig + ); + + + } + + /* + * create a temp file and write text to it. + * @param $testToWrite the text to write to the temp file + */ + private function writeToTempFile($textToWrite) { + $this->tempFileName = tempnam(sys_get_temp_dir(), 'test_settings.'); + $tempFile = fopen( $this->tempFileName, "w" ); + fwrite($tempFile , $textToWrite); + fclose($tempFile); + } + + /* + * Returns an array containing: + * The contents of the selenium cingiguration ini file + * The expected selenium configuration array that getSeleniumSettings should return + * The expected available browsers array that getSeleniumSettings should return + * The expected test suites arrya that getSeleniumSettings should return + */ + public function sampleConfigs() { + return array( + array($this->testConfig0, $this->testSettings0, $this->testBrowsers0, $this->testSuites0 ), + array($this->testConfig1, $this->testSettings1, $this->testBrowsers1, $this->testSuites1 ) + ); + } + + +} diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php new file mode 100644 index 0000000000..9bfab9ede2 --- /dev/null +++ b/tests/phpunit/includes/SiteConfigurationTest.php @@ -0,0 +1,311 @@ +suffixes as $suffix ) { + if ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) { + $site = $suffix; + $lang = substr( $wiki, 0, -strlen( $suffix ) ); + break; + } + } + return array( + 'suffix' => $site, + 'lang' => $lang, + 'params' => array( + 'lang' => $lang, + 'site' => $site, + 'wiki' => $wiki, + ), + 'tags' => array( 'tag' ), + ); +} + +class SiteConfigurationTest extends PHPUnit_Framework_TestCase { + var $mConf; + + function setUp() { + $this->mConf = new SiteConfiguration; + + $this->mConf->suffixes = array( 'wiki' ); + $this->mConf->wikis = array( 'enwiki', 'dewiki', 'frwiki' ); + $this->mConf->settings = array( + 'simple' => array( + 'wiki' => 'wiki', + 'tag' => 'tag', + 'enwiki' => 'enwiki', + 'dewiki' => 'dewiki', + 'frwiki' => 'frwiki', + ), + + 'fallback' => array( + 'default' => 'default', + 'wiki' => 'wiki', + 'tag' => 'tag', + ), + + 'params' => array( + 'default' => '$lang $site $wiki', + ), + + '+global' => array( + 'wiki' => array( + 'wiki' => 'wiki', + ), + 'tag' => array( + 'tag' => 'tag', + ), + 'enwiki' => array( + 'enwiki' => 'enwiki', + ), + 'dewiki' => array( + 'dewiki' => 'dewiki', + ), + 'frwiki' => array( + 'frwiki' => 'frwiki', + ), + ), + + 'merge' => array( + '+wiki' => array( + 'wiki' => 'wiki', + ), + '+tag' => array( + 'tag' => 'tag', + ), + 'default' => array( + 'default' => 'default', + ), + '+enwiki' => array( + 'enwiki' => 'enwiki', + ), + '+dewiki' => array( + 'dewiki' => 'dewiki', + ), + '+frwiki' => array( + 'frwiki' => 'frwiki', + ), + ), + ); + + $GLOBALS['global'] = array( 'global' => 'global' ); + } + + + function testSiteFromDB() { + $this->assertEquals( + array( 'wikipedia', 'en' ), + $this->mConf->siteFromDB( 'enwiki' ), + 'siteFromDB()' + ); + $this->assertEquals( + array( 'wikipedia', '' ), + $this->mConf->siteFromDB( 'wiki' ), + 'siteFromDB() on a suffix' + ); + $this->assertEquals( + array( null, null ), + $this->mConf->siteFromDB( 'wikien' ), + 'siteFromDB() on a non-existing wiki' + ); + + $this->mConf->suffixes = array( 'wiki', '' ); + $this->assertEquals( + array( '', 'wikien' ), + $this->mConf->siteFromDB( 'wikien' ), + 'siteFromDB() on a non-existing wiki (2)' + ); + } + + function testGetLocalDatabases() { + $this->assertEquals( + array( 'enwiki', 'dewiki', 'frwiki' ), + $this->mConf->getLocalDatabases(), + 'getLocalDatabases()' + ); + } + + function testGet() { + $this->assertEquals( + 'enwiki', + $this->mConf->get( 'simple', 'enwiki', 'wiki' ), + 'get(): simple setting on an existing wiki' + ); + $this->assertEquals( + 'dewiki', + $this->mConf->get( 'simple', 'dewiki', 'wiki' ), + 'get(): simple setting on an existing wiki (2)' + ); + $this->assertEquals( + 'frwiki', + $this->mConf->get( 'simple', 'frwiki', 'wiki' ), + 'get(): simple setting on an existing wiki (3)' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'simple', 'wiki', 'wiki' ), + 'get(): simple setting on an suffix' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'simple', 'eswiki', 'wiki' ), + 'get(): simple setting on an non-existing wiki' + ); + + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'enwiki', 'wiki' ), + 'get(): fallback setting on an existing wiki' + ); + $this->assertEquals( + 'tag', + $this->mConf->get( 'fallback', 'dewiki', 'wiki', array(), array( 'tag' ) ), + 'get(): fallback setting on an existing wiki (with wiki tag)' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'wiki', 'wiki' ), + 'get(): fallback setting on an suffix' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'wiki', 'wiki', array(), array( 'tag' ) ), + 'get(): fallback setting on an suffix (with wiki tag)' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'eswiki', 'wiki' ), + 'get(): fallback setting on an non-existing wiki' + ); + $this->assertEquals( + 'tag', + $this->mConf->get( 'fallback', 'eswiki', 'wiki', array(), array( 'tag' ) ), + 'get(): fallback setting on an non-existing wiki (with wiki tag)' + ); + + $common = array( 'wiki' => 'wiki', 'default' => 'default' ); + $commonTag = array( 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ); + $this->assertEquals( + array( 'enwiki' => 'enwiki' ) + $common, + $this->mConf->get( 'merge', 'enwiki', 'wiki' ), + 'get(): merging setting on an existing wiki' + ); + $this->assertEquals( + array( 'enwiki' => 'enwiki' ) + $commonTag, + $this->mConf->get( 'merge', 'enwiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an existing wiki (with tag)' + ); + $this->assertEquals( + array( 'dewiki' => 'dewiki' ) + $common, + $this->mConf->get( 'merge', 'dewiki', 'wiki' ), + 'get(): merging setting on an existing wiki (2)' + ); + $this->assertEquals( + array( 'dewiki' => 'dewiki' ) + $commonTag, + $this->mConf->get( 'merge', 'dewiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an existing wiki (2) (with tag)' + ); + $this->assertEquals( + array( 'frwiki' => 'frwiki' ) + $common, + $this->mConf->get( 'merge', 'frwiki', 'wiki' ), + 'get(): merging setting on an existing wiki (3)' + ); + $this->assertEquals( + array( 'frwiki' => 'frwiki' ) + $commonTag, + $this->mConf->get( 'merge', 'frwiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an existing wiki (3) (with tag)' + ); + $this->assertEquals( + array( 'wiki' => 'wiki' ) + $common, + $this->mConf->get( 'merge', 'wiki', 'wiki' ), + 'get(): merging setting on an suffix' + ); + $this->assertEquals( + array( 'wiki' => 'wiki' ) + $commonTag, + $this->mConf->get( 'merge', 'wiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an suffix (with tag)' + ); + $this->assertEquals( + $common, + $this->mConf->get( 'merge', 'eswiki', 'wiki' ), + 'get(): merging setting on an non-existing wiki' + ); + $this->assertEquals( + $commonTag, + $this->mConf->get( 'merge', 'eswiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an non-existing wiki (with tag)' + ); + } + + function testSiteFromDBWithCallback() { + $this->mConf->siteParamsCallback = 'getSiteParams'; + + $this->assertEquals( + array( 'wiki', 'en' ), + $this->mConf->siteFromDB( 'enwiki' ), + 'siteFromDB() with callback' + ); + $this->assertEquals( + array( 'wiki', '' ), + $this->mConf->siteFromDB( 'wiki' ), + 'siteFromDB() with callback on a suffix' + ); + $this->assertEquals( + array( null, null ), + $this->mConf->siteFromDB( 'wikien' ), + 'siteFromDB() with callback on a non-existing wiki' + ); + } + + function testParamReplacement() { + $this->mConf->siteParamsCallback = 'getSiteParams'; + + $this->assertEquals( + 'en wiki enwiki', + $this->mConf->get( 'params', 'enwiki', 'wiki' ), + 'get(): parameter replacement on an existing wiki' + ); + $this->assertEquals( + 'de wiki dewiki', + $this->mConf->get( 'params', 'dewiki', 'wiki' ), + 'get(): parameter replacement on an existing wiki (2)' + ); + $this->assertEquals( + 'fr wiki frwiki', + $this->mConf->get( 'params', 'frwiki', 'wiki' ), + 'get(): parameter replacement on an existing wiki (3)' + ); + $this->assertEquals( + ' wiki wiki', + $this->mConf->get( 'params', 'wiki', 'wiki' ), + 'get(): parameter replacement on an suffix' + ); + $this->assertEquals( + 'es wiki eswiki', + $this->mConf->get( 'params', 'eswiki', 'wiki' ), + 'get(): parameter replacement on an non-existing wiki' + ); + } + + function testGetAll() { + $this->mConf->siteParamsCallback = 'getSiteParams'; + + $getall = array( + 'simple' => 'enwiki', + 'fallback' => 'tag', + 'params' => 'en wiki enwiki', + 'global' => array( 'enwiki' => 'enwiki' ) + $GLOBALS['global'], + 'merge' => array( 'enwiki' => 'enwiki', 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ), + ); + $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' ); + + $this->mConf->extractAllGlobals( 'enwiki', 'wiki' ); + + $this->assertEquals( $getall['simple'], $GLOBALS['simple'], 'extractAllGlobals(): simple setting' ); + $this->assertEquals( $getall['fallback'], $GLOBALS['fallback'], 'extractAllGlobals(): fallback setting' ); + $this->assertEquals( $getall['params'], $GLOBALS['params'], 'extractAllGlobals(): parameter replacement' ); + $this->assertEquals( $getall['global'], $GLOBALS['global'], 'extractAllGlobals(): merging with global' ); + $this->assertEquals( $getall['merge'], $GLOBALS['merge'], 'extractAllGlobals(): merging setting' ); + } +} diff --git a/tests/phpunit/includes/TimeAdjustTest.php b/tests/phpunit/includes/TimeAdjustTest.php new file mode 100644 index 0000000000..820e7130fd --- /dev/null +++ b/tests/phpunit/includes/TimeAdjustTest.php @@ -0,0 +1,49 @@ +iniSet( 'precision', 15 ); + } + + public function tearDown() { + global $wgLocalTZoffset; + $wgLocalTZoffset = self::$offset; + } + + # Test offset usage for a given language::userAdjust + function testUserAdjust() { + global $wgLocalTZoffset, $wgContLang; + + $wgContLang = $en = Language::factory( 'en' ); + + #  Collection of parameters for Language_t_Offset. + # Format: date to be formatted, localTZoffset value, expected date + $userAdjust_tests = array( + array( 20061231235959, 0, 20061231235959 ), + array( 20061231235959, 5, 20070101000459 ), + array( 20061231235959, 15, 20070101001459 ), + array( 20061231235959, 60, 20070101005959 ), + array( 20061231235959, 90, 20070101012959 ), + array( 20061231235959, 120, 20070101015959 ), + array( 20061231235959, 540, 20070101085959 ), + array( 20061231235959, -5, 20061231235459 ), + array( 20061231235959, -30, 20061231232959 ), + array( 20061231235959, -60, 20061231225959 ), + ); + + foreach ( $userAdjust_tests as $data ) { + $wgLocalTZoffset = $data[1]; + + $this->assertEquals( + strval( $data[2] ), + strval( $en->userAdjust( $data[0], '' ) ), + "User adjust {$data[0]} by {$data[1]} minutes should give {$data[2]}" + ); + } + } +} diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php new file mode 100644 index 0000000000..fa83b9a669 --- /dev/null +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -0,0 +1,652 @@ +getID() ) { + self::$userUser = User::createNew( self::$userName, array( + "email" => "test@example.com", + "real_name" => "Test User" ) ); + } + + self::$altUser = User::newFromName( self::$altUserName ); + if ( !self::$altUser->getID() ) { + self::$altUser = User::createNew( self::$altUserName, array( + "email" => "alttest@example.com", + "real_name" => "Test User Alt" ) ); + } + + self::$anonUser = User::newFromId( 0 ); + + self::$user = self::$userUser; + } + } + + function setUserPerm( $perm ) { + if ( is_array( $perm ) ) { + self::$user->mRights = $perm; + } else { + self::$user->mRights = array( $perm ); + } + } + + function setTitle( $ns, $title = "Main_Page" ) { + self::$title = Title::makeTitle( $ns, $title ); + } + + function setUser( $userName = null ) { + if ( $userName === 'anon' ) { + self::$user = self::$anonUser; + } else if ( $userName === null || $userName === self::$userName ) { + self::$user = self::$userUser; + } else { + self::$user = self::$altUser; + } + + global $wgUser; + $wgUser = self::$user; + } + + function testQuickPermissions() { + global $wgContLang; + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( "nocreatetext" ) ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( 'nocreatetext' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( 'nocreatetext' ) ), $res ); + + $this->setUser( self::$userName ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'create', self::$user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_USER, self::$userName . '' ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '' ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, self::$userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setUser( self::$userName ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenotallowed' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = self::$title->getUserPermissionsErrors( 'move', self::$user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setUser( self::$userName ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ) ); + + $this->setUser( 'anon' ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), + array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ) ); + + $this->setTitle( NS_MAIN ); + $this->setUser( 'anon' ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( ) ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ), + array( array( 'movenologintext' ) ) ); + + $this->setUser( self::$userName ); + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ) ); + + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( ) ); + + $this->setUser( 'anon' ); + $this->setUserPerm( 'move' ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setUserPerm( '' ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( array( 'movenotallowed' ) ), $res ); + + $this->setTitle( NS_USER ); + $this->setUser( self::$userName ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setUserPerm( "move" ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( array( 'cant-move-to-user-page' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_USER, "User/subpage" ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setUserPerm( "move" ); + $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user ); + $this->assertEquals( array( ), $res ); + + $this->setUser( 'anon' ); + $check = array( 'edit' => array( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ) ), + array( array( 'badaccess-group0' ) ), + array( ), true ), + 'protect' => array( array( array( 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ), array( 'protect-cantedit' ) ), + array( array( 'badaccess-group0' ), array( 'protect-cantedit' ) ), + array( array( 'protect-cantedit' ) ), false ), + '' => array( array( ), array( ), array( ), true ) ); + global $wgUser; + $wgUser = self::$user; + foreach ( array( "edit", "protect", "" ) as $action ) { + $this->setUserPerm( null ); + $this->assertEquals( $check[$action][0], + self::$title->getUserPermissionsErrors( $action, self::$user, true ) ); + + global $wgGroupPermissions; + $old = $wgGroupPermissions; + $wgGroupPermissions = array(); + + $this->assertEquals( $check[$action][1], + self::$title->getUserPermissionsErrors( $action, self::$user, true ) ); + $wgGroupPermissions = $old; + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][2], + self::$title->getUserPermissionsErrors( $action, self::$user, true ) ); + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][3], + self::$title->userCan( $action, true ) ); + $this->assertEquals( $check[$action][3], + self::$title->quickUserCan( $action, false ) ); + + # count( User::getGroupsWithPermissions( $action ) ) < 1 + } + } + + function runGroupPermissions( $action, $result, $result2 = null ) { + global $wgGroupPermissions; + + if ( $result2 === null ) $result2 = $result; + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = false; + $res = self::$title->getUserPermissionsErrors( $action, self::$user ); + $this->assertEquals( $result, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = false; + $res = self::$title->getUserPermissionsErrors( $action, self::$user ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = true; + $res = self::$title->getUserPermissionsErrors( $action, self::$user ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = true; + $res = self::$title->getUserPermissionsErrors( $action, self::$user ); + $this->assertEquals( $result2, $res ); + } + + function testPermissionHooks() { } + function testSpecialsAndNSPermissions() { + $this->setUser( self::$userName ); + global $wgUser, $wgContLang; + $wgUser = self::$user; + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $this->setTitle( NS_SPECIAL ); + + $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Administrators|Administrators]]", 2 ) ), + self::$title->getUserPermissionsErrors( 'createaccount', self::$user ) ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + self::$title->getUserPermissionsErrors( 'execute', self::$user ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + + global $wgNamespaceProtection; + $wgNamespaceProtection[NS_USER] = array ( 'bogus' ); + $this->setTitle( NS_USER ); + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User' ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( array( 'protectedinterface' ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( array( 'protectedinterface' ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + + $wgNamespaceProtection = null; + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + $this->assertEquals( true, + self::$title->userCan( 'bogus' ) ); + + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + $this->assertEquals( false, + self::$title->userCan( 'bogus' ) ); + } + + function testCSSandJSPermissions() { + $this->setUser( self::$userName ); + global $wgUser; + $wgUser = self::$user; + + $this->setTitle( NS_USER, self::$altUserName . '/test.js' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ), + array( array( 'badaccess-group0' ) ) ); + + $this->setTitle( NS_USER, self::$altUserName . '/test.css' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ) ); + + $this->setTitle( NS_USER, self::$altUserName . '/tempo' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ) ) ); + } + + function runCSSandJSPermissions( $result0, $result1, $result2 ) { + $this->setUserPerm( '' ); + $this->assertEquals( $result0, + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + + $this->setUserPerm( 'editusercss' ); + $this->assertEquals( $result1, + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + + $this->setUserPerm( 'edituserjs' ); + $this->assertEquals( $result2, + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + + $this->setUserPerm( 'editusercssjs' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + + $this->setUserPerm( array( 'edituserjs', 'editusercss' ) ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + } + + function testPageRestrictions() { + global $wgUser, $wgContLang; + + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $wgUser = self::$user; + $this->setTitle( NS_MAIN ); + self::$title->mRestrictionsLoaded = true; + $this->setUserPerm( "edit" ); + self::$title->mRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'edit', + self::$user ) ); + + $this->assertEquals( true, + self::$title->quickUserCan( 'edit', false ) ); + self::$title->mRestrictions = array( "edit" => array( 'bogus', "sysop", "protect", "" ), + "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'edit', + self::$user ) ); + $this->setUserPerm( "" ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'edit', + self::$user ) ); + $this->setUserPerm( array( "edit", "editprotected" ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'edit', + self::$user ) ); + self::$title->mCascadeRestriction = true; + $this->assertEquals( false, + self::$title->quickUserCan( 'bogus', false ) ); + $this->assertEquals( false, + self::$title->quickUserCan( 'edit', false ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'bogus', + self::$user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + self::$title->getUserPermissionsErrors( 'edit', + self::$user ) ); + } + + function testCascadingSourcesRestrictions() { + global $wgUser; + $wgUser = self::$user; + $this->setTitle( NS_MAIN, "test page" ); + $this->setUserPerm( array( "edit", "bogus" ) ); + + self::$title->mCascadeSources = array( Title::makeTitle( NS_MAIN, "Bogus" ), Title::makeTitle( NS_MAIN, "UnBogus" ) ); + self::$title->mCascadingRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( false, + self::$title->userCan( 'bogus' ) ); + $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ), + array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ) ), + self::$title->getUserPermissionsErrors( 'bogus', self::$user ) ); + + $this->assertEquals( true, + self::$title->userCan( 'edit' ) ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'edit', self::$user ) ); + + } + + function testActionPermissions() { + global $wgUser; + $wgUser = self::$user; + + $this->setUserPerm( array( "createpage" ) ); + $this->setTitle( NS_MAIN, "test page" ); + self::$title->mTitleProtection['pt_create_perm'] = ''; + self::$title->mTitleProtection['pt_user'] = self::$user->getID(); + self::$title->mTitleProtection['pt_expiry'] = Block::infinity(); + self::$title->mTitleProtection['pt_reason'] = 'test'; + self::$title->mCascadeRestriction = false; + + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + self::$title->getUserPermissionsErrors( 'create', self::$user ) ); + $this->assertEquals( false, + self::$title->userCan( 'create' ) ); + + self::$title->mTitleProtection['pt_create_perm'] = 'sysop'; + $this->setUserPerm( array( 'createpage', 'protect' ) ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'create', self::$user ) ); + $this->assertEquals( true, + self::$title->userCan( 'create' ) ); + + + $this->setUserPerm( array( 'createpage' ) ); + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + self::$title->getUserPermissionsErrors( 'create', self::$user ) ); + $this->assertEquals( false, + self::$title->userCan( 'create' ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->setUserPerm( array( "move" ) ); + $this->assertEquals( false, + self::$title->userCan( 'move' ) ); + $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ), + self::$title->getUserPermissionsErrors( 'move', self::$user ) ); + + $this->setTitle( NS_MAIN, "test page" ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'move', self::$user ) ); + $this->assertEquals( true, + self::$title->userCan( 'move' ) ); + + self::$title->mInterwiki = "no"; + $this->assertEquals( array( array( 'immobile-page' ) ), + self::$title->getUserPermissionsErrors( 'move', self::$user ) ); + $this->assertEquals( false, + self::$title->userCan( 'move' ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->assertEquals( false, + self::$title->userCan( 'move-target' ) ); + $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ), + self::$title->getUserPermissionsErrors( 'move-target', self::$user ) ); + + $this->setTitle( NS_MAIN, "test page" ); + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'move-target', self::$user ) ); + $this->assertEquals( true, + self::$title->userCan( 'move-target' ) ); + + self::$title->mInterwiki = "no"; + $this->assertEquals( array( array( 'immobile-target-page' ) ), + self::$title->getUserPermissionsErrors( 'move-target', self::$user ) ); + $this->assertEquals( false, + self::$title->userCan( 'move-target' ) ); + + } + + function testUserBlock() { + global $wgUser, $wgEmailConfirmToEdit, $wgEmailAuthentication; + $wgEmailConfirmToEdit = true; + $wgEmailAuthentication = true; + $wgUser = self::$user; + + $this->setUserPerm( array( "createpage", "move" ) ); + $this->setTitle( NS_MAIN, "test page" ); + + # $short + $this->assertEquals( array( array( 'confirmedittext' ) ), + self::$title->getUserPermissionsErrors( 'move-target', self::$user ) ); + $wgEmailConfirmToEdit = false; + $this->assertEquals( true, self::$title->userCan( 'move-target' ) ); + + # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' + $this->assertEquals( array( ), + self::$title->getUserPermissionsErrors( 'move-target', + self::$user ) ); + + global $wgLang; + $prev = time(); + $now = time() + 120; + self::$user->mBlockedby = self::$user->getId(); + self::$user->mBlock = new Block( '127.0.8.1', self::$user->getId(), self::$user->getId(), + 'no reason given', $prev + 3600, 1, 0 ); + self::$user->mBlock->mTimestamp = 0; + $this->assertEquals( array( array( 'autoblockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', 0, 'infinite', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ) ), + self::$title->getUserPermissionsErrors( 'move-target', + self::$user ) ); + + $this->assertEquals( false, + self::$title->userCan( 'move-target', self::$user ) ); + + global $wgLocalTZoffset; + $wgLocalTZoffset = -60; + self::$user->mBlockedby = self::$user->getName(); + self::$user->mBlock = new Block( '127.0.8.1', 2, 1, 'no reason given', $now, 0, 10 ); + $this->assertEquals( array( array( 'blockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', 0, '23:00, 31 December 1969', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ) ), + self::$title->getUserPermissionsErrors( 'move-target', self::$user ) ); + + # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) + # $user->blockedFor() == '' + # $user->mBlock->mExpiry == 'infinity' + } +} diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php new file mode 100644 index 0000000000..5b42c1c575 --- /dev/null +++ b/tests/phpunit/includes/TitleTest.php @@ -0,0 +1,17 @@ +|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) { + $this->assertFalse( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is not a valid titlechar" ); + } else { + $this->assertTrue( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is a valid titlechar" ); + } + } + } +} diff --git a/tests/phpunit/includes/UploadFromUrlTest.php b/tests/phpunit/includes/UploadFromUrlTest.php new file mode 100644 index 0000000000..8382299289 --- /dev/null +++ b/tests/phpunit/includes/UploadFromUrlTest.php @@ -0,0 +1,353 @@ +exists() ) { + $this->deleteFile( 'UploadFromUrlTest.png' ); + } + } + + protected function doApiRequest( $params, $unused = null ) { + $sessionId = session_id(); + session_write_close(); + + $req = new FauxRequest( $params, true, $_SESSION ); + $module = new ApiMain( $req, true ); + $module->execute(); + + wfSetupSession( $sessionId ); + return array( $module->getResultData(), $req ); + } + + /** + * Ensure that the job queue is empty before continuing + */ + public function testClearQueue() { + while ( $job = Job::pop() ) { } + $this->assertFalse( $job ); + } + + /** + * @todo Document why we test login, since the $wgUser hack used doesn't + * require login + */ + public function testLogin() { + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => self::$userName, + 'lgpassword' => self::$passWord ) ); + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "NeedToken", $data[0]['login']['result'] ); + $token = $data[0]['login']['token']; + + $data = $this->doApiRequest( array( + 'action' => 'login', + "lgtoken" => $token, + "lgname" => self::$userName, + "lgpassword" => self::$passWord ) ); + + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "Success", $data[0]['login']['result'] ); + $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] ); + + return $data; + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testSetupUrlDownload( $data ) { + $token = self::$user->editToken(); + $exception = false; + + try { + $this->doApiRequest( array( + 'action' => 'upload', + ) ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "The token parameter must be set", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'token' => $token, + ), $data ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required", + $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'url' => 'http://www.example.com/test.png', + 'token' => $token, + ), $data ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "The filename parameter must be set", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + self::$user->removeGroup( 'sysop' ); + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'url' => 'http://www.example.com/test.png', + 'filename' => 'UploadFromUrlTest.png', + 'token' => $token, + ), $data ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "Permission denied", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + self::$user->addGroup( 'sysop' ); + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'filename' => 'UploadFromUrlTest.png', + 'token' => $token, + ), $data ); + + $this->assertEquals( $data[0]['upload']['result'], 'Queued', 'Queued upload' ); + + $job = Job::pop(); + $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' ); + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testAsyncUpload( $data ) { + $token = self::$user->editToken(); + + self::$user->addGroup( 'users' ); + + $data = $this->doAsyncUpload( $token, true ); + $this->assertEquals( $data[0]['upload']['result'], 'Success' ); + $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' ); + $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() ); + + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testAsyncUploadWarning( $data ) { + $token = self::$user->editToken(); + + self::$user->addGroup( 'users' ); + + + $data = $this->doAsyncUpload( $token ); + + $this->assertEquals( $data[0]['upload']['result'], 'Warning' ); + $this->assertTrue( isset( $data[0]['upload']['sessionkey'] ) ); + + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'sessionkey' => $data[0]['upload']['sessionkey'], + 'filename' => 'UploadFromUrlTest.png', + 'ignorewarnings' => 1, + 'token' => $token, + ) ); + $this->assertEquals( $data[0]['upload']['result'], 'Success' ); + $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' ); + $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() ); + + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testSyncDownload( $data ) { + $token = self::$user->editToken(); + + $job = Job::pop(); + $this->assertFalse( $job, 'Starting with an empty jobqueue' ); + + self::$user->addGroup( 'users' ); + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'ignorewarnings' => true, + 'token' => $token, + ), $data ); + + $job = Job::pop(); + $this->assertFalse( $job ); + + $this->assertEquals( 'Success', $data[0]['upload']['result'] ); + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + public function testLeaveMessage() { + $token = self::$user->editToken(); + + $talk = self::$user->getTalkPage(); + if ( $talk->exists() ) { + $a = new Article( $talk ); + $a->doDeleteArticle( '' ); + } + + $this->assertFalse( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk does not exist' ); + + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'token' => $token, + 'leavemessage' => 1, + 'ignorewarnings' => 1, + ) ); + + $job = Job::pop(); + $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) ); + $job->run(); + + $this->assertTrue( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ); + $this->assertTrue( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk exists' ); + + $this->deleteFile( 'UploadFromUrlTest.png' ); + + $talkRev = Revision::newFromTitle( $talk ); + $talkSize = $talkRev->getSize(); + + $exception = false; + try { + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'token' => $token, + 'leavemessage' => 1, + ) ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( 'Using leavemessage without ignorewarnings is not supported', $e->getMessage() ); + } + $this->assertTrue( $exception ); + + $job = Job::pop(); + $this->assertFalse( $job ); + + return; + + /** + // Broken until using leavemessage with ignorewarnings is supported + $job->run(); + + $this->assertFalse( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ); + + $talkRev = Revision::newFromTitle( $talk ); + $this->assertTrue( $talkRev->getSize() > $talkSize, 'New message left' ); + */ + } + + /** + * Helper function to perform an async upload, execute the job and fetch + * the status + * + * @return array The result of action=upload&statuskey=key + */ + private function doAsyncUpload( $token, $ignoreWarnings = false, $leaveMessage = false ) { + $params = array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'token' => $token, + ); + if ( $ignoreWarnings ) { + $params['ignorewarnings'] = 1; + } + if ( $leaveMessage ) { + $params['leavemessage'] = 1; + } + + $data = $this->doApiRequest( $params ); + $this->assertEquals( $data[0]['upload']['result'], 'Queued' ); + $this->assertTrue( isset( $data[0]['upload']['statuskey'] ) ); + $statusKey = $data[0]['upload']['statuskey']; + + $job = Job::pop(); + $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) ); + + $status = $job->run(); + $this->assertTrue( $status ); + + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'statuskey' => $statusKey, + 'token' => $token, + ) ); + + return $data; + } + + + /** + * + */ + protected function deleteFile( $name ) { + $t = Title::newFromText( $name, NS_FILE ); + $this->assertTrue($t->exists(), "File '$name' exists"); + + if ( $t->exists() ) { + $file = wfFindFile( $name, array( 'ignoreRedirect' => true ) ); + $empty = ""; + FileDeleteForm::doDelete( $t, $file, $empty, "none", true ); + $a = new Article ( $t ); + $a->doDeleteArticle( "testing" ); + } + $t = Title::newFromText( $name, NS_FILE ); + + $this->assertFalse($t->exists(), "File '$name' was deleted"); + } + } diff --git a/tests/phpunit/includes/UploadTest.php b/tests/phpunit/includes/UploadTest.php new file mode 100644 index 0000000000..cf8e4d3cc7 --- /dev/null +++ b/tests/phpunit/includes/UploadTest.php @@ -0,0 +1,90 @@ +upload = new UploadTestHandler; + } + + /** + * Test various forms of valid and invalid titles that can be supplied. + */ + public function testTitleValidation() { + + + /* Test a valid title */ + $this->assertUploadTitleAndCode( 'ValidTitle.jpg', + 'ValidTitle.jpg', UploadTestHandler::OK, + 'upload valid title' ); + + /* A title with a slash */ + $this->assertUploadTitleAndCode( 'A/B.jpg', + 'B.jpg', UploadTestHandler::OK, + 'upload title with slash' ); + + /* A title with illegal char */ + $this->assertUploadTitleAndCode( 'A:B.jpg', + 'A-B.jpg', UploadTestHandler::OK, + 'upload title with colon' ); + + /* A title without extension */ + $this->assertUploadTitleAndCode( 'A', + null, UploadTestHandler::FILETYPE_MISSING, + 'upload title without extension' ); + + /* A title with no basename */ + $this->assertUploadTitleAndCode( '.jpg', + null, UploadTestHandler::MIN_LENGTH_PARTNAME, + 'upload title without basename' ); + + } + /** + * Helper function for testTitleValidation. First checks the return code + * of UploadBase::getTitle() and then the actual returned titl + */ + private function assertUploadTitleAndCode( $srcFilename, $dstFilename, $code, $msg ) { + /* Check the result code */ + $this->assertEquals( $code, + $this->upload->testTitleValidation( $srcFilename ), + "$msg code" ); + + /* If we expect a valid title, check the title itself. */ + if ( $code == UploadTestHandler::OK ) { + $this->assertEquals( $dstFilename, + $this->upload->getTitle()->getText(), + "$msg text" ); + } + } + + /** + * Test the upload verification functions + */ + public function testVerifyUpload() { + /* Setup with zero file size */ + $this->upload->initializePathInfo( '', '', 0 ); + $result = $this->upload->verifyUpload(); + $this->assertEquals( UploadTestHandler::EMPTY_FILE, + $result['status'], + 'upload empty file' ); + } + +} + +class UploadTestHandler extends UploadBase { + public function initializeFromRequest( &$request ) { } + public function testTitleValidation( $name ) { + $this->mTitle = false; + $this->mDesiredDestName = $name; + $this->getTitle(); + return $this->mTitleError; + } + + +} diff --git a/tests/phpunit/includes/UserIsValidEmailAddrTest.php b/tests/phpunit/includes/UserIsValidEmailAddrTest.php new file mode 100644 index 0000000000..c1f34193ed --- /dev/null +++ b/tests/phpunit/includes/UserIsValidEmailAddrTest.php @@ -0,0 +1,64 @@ +assertEquals( + $expected, + User::isValidEmailAddr( $addr ), + $msg + ); + } + private function valid( $addr, $msg = '' ) { + $this->checkEmail( $addr, true, $msg ); + } + private function invalid( $addr, $msg = '' ) { + $this->checkEmail( $addr, false, $msg ); + } + + function testEmailWellKnownUserAtHostDotTldAreValid() { + $this->valid( 'user@example.com' ); + $this->valid( 'user@example.museum' ); + } + function testEmailWithUpperCaseCharactersAreValid() { + $this->valid( 'USER@example.com' ); + $this->valid( 'user@EXAMPLE.COM' ); + $this->valid( 'user@Example.com' ); + $this->valid( 'USER@eXAMPLE.com' ); + } + function testEmailWithAPlusInUserName() { + $this->valid( 'user+sub@example.com' ); + $this->valid( 'user+@example.com' ); + } + function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() { + $this->invalid( " user@host" ); + $this->invalid( "user@host " ); + $this->invalid( "\tuser@host" ); + $this->invalid( "user@host\t" ); + } + function testEmailWithWhiteSpacesAreInvalids() { + $this->invalid( "User user@host" ); + $this->invalid( "first last@mycompany" ); + $this->invalid( "firstlast@my company" ); + } + function testEmailDomainCanNotBeginWithDot() { + $this->invalid( "user@." ); + $this->invalid( "user@.localdomain" ); + $this->invalid( "user@localdomain." ); + $this->invalid( "user.@localdomain" ); + $this->invalid( ".@localdomain" ); + $this->invalid( ".@a............" ); + } + function testEmailWithFunnyCharacters() { + $this->valid( "\$user!ex{this}@123.com" ); + } + function testEmailTopLevelDomainCanBeNumerical() { + $this->valid( "user@example.1234" ); + } + function testEmailWithoutAtSignIsInvalid() { + $this->invalid( 'useràexample.com' ); + } + function testEmailWithOneCharacterDomainIsInvalid() { + $this->invalid( 'user@a' ); + } +} diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php new file mode 100644 index 0000000000..f01c0f1cc4 --- /dev/null +++ b/tests/phpunit/includes/XmlTest.php @@ -0,0 +1,179 @@ +assertNull( Xml::expandAttributes(null), + 'Converting a null list of attributes' + ); + $this->assertEquals( '', Xml::expandAttributes( array() ), + 'Converting an empty list of attributes' + ); + } + + public function testExpandAttributesException() { + $this->setExpectedException('MWException'); + Xml::expandAttributes('string'); + } + + function testElementOpen() { + $this->assertEquals( + '', + Xml::element( 'element', null, null ), + 'Opening element with no attributes' + ); + } + + function testElementEmpty() { + $this->assertEquals( + '', + Xml::element( 'element', null, '' ), + 'Terminated empty element' + ); + } + + function testElementInputCanHaveAValueOfZero() { + $this->assertEquals( + '', + Xml::input( 'name', false, 0 ), + 'Input with a value of 0 (bug 23797)' + ); + } + function testElementEscaping() { + $this->assertEquals( + 'hello <there> you & you', + Xml::element( 'element', null, 'hello you & you' ), + 'Element with no attributes and content that needs escaping' + ); + } + + public function testEscapeTagsOnly() { + $this->assertEquals( '"><', Xml::escapeTagsOnly( '"><' ), + 'replace " > and < with their HTML entitites' + ); + } + + function testElementAttributes() { + $this->assertEquals( + '="<>">', + Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ), + 'Element attributes, keys are not escaped' + ); + } + + function testOpenElement() { + $this->assertEquals( + '', + Xml::openElement( 'element', array( 'k' => 'v' ) ), + 'openElement() shortcut' + ); + } + + function testCloseElement() { + $this->assertEquals( '', Xml::closeElement( 'element' ), 'closeElement() shortcut' ); + } + + # + # textarea + # + function testTextareaNoContent() { + $this->assertEquals( + '', + Xml::textarea( 'name', '' ), + 'textarea() with not content' + ); + } + + function testTextareaAttribs() { + $this->assertEquals( + '', + Xml::textarea( 'name', '', 20, 10 ), + 'textarea() with custom attribs' + ); + } + + # + # input and label + # + function testLabelCreation() { + $this->assertEquals( + '', + Xml::label( 'name', 'id' ), + 'label() with no attribs' + ); + } + function testLabelAttributeCanOnlyBeClass() { + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( 'generated' => true ) ), + 'label() can not be given a generated attribute' + ); + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( 'class' => 'nice' ) ), + 'label() can get a class attribute' + ); + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( + 'generated' => true, + 'class' => 'nice', + 'anotherattr' => 'value', + ) + ), + 'label() skip all attributes but "class"' + ); + } + + # + # JS + # + function testEscapeJsStringSpecialChars() { + $this->assertEquals( + '\\\\\r\n', + Xml::escapeJsString( "\\\r\n" ), + 'escapeJsString() with special characters' + ); + } + + function testEncodeJsVarBoolean() { + $this->assertEquals( + 'true', + Xml::encodeJsVar( true ), + 'encodeJsVar() with boolean' + ); + } + + function testEncodeJsVarNull() { + $this->assertEquals( + 'null', + Xml::encodeJsVar( null ), + 'encodeJsVar() with null' + ); + } + + function testEncodeJsVarArray() { + $this->assertEquals( + '["a", 1]', + Xml::encodeJsVar( array( 'a', 1 ) ), + 'encodeJsVar() with array' + ); + $this->assertEquals( + '{"a": "a", "b": 1}', + Xml::encodeJsVar( array( 'a' => 'a', 'b' => 1 ) ), + 'encodeJsVar() with associative array' + ); + } + + function testEncodeJsVarObject() { + $this->assertEquals( + '{"a": "a", "b": 1}', + Xml::encodeJsVar( (object)array( 'a' => 'a', 'b' => 1 ) ), + 'encodeJsVar() with object' + ); + } +} + +// TODO +class XmlSelectTest extends PHPUnit_Framework_TestCase { +} diff --git a/tests/phpunit/includes/api/ApiSetup.php b/tests/phpunit/includes/api/ApiSetup.php new file mode 100644 index 0000000000..f004f3c6be --- /dev/null +++ b/tests/phpunit/includes/api/ApiSetup.php @@ -0,0 +1,65 @@ +execute(); + + $data[0] = $module->getResultData(); + $data[1] = $req; + $data[2] = $_SESSION; + + return $data; + } + + static function setupUser() { + if ( self::$user == null || self::$sysopUser == null ) { + self::$user = new UserWrapper( 'User for MediaWiki automated tests', User::randomPassword() ); + self::$sysopUser = new UserWrapper( 'Sysop for MediaWiki automated tests', User::randomPassword(), 'sysop' ); + } + + $GLOBALS['wgUser'] = self::$sysopUser->user; + } +} + +class UserWrapper { + public $userName, $password, $user; + + public function __construct( $userName, $password, $group = '' ) { + $this->userName = $userName; + $this->password = $password; + + $this->user = User::newFromName( $this->userName ); + if ( !$this->user->getID() ) { + $this->user = User::createNew( $this->userName, array( + "email" => "test@example.com", + "real_name" => "Test User" ) ); + } + $this->user->setPassword( $this->password ); + + if ( $group !== '' ) { + $this->user->addGroup( $group ); + } + $this->user->saveSettings(); + } +} + diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php new file mode 100644 index 0000000000..5df834dbbc --- /dev/null +++ b/tests/phpunit/includes/api/ApiTest.php @@ -0,0 +1,227 @@ + null, + 'enablechunks' => false, + 'sessionkey' => null, + ); + } +} + +/** + * @group Database + * @group Destructive + */ +class ApiTest extends ApiTestSetup { + + function testRequireOnlyOneParameterDefault() { + $mock = new MockApi(); + + $this->assertEquals( + null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt", + "enablechunks" => false ), "filename", "enablechunks" ) ); + } + + /** + * @expectedException UsageException + */ + function testRequireOnlyOneParameterZero() { + $mock = new MockApi(); + + $this->assertEquals( + null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt", + "enablechunks" => 0 ), "filename", "enablechunks" ) ); + } + + /** + * @expectedException UsageException + */ + function testRequireOnlyOneParameterTrue() { + $mock = new MockApi(); + + $this->assertEquals( + null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt", + "enablechunks" => true ), "filename", "enablechunks" ) ); + } + + /** + * Test that the API will accept a FauxRequest and execute. The help action + * (default) throws a UsageException. Just validate we're getting proper XML + * + * @expectedException UsageException + */ + function testApi() { + $api = new ApiMain( + new FauxRequest( array( 'action' => 'help', 'format' => 'xml' ) ) + ); + $api->execute(); + $api->getPrinter()->setBufferResult( true ); + $api->printResult( false ); + $resp = $api->getPrinter()->getBuffer(); + + libxml_use_internal_errors( true ); + $sxe = simplexml_load_string( $resp ); + $this->assertNotType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + } + + /** + * Test result of attempted login with an empty username + */ + function testApiLoginNoName() { + $data = $this->doApiRequest( array( 'action' => 'login', + 'lgname' => '', 'lgpassword' => self::$user->password, + ) ); + $this->assertEquals( 'NoName', $data[0]['login']['result'] ); + } + + function testApiLoginBadPass() { + global $wgServer; + + $user = self::$user; + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + $ret = $this->doApiRequest( array( + "action" => "login", + "lgname" => $user->userName, + "lgpassword" => "bad", + ) + ); + + $result = $ret[0]; + + $this->assertNotType( "bool", $result ); + $a = $result["login"]["result"]; + $this->assertEquals( "NeedToken", $a ); + + $token = $result["login"]["token"]; + + $ret = $this->doApiRequest( array( + "action" => "login", + "lgtoken" => $token, + "lgname" => $user->userName, + "lgpassword" => "bad", + ) + ); + + $result = $ret[0]; + + $this->assertNotType( "bool", $result ); + $a = $result["login"]["result"]; + + $this->assertEquals( "WrongPass", $a ); + } + + function testApiLoginGoodPass() { + global $wgServer; + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + + $user = self::$user; + + $ret = $this->doApiRequest( array( + "action" => "login", + "lgname" => $user->userName, + "lgpassword" => $user->password, + ) + ); + + $result = $ret[0]; + $this->assertNotType( "bool", $result ); + $this->assertNotType( "null", $result["login"] ); + + $a = $result["login"]["result"]; + $this->assertEquals( "NeedToken", $a ); + $token = $result["login"]["token"]; + + $ret = $this->doApiRequest( array( + "action" => "login", + "lgtoken" => $token, + "lgname" => $user->userName, + "lgpassword" => $user->password, + ) + ); + + $result = $ret[0]; + + $this->assertNotType( "bool", $result ); + $a = $result["login"]["result"]; + + $this->assertEquals( "Success", $a ); + } + + function testApiGotCookie() { + $this->markTestIncomplete( "The server can't do external HTTP requests, and the internal one won't give cookies" ); + + global $wgServer, $wgScriptPath; + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml", + array( "method" => "POST", + "postData" => array( + "lgname" => self::$user->userName, + "lgpassword" => self::$user->password ) ) ); + $req->execute(); + + libxml_use_internal_errors( true ); + $sxe = simplexml_load_string( $req->getContent() ); + $this->assertNotType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + $this->assertNotType( "null", $sxe->login[0] ); + + $a = $sxe->login[0]->attributes()->result[0]; + $this->assertEquals( ' result="NeedToken"', $a->asXML() ); + $token = (string)$sxe->login[0]->attributes()->token; + + $req->setData( array( + "lgtoken" => $token, + "lgname" => self::$user->userName, + "lgpassword" => self::$user->password ) ); + $req->execute(); + + $cj = $req->getCookieJar(); + $serverName = parse_url( $wgServer, PHP_URL_HOST ); + $this->assertNotEquals( false, $serverName ); + $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName ); + $this->assertNotEquals( '', $serializedCookie ); + $this->assertRegexp( '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . self::$user->userName . '; .*Token=/', $serializedCookie ); + + return $cj; + } + + /** + * @depends testApiGotCookie + */ + function testApiListPages( CookieJar $cj ) { + $this->markTestIncomplete( "Not done with this yet" ); + global $wgServer; + + if ( $wgServer == "http://localhost" ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + $req = MWHttpRequest::factory( self::$apiUrl . "?action=query&format=xml&prop=revisions&" . + "titles=Main%20Page&rvprop=timestamp|user|comment|content" ); + $req->setCookieJar( $cj ); + $req->execute(); + libxml_use_internal_errors( true ); + $sxe = simplexml_load_string( $req->getContent() ); + $this->assertNotType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + $a = $sxe->query[0]->pages[0]->page[0]->attributes(); + } +} diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php new file mode 100644 index 0000000000..a2da6f8927 --- /dev/null +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -0,0 +1,671 @@ +username = $username; + $this->realname = $realname; + $this->email = $email; + $this->groups = $groups; + + // don't allow user to hardcode or select passwords -- people sometimes run tests + // on live wikis. Sometimes we create sysop users in these tests. A sysop user with + // a known password would be a Bad Thing. + $this->password = User::randomPassword(); + + $this->user = User::newFromName( $this->username ); + $this->user->load(); + + // In an ideal world we'd have a new wiki (or mock data store) for every single test. + // But for now, we just need to create or update the user with the desired properties. + // we particularly need the new password, since we just generated it randomly. + // In core MediaWiki, there is no functionality to delete users, so this is the best we can do. + if ( !$this->user->getID() ) { + // create the user + $this->user = User::createNew( + $this->username, array( + "email" => $this->email, + "real_name" => $this->realname + ) + ); + if ( !$this->user ) { + throw new Exception( "error creating user" ); + } + } + + // update the user to use the new random password and other details + $this->user->setPassword( $this->password ); + $this->user->setEmail( $this->email ); + $this->user->setRealName( $this->realname ); + // remove all groups, replace with any groups specified + foreach ( $this->user->getGroups() as $group ) { + $this->user->removeGroup( $group ); + } + if ( count( $this->groups ) ) { + foreach ( $this->groups as $group ) { + $this->user->addGroup( $group ); + } + } + $this->user->saveSettings(); + + } + +} + +abstract class ApiTestCase extends PHPUnit_Framework_TestCase { + public static $users; + + function setUp() { + global $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser; + + $wgMemc = new FakeMemCachedClient(); + $wgContLang = Language::factory( 'en' ); + $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); + $wgRequest = new FauxRequest( array() ); + + self::$users = array( + 'sysop' => new ApiTestUser( + 'Apitestsysop', + 'Api Test Sysop', + 'api_test_sysop@sample.com', + array( 'sysop' ) + ), + 'uploader' => new ApiTestUser( + 'Apitestuser', + 'Api Test User', + 'api_test_user@sample.com', + array() + ) + ); + + $wgUser = self::$users['sysop']->user; + + } + + protected function doApiRequest( $params, $session = null ) { + $_SESSION = isset( $session ) ? $session : array(); + + $request = new FauxRequest( $params, true, $_SESSION ); + $module = new ApiMain( $request, true ); + $module->execute(); + + return array( $module->getResultData(), $request, $_SESSION ); + } + + /** + * Add an edit token to the API request + * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the + * request, without actually requesting a "real" edit token + * @param $params: key-value API params + * @param $session: session array + */ + protected function doApiRequestWithToken( $params, $session ) { + if ( $session['wsToken'] ) { + // add edit token to fake session + $session['wsEditToken'] = $session['wsToken']; + // add token to request parameters + $params['token'] = md5( $session['wsToken'] ) . EDIT_TOKEN_SUFFIX; + return $this->doApiRequest( $params, $session ); + } else { + throw new Exception( "request data not in right format" ); + } + } + +} + +/** + * @group Database + * @group Destructive + */ +class ApiUploadTest extends ApiTestCase { + /** + * Fixture -- run before every test + */ + public function setUp() { + global $wgEnableUploads, $wgEnableAPI; + parent::setUp(); + + $wgEnableUploads = true; + $wgEnableAPI = true; + wfSetupSession(); + + ini_set( 'log_errors', 1 ); + ini_set( 'error_reporting', 1 ); + ini_set( 'display_errors', 1 ); + + $this->clearFakeUploads(); + } + + /** + * Fixture -- run after every test + * Clean up temporary files etc. + */ + function tearDown() { + } + + + /** + * Testing login + * XXX this is a funny way of getting session context + */ + function testLogin() { + $user = self::$users['uploader']; + + $params = array( + 'action' => 'login', + 'lgname' => $user->username, + 'lgpassword' => $user->password + ); + list( $result, , ) = $this->doApiRequest( $params ); + $this->assertArrayHasKey( "login", $result ); + $this->assertArrayHasKey( "result", $result['login'] ); + $this->assertEquals( "NeedToken", $result['login']['result'] ); + $token = $result['login']['token']; + + $params = array( + 'action' => 'login', + 'lgtoken' => $token, + 'lgname' => $user->username, + 'lgpassword' => $user->password + ); + list( $result, , $session ) = $this->doApiRequest( $params ); + $this->assertArrayHasKey( "login", $result ); + $this->assertArrayHasKey( "result", $result['login'] ); + $this->assertEquals( "Success", $result['login']['result'] ); + $this->assertArrayHasKey( 'lgtoken', $result['login'] ); + + return $session; + + } + + /** + * @depends testLogin + */ + public function testUploadRequiresToken( $session ) { + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload' + ) ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "The token parameter must be set", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + } + + /** + * @depends testLogin + */ + public function testUploadMissingParams( $session ) { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $exception = false; + try { + $this->doApiRequestWithToken( array( + 'action' => 'upload', + ), $session ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required", + $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + } + + + /** + * @depends testLogin + */ + public function testUpload( $session ) { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + } + catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); + $filePath = $filePaths[0]; + $fileSize = filesize( $filePath ); + $fileName = basename( $filePath ); + + $this->deleteFileByFileName( $fileName ); + $this->deleteFileByContent( $filePath ); + + + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + $exception = false; + try { + list( $result, , ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] ); + $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePath ); + } + + + /** + * @depends testLogin + */ + public function testUploadZeroLength( $session ) { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $mimeType = 'image/png'; + + $filePath = tempnam( wfTempDir(), "" ); + $fileName = "apiTestUploadZeroLength.png"; + + $this->deleteFileByFileName( $fileName ); + + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + $exception = false; + try { + $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $this->assertContains( 'The file you submitted was empty', $e->getMessage() ); + $exception = true; + } + $this->assertTrue( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePath ); + } + + + /** + * @depends testLogin + */ + public function testUploadSameFileName( $session ) { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + } + catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() ); + // we'll reuse this filename + $fileName = basename( $filePaths[0] ); + + // clear any other files with the same name + $this->deleteFileByFileName( $fileName ); + + // we reuse these params + $params = array( + 'action' => 'upload', + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + // first upload .... should succeed + + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $exception = false; + try { + list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception ); + + // second upload with the same name (but different content) + + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $exception = false; + try { + list( $result, , ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Warning', $result['upload']['result'] ); + $this->assertTrue( isset( $result['upload']['warnings'] ) ); + $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePaths[0] ); + unlink( $filePaths[1] ); + } + + + /** + * @depends testLogin + */ + public function testUploadSameContent( $session ) { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + } + catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); + $fileNames[0] = basename( $filePaths[0] ); + $fileNames[1] = "SameContentAs" . $fileNames[0]; + + // clear any other files with the same name or content + $this->deleteFileByContent( $filePaths[0] ); + $this->deleteFileByFileName( $fileNames[0] ); + $this->deleteFileByFileName( $fileNames[1] ); + + // first upload .... should succeed + + $params = array( + 'action' => 'upload', + 'filename' => $fileNames[0], + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for " . $fileNames[0], + ); + + if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $exception = false; + try { + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception ); + + + // second upload with the same content (but different name) + + if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'filename' => $fileNames[1], + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for " . $fileNames[1], + ); + + $exception = false; + try { + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Warning', $result['upload']['result'] ); + $this->assertTrue( isset( $result['upload']['warnings'] ) ); + $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileNames[0] ); + $this->deleteFileByFilename( $fileNames[1] ); + unlink( $filePaths[0] ); + } + + + /** + * @depends testLogin + */ + public function testUploadStash( $session ) { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + } + catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); + $filePath = $filePaths[0]; + $fileSize = filesize( $filePath ); + $fileName = basename( $filePath ); + + $this->deleteFileByFileName( $fileName ); + $this->deleteFileByContent( $filePath ); + + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'stash' => 1, + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + $exception = false; + try { + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertFalse( $exception ); + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] ); + $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); + $this->assertTrue( isset( $result['upload']['sessionkey'] ) ); + $sessionkey = $result['upload']['sessionkey']; + + // it should be visible from Special:UploadStash + // XXX ...but how to test this, with a fake WebRequest with the session? + + // now we should try to release the file from stash + $params = array( + 'action' => 'upload', + 'sessionkey' => $sessionkey, + 'filename' => $fileName, + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName, altered", + ); + + $this->clearFakeUploads(); + $exception = false; + try { + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePath ); + } + + + + /** + * Helper function -- remove files and associated articles by Title + * @param $title Title: title to be removed + */ + public function deleteFileByTitle( $title ) { + if ( $title->exists() ) { + $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) ); + $noOldArchive = ""; // yes this really needs to be set this way + $comment = "removing for test"; + $restrictDeletedVersions = false; + $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions ); + if ( !$status->isGood() ) { + return false; + } + $article = new Article( $title ); + $article->doDeleteArticle( "removing for test" ); + + // see if it now doesn't exist; reload + $title = Title::newFromText( $fileName, NS_FILE ); + } + return ! ( $title && $title instanceof Title && $title->exists() ); + } + + /** + * Helper function -- remove files and associated articles with a particular filename + * @param $fileName String: filename to be removed + */ + public function deleteFileByFileName( $fileName ) { + return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) ); + } + + + /** + * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them. + * @param $filePath String: path to file on the filesystem + */ + public function deleteFileByContent( $filePath ) { + $hash = File::sha1Base36( $filePath ); + $dupes = RepoGroup::singleton()->findBySha1( $hash ); + $success = true; + foreach ( $dupes as $dupe ) { + $success &= $this->deleteFileByTitle( $dupe->getTitle() ); + } + return $success; + } + + /** + * Fake an upload by dumping the file into temp space, and adding info to $_FILES. + * (This is what PHP would normally do). + * @param $fieldName String: name this would have in the upload form + * @param $fileName String: name to title this + * @param $type String: mime type + * @param $filePath String: path where to find file contents + */ + function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { + $tmpName = tempnam( wfTempDir(), "" ); + if ( !file_exists( $filePath ) ) { + throw new Exception( "$filePath doesn't exist!" ); + }; + + if ( !copy( $filePath, $tmpName ) ) { + throw new Exception( "couldn't copy $filePath to $tmpName" ); + } + + clearstatcache(); + $size = filesize( $tmpName ); + if ( $size === false ) { + throw new Exception( "couldn't stat $tmpName" ); + } + + $_FILES[ $fieldName ] = array( + 'name' => $fileName, + 'type' => $type, + 'tmp_name' => $tmpName, + 'size' => $size, + 'error' => null + ); + + return true; + + } + + /** + * Remove traces of previous fake uploads + */ + function clearFakeUploads() { + $_FILES = array(); + } + + +} + diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php new file mode 100644 index 0000000000..0742a0af4e --- /dev/null +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -0,0 +1,237 @@ +doApiRequest( array( + 'action' => 'login', + 'lgname' => self::$sysopUser->userName, + 'lgpassword' => self::$sysopUser->password ) ); + + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "NeedToken", $data[0]['login']['result'] ); + $token = $data[0]['login']['token']; + + $data = $this->doApiRequest( array( + 'action' => 'login', + "lgtoken" => $token, + "lgname" => self::$sysopUser->userName, + "lgpassword" => self::$sysopUser->password ), $data ); + + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "Success", $data[0]['login']['result'] ); + $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] ); + + return $data; + } + + function testGettingToken() { + foreach ( array( self::$user, self::$sysopUser ) as $user ) { + $this->getUserTokens( $user ); + } + } + + function getUserTokens( $user ) { + $GLOBALS['wgUser'] = $user->user; + $data = $this->doApiRequest( array( + 'action' => 'query', + 'titles' => 'Main Page', + 'intoken' => 'edit|delete|protect|move|block|unblock', + 'prop' => 'info' ) ); + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'pages', $data[0]['query'] ); + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + + $rights = $user->user->getRights(); + + $this->assertArrayHasKey( $key, $data[0]['query']['pages'] ); + $this->assertArrayHasKey( 'edittoken', $data[0]['query']['pages'][$key] ); + $this->assertArrayHasKey( 'movetoken', $data[0]['query']['pages'][$key] ); + + if ( isset( $rights['delete'] ) ) { + $this->assertArrayHasKey( 'deletetoken', $data[0]['query']['pages'][$key] ); + } + + if ( isset( $rights['block'] ) ) { + $this->assertArrayHasKey( 'blocktoken', $data[0]['query']['pages'][$key] ); + $this->assertArrayHasKey( 'unblocktoken', $data[0]['query']['pages'][$key] ); + } + + if ( isset( $rights['protect'] ) ) { + $this->assertArrayHasKey( 'protecttoken', $data[0]['query']['pages'][$key] ); + } + + return $data; + } + + function testGetToken() { + return $this->getUserTokens( self::$sysopUser ); + } + + /** + * @depends testGetToken + */ + function testWatchEdit( $data ) { + $this->markTestIncomplete( "Broken" ); + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + $data = $this->doApiRequest( array( + 'action' => 'edit', + 'title' => 'Main Page', + 'text' => 'new text', + 'token' => $pageinfo['edittoken'], + 'watchlist' => 'watch' ), $data ); + $this->assertArrayHasKey( 'edit', $data[0] ); + $this->assertArrayHasKey( 'result', $data[0]['edit'] ); + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + return $data; + } + + /** + * @depends testWatchEdit + */ + function testWatchClear( $data ) { + $data = $this->doApiRequest( array( + 'action' => 'query', + 'list' => 'watchlist' ), $data ); + + if ( isset( $data[0]['query']['watchlist'] ) ) { + $wl = $data[0]['query']['watchlist']; + + foreach ( $wl as $page ) { + $data = $this->doApiRequest( array( + 'action' => 'watch', + 'title' => $page['title'], + 'unwatch' => true ), $data ); + } + } + $data = $this->doApiRequest( array( + 'action' => 'query', + 'list' => 'watchlist' ), $data ); + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'watchlist', $data[0]['query'] ); + $this->assertEquals( 0, count( $data[0]['query']['watchlist'] ) ); + + return $data; + } + + /** + * @depends testGetToken + */ + function testWatchProtect( $data ) { + $this->markTestIncomplete( "Broken" ); + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + $data = $this->doApiRequest( array( + 'action' => 'protect', + 'token' => $pageinfo['protecttoken'], + 'title' => 'Main Page', + 'protections' => 'edit=sysop', + 'watchlist' => 'unwatch' ), $data ); + + $this->assertArrayHasKey( 'protect', $data[0] ); + $this->assertArrayHasKey( 'protections', $data[0]['protect'] ); + $this->assertEquals( 1, count( $data[0]['protect']['protections'] ) ); + $this->assertArrayHasKey( 'edit', $data[0]['protect']['protections'][0] ); + } + + /** + * @depends testGetToken + */ + function testGetRollbackToken( $data ) { + if ( !Title::newFromText( 'Main Page' )->exists() ) { + $this->markTestIncomplete( "The article [[Main Page]] does not exist" ); + } + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'prop' => 'revisions', + 'titles' => 'Main Page', + 'rvtoken' => 'rollback' ), $data ); + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'pages', $data[0]['query'] ); + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + + if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) { + $this->markTestIncomplete( "Target page (Main Page) doesn't exist" ); + } + + $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] ); + $this->assertArrayHasKey( 'revisions', $data[0]['query']['pages'][$key] ); + $this->assertArrayHasKey( 0, $data[0]['query']['pages'][$key]['revisions'] ); + $this->assertArrayHasKey( 'rollbacktoken', $data[0]['query']['pages'][$key]['revisions'][0] ); + + return $data; + } + + /** + * @depends testGetRollbackToken + */ + function testWatchRollback( $data ) { + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]['revisions'][0]; + + try { + $data = $this->doApiRequest( array( + 'action' => 'rollback', + 'title' => 'Main Page', + 'user' => $pageinfo['user'], + 'token' => $pageinfo['rollbacktoken'], + 'watchlist' => 'watch' ), $data ); + } catch( UsageException $ue ) { + if( $ue->getCodeString() == 'onlyauthor' ) { + $this->markTestIncomplete( "Only one author to 'Main Page', cannot test rollback" ); + } else { + $this->fail( "Received error " . $ue->getCodeString() ); + } + } + + $this->assertArrayHasKey( 'rollback', $data[0] ); + $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); + } + + /** + * @depends testGetToken + */ + function testWatchDelete( $data ) { + $this->markTestIncomplete( "Broken" ); + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + $data = $this->doApiRequest( array( + 'action' => 'delete', + 'token' => $pageinfo['deletetoken'], + 'title' => 'Main Page' ), $data ); + $this->assertArrayHasKey( 'delete', $data[0] ); + $this->assertArrayHasKey( 'title', $data[0]['delete'] ); + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'list' => 'watchlist' ), $data ); + + $this->markTestIncomplete( 'This test needs to verify the deleted article was added to the users watchlist' ); + } +} diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php new file mode 100644 index 0000000000..6bb9d00fee --- /dev/null +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -0,0 +1,287 @@ + + */ + +/** + * RandomImageGenerator: does what it says on the tin. + * Can fetch a random image, or also write a number of them to disk with random filenames. + */ +class RandomImageGenerator { + + private $dictionaryFile; + private $minWidth = 400; + private $maxWidth = 800; + private $minHeight = 400; + private $maxHeight = 800; + private $circlesToDraw = 5; + private $imageWriteMethod; + + public function __construct( $options ) { + global $wgUseImageMagick, $wgImageMagickConvertCommand; + foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxHeight', 'circlesToDraw' ) as $property ) { + if ( isset( $options[$property] ) ) { + $this->$property = $options[$property]; + } + } + + // find the dictionary file, to generate random names + if ( !isset( $this->dictionaryFile ) ) { + foreach ( array( '/usr/share/dict/words', '/usr/dict/words' ) as $dictionaryFile ) { + if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) { + $this->dictionaryFile = $dictionaryFile; + break; + } + } + } + if ( !isset( $this->dictionaryFile ) ) { + throw new Exception( "RandomImageGenerator: dictionary file not found or not specified properly" ); + } + + // figure out how to write images + if ( class_exists( 'Imagick' ) ) { + $this->imageWriteMethod = 'writeImageWithApi'; + } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) { + $this->imageWriteMethod = 'writeImageWithCommandLine'; + } else { + throw new Exception( "RandomImageGenerator: could not find a suitable method to write images" ); + } + } + + /** + * Writes random images with random filenames to disk in the directory you specify, or current working directory + * + * @param $number Integer: number of filenames to write + * @param $format String: optional, must be understood by ImageMagick, such as 'jpg' or 'gif' + * @param $dir String: directory, optional (will default to current working directory) + * @return Array: filenames we just wrote + */ + function writeImages( $number, $format = 'jpg', $dir = null ) { + $filenames = $this->getRandomFilenames( $number, $format, $dir ); + foreach( $filenames as $filename ) { + $this->{$this->imageWriteMethod}( $this->getImageSpec(), $format, $filename ); + } + return $filenames; + } + + /** + * Return a number of randomly-generated filenames + * Each filename uses two words randomly drawn from the dictionary, like elephantine_spatula.jpg + * + * @param $number Integer: of filenames to generate + * @param $extension String: optional, defaults to 'jpg' + * @param $dir String: optional, defaults to current working directory + * @return Array: of filenames + */ + private function getRandomFilenames( $number, $extension = 'jpg', $dir = null ) { + if ( is_null( $dir ) ) { + $dir = getcwd(); + } + $filenames = array(); + foreach( $this->getRandomWordPairs( $number ) as $pair ) { + $basename = $pair[0] . '_' . $pair[1]; + if ( !is_null( $extension ) ) { + $basename .= '.' . $extension; + } + $basename = preg_replace( '/\s+/', '', $basename ); + $filenames[] = "$dir/$basename"; + } + + return $filenames; + + } + + + /** + * Generate data representing an image of random size (within limits), + * consisting of randomly colored and sized circles against a random background color + * (This data is used in the writeImage* methods). + * @return {Mixed} + */ + public function getImageSpec() { + $spec = array(); + + $spec['width'] = mt_rand( $this->minWidth, $this->maxWidth ); + $spec['height'] = mt_rand( $this->minHeight, $this->maxHeight ); + $spec['fill'] = $this->getRandomColor(); + + $diagonalLength = sqrt( pow( $spec['width'], 2 ) + pow( $spec['height'], 2 ) ); + + $draws = array(); + for ( $i = 0; $i <= $this->circlesToDraw; $i++ ) { + $radius = mt_rand( 0, $diagonalLength / 4 ); + $originX = mt_rand( -1 * $radius, $spec['width'] + $radius ); + $originY = mt_rand( -1 * $radius, $spec['height'] + $radius ); + $perimeterX = $originX + $radius; + $perimeterY = $originY + $radius; + + $draw = array(); + $draw['fill'] = $this->getRandomColor(); + $draw['circle'] = array( + 'originX' => $originX, + 'originY' => $originY, + 'perimeterX' => $perimeterX, + 'perimeterY' => $perimeterY + ); + $draws[] = $draw; + + } + + $spec['draws'] = $draws; + + return $spec; + } + + + /** + * Based on an image specification, write such an image to disk, using Imagick PHP extension + * @param $spec: spec describing background and circles to draw + * @param $format: file format to write + * @param $filename: filename to write to + */ + public function writeImageWithApi( $spec, $format, $filename ) { + $image = new Imagick(); + $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) ); + + foreach ( $spec['draws'] as $drawSpec ) { + $draw = new ImagickDraw(); + $draw->setFillColor( $drawSpec['fill'] ); + $circle = $drawSpec['circle']; + $draw->circle( $circle['originX'], $circle['originY'], $circle['perimeterX'], $circle['perimeterY'] ); + $image->drawImage( $draw ); + } + + $image->setImageFormat( $format ); + $image->writeImage( $filename ); + } + + + /** + * Based on an image specification, write such an image to disk, using the command line ImageMagick program ('convert'). + * + * Sample command line: + * $ convert -size 100x60 xc:rgb(90,87,45) \ + * -draw 'fill rgb(12,34,56) circle 41,39 44,57' \ + * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \ + * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png + * + * @param $spec: spec describing background and circles to draw + * @param $format: file format to write (unused by this method but kept so it has the same signature as writeImageWithApi) + * @param $filename: filename to write to + */ + public function writeImageWithCommandLine( $spec, $format, $filename ) { + global $wgImageMagickConvertCommand; + $args = array(); + $args[] = "-size " . wfEscapeShellArg( $spec['width'] . 'x' . $spec['height'] ); + $args[] = wfEscapeShellArg( "xc:" . $spec['fill'] ); + foreach( $spec['draws'] as $draw ) { + $fill = $draw['fill']; + $originX = $draw['circle']['originX']; + $originY = $draw['circle']['originY']; + $perimeterX = $draw['circle']['perimeterX']; + $perimeterY = $draw['circle']['perimeterY']; + $drawCommand = "fill $fill circle $originX,$originY $perimeterX,$perimeterY"; + $args[] = '-draw ' . wfEscapeShellArg( $drawCommand ); + } + $args[] = wfEscapeShellArg( $filename ); + + $command = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . implode( " ", $args ); + $retval = null; + wfShellExec( $command, $retval ); + return ( $retval === 0 ); + } + + /** + * Generate a string of random colors for ImageMagick, like "rgb(12, 37, 98)" + * + * @return {String} + */ + public function getRandomColor() { + $components = array(); + for ($i = 0; $i <= 2; $i++ ) { + $components[] = mt_rand( 0, 255 ); + } + return 'rgb(' . join(', ', $components) . ')'; + } + + /** + * Get an array of random pairs of random words, like array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) ); + * + * @param $number Integer: number of pairs + * @return Array: of two-element arrays + */ + private function getRandomWordPairs( $number ) { + $lines = $this->getRandomLines( $number * 2 ); + // construct pairs of words + $pairs = array(); + $count = count( $lines ); + for( $i = 0; $i < $count; $i += 2 ) { + $pairs[] = array( $lines[$i], $lines[$i+1] ); + } + return $pairs; + } + + + /** + * Return N random lines from a file + * + * Will throw exception if the file could not be read or if it had fewer lines than requested. + * + * @param $number_desired Integer: number of lines desired + * @return Array: of exactly n elements, drawn randomly from lines the file + */ + private function getRandomLines( $number_desired ) { + $filepath = $this->dictionaryFile; + + // initialize array of lines + $lines = array(); + for ( $i = 0; $i < $number_desired; $i++ ) { + $lines[] = null; + } + + /* + * This algorithm obtains N random lines from a file in one single pass. It does this by replacing elements of + * a fixed-size array of lines, less and less frequently as it reads the file. + */ + $fh = fopen( $filepath, "r" ); + if ( !$fh ) { + throw new Exception( "couldn't open $filepath" ); + } + $line_number = 0; + $max_index = $number_desired - 1; + while( !feof( $fh ) ) { + $line = fgets( $fh ); + if ( $line !== false ) { + $line_number++; + $line = trim( $line ); + if ( mt_rand( 0, $line_number ) <= $max_index ) { + $lines[ mt_rand( 0, $max_index ) ] = $line; + } + } + } + fclose( $fh ); + if ( $line_number < $number_desired ) { + throw new Exception( "not enough lines in $filepath" ); + } + + return $lines; + } + +} diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php new file mode 100644 index 0000000000..cab1ec7de6 --- /dev/null +++ b/tests/phpunit/includes/api/generateRandomImages.php @@ -0,0 +1,25 @@ +writeImages( $number, $format ); diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php new file mode 100644 index 0000000000..4cd3daf04a --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -0,0 +1,87 @@ +lastQuery = $sql; + return true; + } + + function replaceVars( $s ) { + return parent::replaceVars( $s ); + } +} + +/** + * @group sqlite + */ +class DatabaseSqliteTest extends PHPUnit_Framework_TestCase { + var $db; + + public function setUp() { + if ( !Sqlite::isPresent() ) { + $this->markTestSkipped( 'No SQLite support detected' ); + } + $this->db = new MockDatabaseSqlite(); + } + + private function replaceVars( $sql ) { + // normalize spacing to hide implementation details + return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) ); + } + + public function testReplaceVars() { + $this->assertEquals( 'foo', $this->replaceVars( 'foo' ), "Don't break anything accidentally" ); + + $this->assertEquals( "CREATE TABLE /**/foo (foo_key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + . "foo_bar TEXT, foo_name TEXT NOT NULL DEFAULT '', foo_int INTEGER, foo_int2 INTEGER );", + $this->replaceVars( "CREATE TABLE /**/foo (foo_key int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + foo_bar char(13), foo_name varchar(255) binary NOT NULL DEFAULT '', foo_int tinyint ( 8 ), foo_int2 int(16) ) ENGINE=MyISAM;" ) + ); + + $this->assertEquals( "CREATE TABLE foo ( foo1 REAL, foo2 REAL, foo3 REAL );", + $this->replaceVars( "CREATE TABLE foo ( foo1 FLOAT, foo2 DOUBLE( 1,10), foo3 DOUBLE PRECISION );" ) + ); + + $this->assertEquals( "CREATE TABLE foo ( foo_binary1 BLOB, foo_binary2 BLOB );", + $this->replaceVars( "CREATE TABLE foo ( foo_binary1 binary(16), foo_binary2 varbinary(32) );" ) + ); + + $this->assertEquals( "CREATE TABLE text ( text_foo TEXT );", + $this->replaceVars( "CREATE TABLE text ( text_foo tinytext );" ), + 'Table name changed' + ); + + $this->assertEquals( "CREATE TABLE enums( enum1 TEXT, myenum TEXT)", + $this->replaceVars( "CREATE TABLE enums( enum1 ENUM('A', 'B'), myenum ENUM ('X', 'Y'))" ) + ); + + $this->assertEquals( "ALTER TABLE foo ADD COLUMN foo_bar INTEGER DEFAULT 42", + $this->replaceVars( "ALTER TABLE foo\nADD COLUMN foo_bar int(10) unsigned DEFAULT 42" ) + ); + } + + public function testTableName() { + // @todo Moar! + $db = new DatabaseSqliteStandalone( ':memory:' ); + $this->assertEquals( 'foo', $db->tableName( 'foo' ) ); + $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) ); + $db->tablePrefix( 'foo' ); + $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) ); + $this->assertEquals( 'foobar', $db->tableName( 'bar' ) ); + } + + function testEntireSchema() { + global $IP; + + $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" ); + if ( $result !== true ) { + $this->fail( $result ); + } + } +} \ No newline at end of file diff --git a/tests/phpunit/includes/db/DatabaseTest.php b/tests/phpunit/includes/db/DatabaseTest.php new file mode 100644 index 0000000000..b184331ff4 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseTest.php @@ -0,0 +1,95 @@ +db = wfGetDB( DB_SLAVE ); + } + + function testAddQuotesNull() { + $check = "NULL"; + if ( $this->db->getType() === 'sqlite' ) { + $check = "''"; + } + $this->assertEquals( $check, $this->db->addQuotes( null ) ); + } + + function testAddQuotesInt() { + # returning just "1234" should be ok too, though... + # maybe + $this->assertEquals( + "'1234'", + $this->db->addQuotes( 1234 ) ); + } + + function testAddQuotesFloat() { + # returning just "1234.5678" would be ok too, though + $this->assertEquals( + "'1234.5678'", + $this->db->addQuotes( 1234.5678 ) ); + } + + function testAddQuotesString() { + $this->assertEquals( + "'string'", + $this->db->addQuotes( 'string' ) ); + } + + function testAddQuotesStringQuote() { + $check = "'string''s cause trouble'"; + if ( $this->db->getType() === 'mysql' ) { + $check = "'string\'s cause trouble'"; + } + $this->assertEquals( + $check, + $this->db->addQuotes( "string's cause trouble" ) ); + } + + function testFillPreparedEmpty() { + $sql = $this->db->fillPrepared( + 'SELECT * FROM interwiki', array() ); + $this->assertEquals( + "SELECT * FROM interwiki", + $sql ); + } + + function testFillPreparedQuestion() { + $sql = $this->db->fillPrepared( + 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?', + array( 4, "Snicker's_paradox" ) ); + + $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker''s_paradox'"; + if ( $this->db->getType() === 'mysql' ) { + $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'"; + } + $this->assertEquals( $check, $sql ); + } + + function testFillPreparedBang() { + $sql = $this->db->fillPrepared( + 'SELECT user_id FROM ! WHERE user_name=?', + array( '"user"', "Slash's Dot" ) ); + + $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash''s Dot'"; + if ( $this->db->getType() === 'mysql' ) { + $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'"; + } + $this->assertEquals( $check, $sql ); + } + + function testFillPreparedRaw() { + $sql = $this->db->fillPrepared( + "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'", + array( '"user"', "Slash's Dot" ) ); + $this->assertEquals( + "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'", + $sql ); + } + +} + + diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php new file mode 100644 index 0000000000..3825a8fabf --- /dev/null +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -0,0 +1,73 @@ +backend = new ParserTestSuiteBackend; + $this->setName( 'Parser tests' ); + } + + public static function suite() { + global $IP; + + $tester = new self; + + $iter = new TestFileIterator( "$IP/maintenance/tests/parser/parserTests.txt", $tester ); + $tester->count = 0; + + foreach ( $iter as $test ) { + $tester->suite->addTest( new ParserUnitTest( $tester, $test ), array( 'Parser', 'Destructive', 'Database', 'Broken' ) ); + $tester->count++; + } + + return $tester->suite; + } + + public function count() { + return $this->count; + } + + public function toString() { + return "MediaWiki Parser Tests"; + } + + public function getBackend() { + return $this->backend; + } + + public function getIterator() { + return $this->iterator; + } + + public function publishTestArticles() { + if ( empty( $this->articles ) ) { + return; + } + + foreach ( $this->articles as $name => $text ) { + $title = Title::newFromText( $name ); + + if ( $title->getArticleID( Title::GAID_FOR_UPDATE ) == 0 ) { + ParserTest::addArticle( $name, $text ); + } + } + $this->articles = array(); + } + + public function addArticle( $name, $text, $line ) { + $this->articles[$name] = $text; + } + + public function showRunFile( $path ) { + /* Nothing shown when run from phpunit */ + } +} + diff --git a/tests/phpunit/includes/parser/ParserHelpers.php b/tests/phpunit/includes/parser/ParserHelpers.php new file mode 100644 index 0000000000..43da34218b --- /dev/null +++ b/tests/phpunit/includes/parser/ParserHelpers.php @@ -0,0 +1,132 @@ +suite = $suite; + $this->test = $test; + } + + function count() { return 1; } + + public function run( PHPUnit_Framework_TestResult $result = null ) { + PHPUnit_Framework_Assert::resetCount(); + if ( $result === NULL ) { + $result = new PHPUnit_Framework_TestResult; + } + + $this->suite->publishTestArticles(); // Add articles needed by the tests. + $backend = new ParserTestSuiteBackend; + $result->startTest( $this ); + + // Support the transition to PHPUnit 3.5 where PHPUnit_Util_Timer is replaced with PHP_Timer + if ( class_exists( 'PHP_Timer' ) ) { + PHP_Timer::start(); + } else { + PHPUnit_Util_Timer::start(); + } + + $r = false; + try { + # Run the test. + # On failure, the subclassed backend will throw an exception with + # the details. + $pt = new PHPUnitParserTest; + $r = $pt->runTest( $this->test['test'], $this->test['input'], + $this->test['result'], $this->test['options'], $this->test['config'] + ); + } + catch ( PHPUnit_Framework_AssertionFailedError $e ) { + + // PHPUnit_Util_Timer -> PHP_Timer support (see above) + if ( class_exists( 'PHP_Timer' ) ) { + $result->addFailure( $this, $e, PHP_Timer::stop() ); + } else { + $result->addFailure( $this, $e, PHPUnit_Util_Timer::stop() ); + } + } + catch ( Exception $e ) { + // PHPUnit_Util_Timer -> PHP_Timer support (see above) + if ( class_exists( 'PHP_Timer' ) ) { + $result->addFailure( $this, $e, PHP_Timer::stop() ); + } else { + $result->addFailure( $this, $e, PHPUnit_Util_Timer::stop() ); + } + } + + // PHPUnit_Util_Timer -> PHP_Timer support (see above) + if ( class_exists( 'PHP_Timer' ) ) { + $result->endTest( $this, PHP_Timer::stop() ); + } else { + $result->endTest( $this, PHPUnit_Util_Timer::stop() ); + } + + $backend->recorder->record( $this->test['test'], $r ); + $this->addToAssertionCount( PHPUnit_Framework_Assert::getCount() ); + + return $result; + } + + public function toString() { + return $this->test['test']; + } + +} + +class ParserTestSuiteBackend extends PHPUnit_FrameWork_TestSuite { + public $recorder; + public $term; + static $usePHPUnit = false; + + function __construct() { + parent::__construct(); + $this->setupRecorder(null); + self::$usePHPUnit = method_exists('PHPUnit_Framework_Assert', 'assertEquals'); + } + + function showTesting( $desc ) { + } + + function showRunFile( $path ) { + } + + function showTestResult( $desc, $result, $out ) { + if ( $result === $out ) { + return self::showSuccess( $desc, $result, $out ); + } else { + return self::showFailure( $desc, $result, $out ); + } + } + + public function setupRecorder( $options ) { + $this->recorder = new PHPUnitTestRecorder( $this ); + } +} + +class PHPUnitTestRecorder extends TestRecorder { + function record( $test, $result ) { + $this->total++; + $this->success += $result; + + } + + function reportPercentage( $success, $total ) { } +} diff --git a/tests/phpunit/includes/search/SearchDbTest.php b/tests/phpunit/includes/search/SearchDbTest.php new file mode 100644 index 0000000000..daf9747fe6 --- /dev/null +++ b/tests/phpunit/includes/search/SearchDbTest.php @@ -0,0 +1,37 @@ +db = wfGetDB( DB_MASTER ); + if ( !$this->db ) { + $this->markTestIncomplete( "Can't find a database to test with." ); + } + + parent::setup(); + + // Initialize search database with data + $GLOBALS['wgContLang'] = new Language; + $this->insertSearchData(); + + $this->insertSearchData(); + $searchType = preg_replace( "/Database/", "Search", + get_class( $this->db ) ); + $this->search = new $searchType( $this->db ); + } + + function tearDown() { + $this->removeSearchData(); + unset( $this->search ); + } +} + + diff --git a/tests/phpunit/includes/search/SearchEngineTest.php b/tests/phpunit/includes/search/SearchEngineTest.php new file mode 100644 index 0000000000..b97afb6728 --- /dev/null +++ b/tests/phpunit/includes/search/SearchEngineTest.php @@ -0,0 +1,195 @@ +db->getType(); + $dbSupported = + ($dbType === 'mysql') + || ( $dbType === 'sqlite' && $this->db->getFulltextSearchModule() == 'FTS3' ); + + if( !$dbSupported ) { + $this->markTestSkipped( "MySQL or SQLite with FTS3 only" ); + } + } + + function pageExists( $title ) { + return false; + } + + function insertSearchData() { + if ( $this->pageExists( 'Not_Main_Page' ) ) { + return; + } + $this->insertPage( "Not_Main_Page", "This is not a main page", 0 ); + $this->insertPage( 'Talk:Not_Main_Page', 'This is not a talk page to the main page, see [[smithee]]', 1 ); + $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]', 0 ); + $this->insertPage( 'Talk:Smithee', 'This article sucks.', 1 ); + $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.', 0 ); + $this->insertPage( 'Another_page', 'This page also is unrelated.', 0 ); + $this->insertPage( 'Help:Help', 'Help me!', 4 ); + $this->insertPage( 'Thppt', 'Blah blah', 0 ); + $this->insertPage( 'Alan_Smithee', 'yum', 0 ); + $this->insertPage( 'Pages', 'are\'food', 0 ); + $this->insertPage( 'HalfOneUp', 'AZ', 0 ); + $this->insertPage( 'FullOneUp', 'AZ', 0 ); + $this->insertPage( 'HalfTwoLow', 'az', 0 ); + $this->insertPage( 'FullTwoLow', 'az', 0 ); + $this->insertPage( 'HalfNumbers', '1234567890', 0 ); + $this->insertPage( 'FullNumbers', '1234567890', 0 ); + $this->insertPage( 'DomainName', 'example.com', 0 ); + } + + function removeSearchData() { + return; + /*while ( count( $this->pageList ) ) { + list( $title, $id ) = array_pop( $this->pageList ); + $article = new Article( $title, $id ); + $article->doDeleteArticle( "Search Test" ); + }*/ + } + + function fetchIds( $results ) { + $this->assertTrue( is_object( $results ) ); + + $matches = array(); + while ( $row = $results->next() ) { + $matches[] = $row->getTitle()->getPrefixedText(); + } + $results->free(); + # Search is not guaranteed to return results in a certain order; + # sort them numerically so we will compare simply that we received + # the expected matches. + sort( $matches ); + return $matches; + } + + // Modified version of WikiRevision::importOldRevision() + function insertPage( $pageName, $text, $ns ) { + $dbw = $this->db; + $title = Title::newFromText( $pageName ); + + $userId = 0; + $userText = 'WikiSysop'; + $comment = 'Search Test'; + + // avoid memory leak...? + $linkCache = LinkCache::singleton(); + $linkCache->clear(); + + $article = new Article( $title ); + $pageId = $article->getId(); + $created = false; + if ( $pageId == 0 ) { + # must create the page... + $pageId = $article->insertOn( $dbw ); + $created = true; + } + + # FIXME: Use original rev_id optionally (better for backups) + # Insert the row + $revision = new Revision( array( + 'page' => $pageId, + 'text' => $text, + 'comment' => $comment, + 'user' => $userId, + 'user_text' => $userText, + 'timestamp' => 0, + 'minor_edit' => false, + ) ); + $revId = $revision->insertOn( $dbw ); + $changed = $article->updateIfNewerOn( $dbw, $revision ); + + $GLOBALS['wgTitle'] = $title; + if ( $created ) { + Article::onArticleCreate( $title ); + $article->createUpdates( $revision ); + } elseif ( $changed ) { + Article::onArticleEdit( $title ); + $article->editUpdates( + $text, $comment, false, 0, $revId ); + } + + $su = new SearchUpdate( $article->getId(), $pageName, $text ); + $su->doUpdate(); + + $this->pageList[] = array( $title, $article->getId() ); + + return true; + } + + function testFullWidth() { + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'AZ' ) ), + "Search for normalized from Half-width Upper" ); + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'az' ) ), + "Search for normalized from Half-width Lower" ); + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'AZ' ) ), + "Search for normalized from Full-width Upper" ); + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'az' ) ), + "Search for normalized from Full-width Lower" ); + } + + function testTextSearch() { + $this->assertEquals( + array( 'Smithee' ), + $this->fetchIds( $this->search->searchText( 'smithee' ) ), + "Plain search failed" ); + } + + function testTextPowerSearch() { + $this->search->setNamespaces( array( 0, 1, 4 ) ); + $this->assertEquals( + array( + 'Smithee', + 'Talk:Not Main Page', + ), + $this->fetchIds( $this->search->searchText( 'smithee' ) ), + "Power search failed" ); + } + + function testTitleSearch() { + $this->assertEquals( + array( + 'Alan Smithee', + 'Smithee', + ), + $this->fetchIds( $this->search->searchTitle( 'smithee' ) ), + "Title search failed" ); + } + + function testTextTitlePowerSearch() { + $this->search->setNamespaces( array( 0, 1, 4 ) ); + $this->assertEquals( + array( + 'Alan Smithee', + 'Smithee', + 'Talk:Smithee', + ), + $this->fetchIds( $this->search->searchTitle( 'smithee' ) ), + "Title power search failed" ); + } + +} diff --git a/tests/phpunit/includes/search/SearchUpdateTest.php b/tests/phpunit/includes/search/SearchUpdateTest.php new file mode 100644 index 0000000000..108b280270 --- /dev/null +++ b/tests/phpunit/includes/search/SearchUpdateTest.php @@ -0,0 +1,80 @@ +doUpdate(); + return array( MockSearch::$title, MockSearch::$text ); + } + + function updateText( $text ) { + list( , $resultText ) = $this->update( $text ); + $resultText = trim( $resultText ); // abstract from some implementation details + return $resultText; + } + + function setUp() { + global $wgSearchType; + + self::$searchType = $wgSearchType; + $wgSearchType = 'MockSearch'; + } + + function tearDown() { + global $wgSearchType; + + $wgSearchType = self::$searchType; + } + + function testUpdateText() { + $this->assertEquals( + 'test', + $this->updateText( '
    TeSt
    ' ), + 'HTML stripped, text lowercased' + ); + + $this->assertEquals( + 'foo bar boz quux', + $this->updateText( << +
    foo
    bar + bozquux + +EOT + ), 'Stripping HTML tables' ); + + $this->assertEquals( + 'a b', + $this->updateText( 'a > b' ), + 'Handle unclosed tags' + ); + + $text = str_pad( "foo assertNotEquals( + '', + $this->updateText( $text ), + 'Bug 18609' + ); + } +} diff --git a/tests/phpunit/install-phpunit.sh b/tests/phpunit/install-phpunit.sh new file mode 100755 index 0000000000..aca7ace6e5 --- /dev/null +++ b/tests/phpunit/install-phpunit.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +if [ `id -u` -ne 0 ]; then + echo '*** ERROR' Must be root to run + exit 1 +fi + +if ( has_binary phpunit ); then + echo PHPUnit already installed +else if ( has_binary pear ); then + echo Installing phpunit with pear + pear channel-discover pear.phpunit.de + pear install phpunit/PHPUnit +else if ( has_binary apt-get ); then + echo Installing phpunit with apt-get + apt-get install phpunit +else if ( has_binary yum ); then + echo Installing phpunit with yum + yum install phpunit +fi +fi +fi +fi diff --git a/tests/phpunit/languages/LanguageBe_taraskTest.php b/tests/phpunit/languages/LanguageBe_taraskTest.php new file mode 100644 index 0000000000..bf01e14ab7 --- /dev/null +++ b/tests/phpunit/languages/LanguageBe_taraskTest.php @@ -0,0 +1,31 @@ +lang = Language::factory( 'Be_tarask' ); + } + function tearDown() { + unset( $this->lang ); + } + + /** see bug 23156 & r64981 */ + function testSearchRightSingleQuotationMarkAsApostroph() { + $this->assertEquals( + "'", + $this->lang->normalizeForSearch( '’' ), + 'bug 23156: U+2019 conversion to U+0027' + ); + } + /** see bug 23156 & r64981 */ + function testCommafy() { + $this->assertEquals( '1,234,567', $this->lang->commafy( '1234567' ) ); + $this->assertEquals( '12,345', $this->lang->commafy( '12345' ) ); + } + /** see bug 23156 & r64981 */ + function testDoesNotCommafyFourDigitsNumber() { + $this->assertEquals( '1234', $this->lang->commafy( '1234' ) ); + } +} diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php new file mode 100644 index 0000000000..8e30259b17 --- /dev/null +++ b/tests/phpunit/languages/LanguageTest.php @@ -0,0 +1,61 @@ +lang = Language::factory( 'en' ); + } + function tearDown() { + unset( $this->lang ); + } + + function testLanguageConvertDoubleWidthToSingleWidth() { + $this->assertEquals( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + $this->lang->normalizeForSearch( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + ), + 'convertDoubleWidth() with the full alphabet and digits' + ); + } + + function testFormatTimePeriod() { + $this->assertEquals( + "9.5s", + $this->lang->formatTimePeriod( 9.45 ), + 'formatTimePeriod() rounding (<10s)' + ); + + $this->assertEquals( + "10s", + $this->lang->formatTimePeriod( 9.95 ), + 'formatTimePeriod() rounding (<10s)' + ); + + $this->assertEquals( + "1m 0s", + $this->lang->formatTimePeriod( 59.55 ), + 'formatTimePeriod() rounding (<60s)' + ); + + $this->assertEquals( + "2m 0s", + $this->lang->formatTimePeriod( 119.55 ), + 'formatTimePeriod() rounding (<1h)' + ); + + $this->assertEquals( + "1h 0m 0s", + $this->lang->formatTimePeriod( 3599.55 ), + 'formatTimePeriod() rounding (<1h)' + ); + + $this->assertEquals( + "2h 0m 0s", + $this->lang->formatTimePeriod( 7199.55 ), + 'formatTimePeriod() rounding (>=1h)' + ); + } +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php new file mode 100755 index 0000000000..94d4ebd2d6 --- /dev/null +++ b/tests/phpunit/phpunit.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +=' ) ) { + # PHPUnit 3.5.0 introduced a nice autoloader based on class name + require_once( 'PHPUnit/Autoload.php' ); +} else { + # Keep the old pre PHPUnit 3.5.0 behaviour for compatibility + require_once( 'PHPUnit/TextUI/Command.php' ); +} +PHPUnit_TextUI_Command::main(); diff --git a/tests/phpunit/run-tests.bat b/tests/phpunit/run-tests.bat new file mode 100644 index 0000000000..e6eb3e0cfc --- /dev/null +++ b/tests/phpunit/run-tests.bat @@ -0,0 +1 @@ +php phpunit.php --configuration suite.xml %* diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml new file mode 100644 index 0000000000..cb25963bda --- /dev/null +++ b/tests/phpunit/suite.xml @@ -0,0 +1,35 @@ + + + + + + ./includes + + + ./languages + + + ./skins + + + ./suites/UploadFromUrlTestSuite.php + + + ./suites/ExtensionsTestSuite.php + + + + + Utility + Broken + Stub + + + diff --git a/tests/phpunit/suites/ExtensionsTestSuite.php b/tests/phpunit/suites/ExtensionsTestSuite.php new file mode 100644 index 0000000000..4bf3542799 --- /dev/null +++ b/tests/phpunit/suites/ExtensionsTestSuite.php @@ -0,0 +1,33 @@ +addTestFile( $file ); + } + if ( !count( $files ) ) { + $this->addTest( new DummyExtensionsTest( 'testNothing' ) ); + } + } + + public static function suite() { + return new self; + } +} + +/** + * Needed to avoid warnings like 'No tests found in class "ExtensionsTestSuite".' + * when no extensions with tests are used. + */ +class DummyExtensionsTest extends PHPUnit_Framework_TestCase { + public function testNothing() { + + } +} diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php new file mode 100644 index 0000000000..3f5b1fe53e --- /dev/null +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -0,0 +1,181 @@ + 'LocalRepo', + 'name' => 'local', + 'directory' => wfTempDir() . '/test-repo', + 'url' => 'http://example.com/images', + 'deletedDir' => wfTempDir() . '/test-repo/delete', + 'hashLevels' => 2, + 'transformVia404' => false, + ); + $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + + + $wgEnableParserCache = false; + $wgDeferredUpdateList = array(); + $wgMemc = &wfGetMainCache(); + $messageMemc = &wfGetMessageCacheStorage(); + $parserMemc = &wfGetParserCacheStorage(); + + // $wgContLang = new StubContLang; + $wgUser = new User; + $wgLang = new StubUserLang; + $wgOut = new StubObject( 'wgOut', 'OutputPage' ); + $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + $wgRequest = new WebRequest; + + $wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', + array( $messageMemc, $wgUseDatabaseMessages, + $wgMsgCacheExpiry ) ); + if ( $wgStyleDirectory === false ) { + $wgStyleDirectory = "$IP/skins"; + } + + } + + public function tearDown() { + $this->teardownUploadDir( $this->uploadDir ); + } + + private $uploadDir; + private $keepUploads; + + /** + * Remove the dummy uploads directory + */ + private function teardownUploadDir( $dir ) { + if ( $this->keepUploads ) { + return; + } + + // delete the files first, then the dirs. + self::deleteFiles( + array ( + "$dir/3/3a/Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", + + "$dir/0/09/Bad.jpg", + ) + ); + + self::deleteDirs( + array ( + "$dir/3/3a", + "$dir/3", + "$dir/thumb/6/65", + "$dir/thumb/6", + "$dir/thumb/3/3a/Foobar.jpg", + "$dir/thumb/3/3a", + "$dir/thumb/3", + + "$dir/0/09/", + "$dir/0/", + + "$dir/thumb", + "$dir", + ) + ); + } + + /** + * Delete the specified files, if they exist. + * + * @param $files Array: full paths to files to delete. + */ + private static function deleteFiles( $files ) { + foreach ( $files as $file ) { + if ( file_exists( $file ) ) { + unlink( $file ); + } + } + } + + /** + * Delete the specified directories, if they exist. Must be empty. + * + * @param $dirs Array: full paths to directories to delete. + */ + private static function deleteDirs( $dirs ) { + foreach ( $dirs as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); + } + } + } + + /** + * Create a dummy uploads directory which will contain a couple + * of files in order to pass existence tests. + * + * @return String: the directory + */ + private function setupUploadDir() { + global $IP; + + if ( $this->keepUploads ) { + $dir = wfTempDir() . '/mwParser-images'; + + if ( is_dir( $dir ) ) { + return $dir; + } + } else { + $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; + } + + wfDebug( "Creating upload directory $dir\n" ); + + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + return $dir; + } + + wfMkdirParents( $dir . '/3/3a' ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); + + wfMkdirParents( $dir . '/0/09' ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" ); + + return $dir; + } + + public static function suite() { + // Hack to invoke the autoloader required to get phpunit to recognize + // the UploadFromUrlTest class + class_exists( 'UploadFromUrlTest' ); + $suite = new UploadFromUrlTestSuite( 'UploadFromUrlTest' ); + return $suite; + } +} diff --git a/tests/selenium/Selenium.php b/tests/selenium/Selenium.php new file mode 100644 index 0000000000..ecf7f9ec94 --- /dev/null +++ b/tests/selenium/Selenium.php @@ -0,0 +1,190 @@ +tester = new Testing_Selenium( $this->browser, self::$url, $this->host, + $this->port, $this->timeout ); + if ( method_exists( $this->tester, "setVerbose" ) ) $this->tester->setVerbose( $this->verbose ); + + $this->tester->start(); + $this->isStarted = true; + } + + public function stop() { + $this->tester->stop(); + $this->tester = null; + $this->isStarted = false; + } + + public function login() { + if ( strlen( $this->user ) == 0 ) { + return; + } + $this->open( self::$url . '/index.php?title=Special:Userlogin' ); + $this->type( 'wpName1', $this->user ); + $this->type( 'wpPassword1', $this->pass ); + $this->click( "//input[@id='wpLoginAttempt']" ); + $this->waitForPageToLoad( 10000 ); + + // after login we redirect to the main page. So check whether the "Prefernces" top menu item exists + $value = $this->isElementPresent( "//li[@id='pt-preferences']" ); + + if ( $value != true ) { + throw new Testing_Selenium_Exception( "Login Failed" ); + } + + } + + public static function getInstance() { + if ( null === self::$_instance ) { + throw new MWException( "No instance set yet" ); + } + + return self::$_instance; + } + + public function loadPage( $title, $action ) { + $this->open( self::$url . '/index.php?title=' . $title . '&action=' . $action ); + } + + public function setLogger( $logger ) { + $this->logger = $logger; + } + + public function getLogger( ) { + return $this->logger; + } + + public function log( $message ) { + $this->logger->write( $message ); + } + + public function setUrl( $url ) { + self::$url = $url; + } + + static public function getUrl() { + return self::$url; + } + + public function setPort( $port ) { + $this->port = $port; + } + + public function getPort() { + return $this->port; + } + + public function setUser( $user ) { + $this->user = $user; + } + + // Function to get username + public function getUser() { + return $this->user; + } + + + public function setPass( $pass ) { + $this->pass = $pass; + } + + //add function to get password + public function getPass( ) { + return $this->pass; + } + + + public function setHost( $host ) { + $this->host = $host; + } + + public function setVerbose( $verbose ) { + $this->verbose = $verbose; + } + + public function setAvailableBrowsers( $availableBrowsers ) { + $this->browsers = $availableBrowsers; + } + + public function setJUnitLogfile( $junitlogfile ) { + $this->junitlogfile = $junitlogfile; + } + + public function getJUnitLogfile( ) { + return $this->junitlogfile; + } + + public function setRunAgainstGrid( $runagainstgrid ) { + $this->runagainstgrid = $runagainstgrid; + } + + public function setBrowser( $b ) { + if ($this->runagainstgrid) { + $this->browser = $b; + return true; + } + if ( !isset( $this->browsers[$b] ) ) { + throw new MWException( "Invalid Browser: $b.\n" ); + } + + $this->browser = $this->browsers[$b]; + } + + public function getAvailableBrowsers() { + return $this->browsers; + } + + public function __call( $name, $args ) { + $t = call_user_func_array( array( $this->tester, $name ), $args ); + return $t; + } + + // Prevent external cloning + protected function __clone() { } + // Prevent external construction + // protected function __construct() {} +} diff --git a/tests/selenium/SeleniumConfig.php b/tests/selenium/SeleniumConfig.php new file mode 100644 index 0000000000..ca69b1f0e0 --- /dev/null +++ b/tests/selenium/SeleniumConfig.php @@ -0,0 +1,88 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + */ + +class SeleniumServerManager { + private $SeleniumStartServer = false; + private $OS = ''; + private $SeleniumServerPid = 'NaN'; + private $SeleniumServerPort = 4444; + private $SeleniumServerStartTimeout = 10; // 10 secs. + private $SeleniumServerExecPath; + + public function __construct( $startServer, + $serverPort, + $serverExecPath ) { + $this->OS = (string) PHP_OS; + if ( isset( $startServer ) ) + $this->SeleniumStartServer = $startServer; + if ( isset( $serverPort ) ) + $this->SeleniumServerPort = $serverPort; + if ( isset( $serverExecPath ) ) + $this->SeleniumServerExecPath = $serverExecPath; + return; + } + + // Getters for certain private attributes. No setters, since they + // should not change after the manager object is created. + + public function getSeleniumStartServer() { + return $this->SeleniumStartServer; + } + + public function getSeleniumServerPort() { + return $this->SeleniumServerPort; + } + + public function getSeleniumServerPid() { + return $this->SeleniumServerPid; + } + + // Changing value of SeleniumStartServer allows starting server after + // creation of the class instance. Only allow setting SeleniumStartServer + // to true, since after server is started, it is shut down by stop(). + + public function setSeleniumStartServer( $startServer ) { + if ( $startServer == true ) $this->SeleniumStartServer = true; + } + + // return values are: 1) started - server started, 2) failed - + // server not started, 3) running - instructed to start server, but + // server already running + + public function start() { + + if ( !$this->SeleniumStartServer ) return 'failed'; + + // commented out cases are untested + + switch ( $this->OS ) { + case "Linux": +# case' CYGWIN_NT-5.1': + case 'Darwin': +# case 'FreeBSD': +# case 'HP-UX': +# case 'IRIX64': +# case 'NetBSD': +# case 'OpenBSD': +# case 'SunOS': +# case 'Unix': + // *nix based OS + return $this->startServerOnUnix(); + break; + case "Windows": + case "WIN32": + case "WINNT": + // Windows + return $this->startServerOnWindows(); + break; + default: + // An untested OS + return 'failed'; + break; + } + } + + public function stop() { + + // commented out cases are untested + + switch ( $this->OS ) { + case "Linux": +# case' CYGWIN_NT-5.1': + case 'Darwin': +# case 'FreeBSD': +# case 'HP-UX': +# case 'IRIX64': +# case 'NetBSD': +# case 'OpenBSD': +# case 'SunOS': +# case 'Unix': + // *nix based OS + return $this->stopServerOnUnix(); + break; + case "Windows": + case "WIN32": + case "WINNT": + // Windows + return $this->stopServerOnWindows(); + break; + default: + // An untested OS + return 'failed'; + break; + } + } + + private function startServerOnUnix() { + + $output = array(); + $user = $_ENV['USER']; + // @fixme this should be a little more generalized :) + if (PHP_OS == 'Darwin') { + // Mac OS X's ps barfs on the 'w' param, but doesn't need it. + $ps = "ps -U %s"; + } else { + // Good on Linux + $ps = "ps -U %s w"; + } + $psCommand = sprintf($ps, escapeshellarg($user)); + exec($psCommand . " | grep -i selenium-server", $output); + + // Start server. If there is already a server running, + // return running. + + if ( isset( $this->SeleniumServerExecPath ) ) { + $found = 0; + foreach ( $output as $string ) { + $found += preg_match( + '~^(.*)java(.+)-jar(.+)selenium-server~', + $string ); + } + if ( $found == 0 ) { + + // Didn't find the selenium server. Start it up. + // First set up comamand line suffix. + // NB: $! is pid of last job run in background + // The echo guarentees it is put into $op when + // the exec command is run. + + $commandSuffix = ' > /dev/null 2>&1'. ' & echo $!'; + $portText = ' -port ' . $this->SeleniumServerPort; + $command = "java -jar " . + escapeshellarg($this->SeleniumServerExecPath) . + $portText . $commandSuffix; + exec($command ,$op); + $pid = (int)$op[0]; + if ( $pid != "" ) + $this->SeleniumServerPid = $pid; + else { + $this->SeleniumServerPid = 'NaN'; + // Server start failed. + return 'failed'; + } + // Wait for the server to startup and listen + // on its port. Note: this solution kinda + // stinks, since it uses a wait loop - dnessett + + wfSuppressWarnings(); + for ( $cnt = 1; + $cnt <= $this->SeleniumServerStartTimeout; + $cnt++ ) { + $fp = fsockopen ( 'localhost', + $this->SeleniumServerPort, + $errno, $errstr, 0 ); + if ( !$fp ) { + sleep( 1 ); + continue; + // Server start succeeded. + } else { + fclose ( $fp ); + return 'started'; + } + } + wfRestoreWarnings(); + echo ( "Starting Selenium server timed out.\n" ); + return 'failed'; + } + // server already running. + else return 'running'; + + } + // No Server execution path defined. + return 'failed'; + } + + private function startServerOnWindows() { + // Unimplemented. + return 'failed'; + } + + private function stopServerOnUnix() { + + if ( !empty( $this->SeleniumServerPid ) && + $this->SeleniumServerPid != 'NaN' ) { + exec( "kill -9 " . $this->SeleniumServerPid ); + return 'stopped'; + } + else return 'failed'; + } + + private function stopServerOnWindows() { + // Unimplemented. + return 'failed'; + + } +} diff --git a/tests/selenium/SeleniumTestCase.php b/tests/selenium/SeleniumTestCase.php new file mode 100644 index 0000000000..11e1b192ec --- /dev/null +++ b/tests/selenium/SeleniumTestCase.php @@ -0,0 +1,103 @@ +selenium = Selenium::getInstance(); + } + + public function tearDown() { + + } + + public function __call( $method, $args ) { + return call_user_func_array( array( $this->selenium, $method ), $args ); + } + + public function assertSeleniumAttributeEquals( $attribute, $value ) { + $attr = $this->getAttribute( $attribute ); + $this->assertEquals( $attr, $value ); + } + + public function assertSeleniumHTMLContains( $element, $text ) { + $innerHTML = $this->getText( $element ); + // or assertContains + $this->assertRegExp( "/$text/", $innerHTML ); + } + +//Common Funtions Added for Selenium Tests + + public function getExistingPage(){ + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->type("searchInput", "new" ); + $this->click("searchGoButton"); + $this->waitForPageToLoad("30000"); + } + + public function getNewPage($pageName){ + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->type("searchInput", $pageName ); + $this->click("searchGoButton"); + $this->waitForPageToLoad("30000"); + $this->click("link=".$pageName); + $this->waitForPageToLoad("600000"); + + + } + // Loading the mediawiki editor + public function loadWikiEditor(){ + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + } + + // Clear the content of the mediawiki editor + public function clearWikiEditor(){ + $this->type("wpTextbox1", ""); + } + + // Click on the 'Show preview' button of the mediawiki editor + public function clickShowPreviewBtn(){ + $this->click("wpPreview"); + } + + // Click on the 'Save Page' button of the mediawiki editor + public function clickSavePageBtn(){ + $this->click("wpSave"); + } + + // Click on the 'Edit' link + public function clickEditLink(){ + $this->click("link=Edit"); + $this->waitForPageToLoad("30000"); + } + + public function deletePage($pageName){ + $isLinkPresent = False; + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click("link=Log out"); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=Log in / create account" ); + $this->waitForPageToLoad( "30000" ); + $this->type( "wpName1", "nadeesha" ); + $this->type( "wpPassword1", "12345" ); + $this->click( "wpLoginAttempt" ); + $this->waitForPageToLoad( "30000" ); + $this->type( "searchInput", $pageName ); + $this->click( "searchGoButton"); + $this->waitForPageToLoad( "30000" ); + + $this->click( "link=Delete" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "wpConfirmB" ); + $this->waitForPageToLoad( "30000" ); + + } + + + +} diff --git a/tests/selenium/SeleniumTestConsoleLogger.php b/tests/selenium/SeleniumTestConsoleLogger.php new file mode 100644 index 0000000000..b6f5496ce9 --- /dev/null +++ b/tests/selenium/SeleniumTestConsoleLogger.php @@ -0,0 +1,25 @@ +'; + $out .= htmlentities( $message ); + // if ( $mode == SeleniumTestSuite::RESULT_OK ) $out .= '
    '; + if ( $mode != SeleniumTestSuite::CONTINUE_LINE ) { + $out .= "\n"; + } + + echo $out; + } +} diff --git a/tests/selenium/SeleniumTestHTMLLogger.php b/tests/selenium/SeleniumTestHTMLLogger.php new file mode 100644 index 0000000000..21332cf000 --- /dev/null +++ b/tests/selenium/SeleniumTestHTMLLogger.php @@ -0,0 +1,36 @@ +addHeadItem( 'selenium', '' ); + } + + public function write( $message, $mode = false ) { + global $wgOut; + $out = ''; + if ( $mode == SeleniumTestSuite::RESULT_OK ) { + $out .= ''; + } + $out .= htmlspecialchars( $message ); + if ( $mode == SeleniumTestSuite::RESULT_OK ) { + $out .= ''; + } + if ( $mode != SeleniumTestSuite::CONTINUE_LINE ) { + $out .= '
    '; + } + + $wgOut->addHTML( $out ); + } +} diff --git a/tests/selenium/SeleniumTestListener.php b/tests/selenium/SeleniumTestListener.php new file mode 100644 index 0000000000..9436f67271 --- /dev/null +++ b/tests/selenium/SeleniumTestListener.php @@ -0,0 +1,68 @@ +logger = $loggerInstance; + } + + public function addError( PHPUnit_Framework_Test $test, Exception $e, $time ) { + $this->logger->write( 'Error: ' . $e->getMessage() ); + $this->tests_failed++; + } + + public function addFailure( PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time ) + { + $this->logger->write( 'Failed: ' . $e->getMessage() ); + $this->tests_failed++; + } + + public function addIncompleteTest( PHPUnit_Framework_Test $test, Exception $e, $time ) + { + $this->logger->write( 'Incomplete.' ); + $this->tests_failed++; + } + + public function addSkippedTest( PHPUnit_Framework_Test $test, Exception $e, $time ) + { + $this->logger->write( 'Skipped.' ); + $this->tests_failed++; + } + + public function startTest( PHPUnit_Framework_Test $test ) { + $this->logger->write( + 'Testing ' . $test->getName() . ' ... ', + SeleniumTestSuite::CONTINUE_LINE + ); + } + + public function endTest( PHPUnit_Framework_Test $test, $time ) { + if ( !$test->hasFailed() ) { + $this->logger->write( 'OK', SeleniumTestSuite::RESULT_OK ); + $this->tests_ok++; + } + } + + public function startTestSuite( PHPUnit_Framework_TestSuite $suite ) { + $this->logger->write( 'Testsuite ' . $suite->getName() . ' started.' ); + $this->tests_ok = 0; + $this->tests_failed = 0; + } + + public function endTestSuite( PHPUnit_Framework_TestSuite $suite ) { + $this->logger->write('Testsuite ' . $suite->getName() . ' ended.' ); + if ( $this->tests_ok > 0 || $this->tests_failed > 0 ) { + $this->logger->write( ' OK: ' . $this->tests_ok . ' Failed: ' . $this->tests_failed ); + } + $this->tests_ok = 0; + $this->tests_failed = 0; + } + + public function statusMessage( $message ) { + $this->logger->write( $message ); + } +} + diff --git a/tests/selenium/SeleniumTestSuite.php b/tests/selenium/SeleniumTestSuite.php new file mode 100644 index 0000000000..ba178051fa --- /dev/null +++ b/tests/selenium/SeleniumTestSuite.php @@ -0,0 +1,46 @@ +isSetUp ) { + return; + } + $this->isSetUp = true; + $this->selenium = Selenium::getInstance(); + $this->selenium->start(); + $this->selenium->open( $this->selenium->getUrl() . '/index.php?setupTestSuite=' . $this->getName() ); + if ( $this->loginBeforeTests ) { + $this->login(); + } + } + + public function tearDown() { + $this->selenium->open( $this->selenium->getUrl() . '/index.php?clearTestSuite=' . $this->getName() ); + $this->selenium->stop(); + } + + public function login() { + $this->selenium->login(); + } + + public function loadPage( $title, $action ) { + $this->selenium->loadPage( $title, $action ); + } + + protected function setLoginBeforeTests( $loginBeforeTests = true ) { + $this->loginBeforeTests = $loginBeforeTests; + } +} diff --git a/tests/selenium/data/Wikipedia-logo-v2-de.png b/tests/selenium/data/Wikipedia-logo-v2-de.png new file mode 100644 index 0000000000000000000000000000000000000000..70385243c6e5672522a3c0df8c8688a129508094 GIT binary patch literal 21479 zcmaI71yChHvo3gWcXwxScb5Yk+}+*X-JQW1++7BDceg|zcOHFGpECzZB0wlr5UH#YNjo-h{x03b-M)wJBSuzZ$IR^M>B;2D&gAG~$;`^j%ljW5Y;25Q2u4?L2RCCcMh91l|KGb7wnz>4_gP<|2<4!1IFxS?8MB<#PXjm{Wnlv{{J6p zZ~uSLu5K#k|F^vVpNw7Ayq(OMRm@!--Caz-CeDK5KTSFDiMyB^yE(e3IXc??_b4h^ zIl4KzS~)tAimP&xYFay(IeNPO7g}DPPujuN&Dg=zTpB1y{zcDZZEeOUDJjm)F3u*( z4dmfsWd%y|u!wSqvPp{b@bZXr@$icNH!aZ7)ZO0P!R^0k&His%vHy|wKZ;=Q^wl!Z z+{N0%+)UEN(Vq0bI?QMNKg+`NKl1zEv}XUaEWH0CE%TQ!%>S9z|Hm}{ch#5f{3rW= zl=iFef7IUG;Y-(DzLff^Y7zkeP|}wMimG|7|MQ12N?Tt0bai|nPOK+`L`{@;B4rzf zUEmXeY@yqs30OgH(XeX?Z1E7WjqEB4Y0;s-P{yGVFKcXz3zH}H{pQ*8dHk5;dg1Td zBMl2zyq&(fm3*-E8t)o#>>V&CfIQV$ac?CkMl`!ZW+kXI@XMz;r1;(nO6u<~`1qA# zEW)2s^gKEV8cxl`KX?8gfd3cN89w3aC~s%Jkm6f{TzTl^6#dh;*uA~|iF1$J1Z=Ws zQaZL6kMng-7C}LK*GOa{TF>p?_^yY4vvoB!Zjs^PXuM~_bz|p+kV1wQZ4Q{k#0peY zRBi_+Xp`e73i_RHWchPaAoYIVvD@3zr|bO#kB-BDtG19q@R;c6(t`uDs-Kl)FoxA7 zC2=H7rwmAnXjg)d0tyl@uaM-zC3x?-0X}P zuY;&pA&yiQy0^b)*O#9+WC?~!oX6AFSu3HCr(`HE&zVGj1PwtSN2-lXEFkhG_yfTDJp`?qkY0cRQP|g9T~b1V8=j#%;d0MFPhjn! z7%WNo*Zw|6vLp}~_K1vtpvb_?yfG?*J4e#TPR+;F(Or~7N?>ZbUk|&`{lgJ2|L7ZH zV!7^s@^;m}sDixodAH|fUILlM>)ok%5*ix0jO*iNxwusP{DE|e!{7Nre+koLx^d=$ zm_|1E{mYGS=mpU+F*mVhXxrdyWA5kcEqyuPIKF?IwoRgl{CvW#&LFRdHA=*F5B9Zi z(#Isi5d5J&A9iwb604;TE5bNMe?~1N)VtVV>OGhG16vrC2m|N%p}ian8=J2%VDLHM z3Qy=$)h`<1dHdT_OFFai5(cgpD4cG#V6*hI4A-&M77u8f!AuLRkCn_FvM{(wlmT zUI4c3?CKS&$1z2TAK)lnlL@#PxtX#5j_02FnjJ6YCR&d7?pbd@rkaZ^G*T=6Q%1XHaH{j^mzWK!E`4*7W)^9N6d9bT+0v%0#b z=-Wd3KiO~m87wO+OOrt`BmhZwcTW55R(%mPrW8V4>6#N44R$XufsR*~jP&6)f~+)m z`da{5Drqp&2mJWS$NOs}+t@Q78JSSBw8{3;(vtFdaZw{4qA9M(E&vDC%*-q-ahwjC z9mEy{6%DDdYkW#p6?@4~#d%K`1YN-6wL{AI;ezJ9X&_@!VBql#`2*+>&}swiX}x$|w8@0if)4v_}6fg>wH^9`?zZUY}djLT*FJX?`?I#GhYRQ&oaBmA*KhL}C5= z8E5`2uv?DGymX1ZY3$ou2lH4Qen*Rcmq$LAn-I4yjcwm^gw5~HD05n)JOujA=H}EG zts)Wdoe`5fjT$X!eoqHW*`nKl4TruS*X>^a%7}pxjG>hC5|vgb+>CIBVDcdfatx>0 zt1Ffy`1*)$osiwM)QtZA8Z4(Usjw+Qpd_%7b02*Rf;03uT&Mcy&qB^>%|)2Y9@*oE zzlv0ThR1go;>HYXgHLao`hw99ms6#5LQ*~(t*ah?R}SsdTT5)rjCr0j2Nk2N@||s= zAXb0^_*g9G{lO4(efLM;c{QdS4sTxgyf&2n?=w0ww%L*tQhJCfY@C*c7yB?M#3iOC z4xLeU)e)IOb>o;ieIAVVzw3jk<#Qm3n9mA@M8rCqtrvfkDfGn06ALb+z?q2Z=xofI z_}tLb@g?u=?P)WL%6o0>xIja;a1ytIQY+6GnqZUH_2+?!@`dos!Q9csKh6{A?azub%UlaY;D7)rKCC4{tpOrARR zA}NWy^F8&ApIj|k1alG)7TyKY2mzE4G*FZYvG6#B7+7~naOWNH@Rll7zjfYJRaX=g zB(AsFDjq+a(nGw>bhhBI><4yMv>%Ldik?9p$0x%G2eFi`})c}^~aCs7Zc=xS+fr^E{K$RYH_J1v8} z3hoMqL@km}L?e-0V??Jw6H#sCBuY+BhaW=bsi~=?2kL6r)mgpA55?O8Z=V(VuMZr5 zHBlDCQKpMt1MlgNMEt%!CSYka`3bSHrGtY-h0o7Ws_b2s3;R1EHCi|oPrWX zU&A1@0Dn{!g=mG4{BZgnFXxInOVnzu3GL4}+U95&>3yT6tsS*c+OdHGl!(xTDY3q` zt4#XS)ATU z8ybxL6xGEl$XNT`opM_lu*$c6=-(gO5`LA~)|O_M&lMc!`$ssmUt9QOm!9m9Y46cr zvt~F6^&WFSX?wDrZXzZ#Cg1i+xL{&qbG+agR5sgha9u0J#7PKL{5`wPZ$zM+sJaio zf(=BQL|C4Y!9_rrF^|DZ8lX)VXQKQD)@Q;}fBj{E-`)(+wlg4WJa~#+yo+2ONeW~o zzxySDsf2;CB?D66j6_VpoM;6y(-_hUvW{l*rFwiH%joO6L)6uJ{a_5H&a$fB8Pq-M z`A%$eEOGVNpGf2E*Gm@USGPqdq!Nn0<$LRVU=R4&yuTndTg>2RMhg0REXEUURLZtF zy!h6WKjA9~;RF&_RW??Ih!QbV+pDQj3Y*rv5tjMm1E?<^x%;wbR@f9hl*#e+dWa{#%*x{Z_h z+%No?74PzQBm1kuUxM%f&!HEiHc`>h=)xnd=loi4t=D=^AKs4}f|-&w2gk>Y+;*8p zBc}5+z;EBE7DJ#APJFjth1|{R`!&h8G1wDZul?^vTAYLd5GCrk7<1&%7TkGP`P-ie zJ|7*&K#5?AlJb_)?qaDhnBAwV_s8e%+)CTkTDy2+B|^}#J%w{BU28u} z&oXC>BTtSJw-E6ai-vjl5*Oq5kB>b8pSxW{Rf8?%&RZ$ZUxh=FzfD}Mzhx;>mv^pE zXkdoguu*Dzd*cNJ@^UeGlqR>wH{lT4XNYsSh0*S<^vY**1&&hEDgR52rZT=cY=m#9 zA}1fDMD<9Om*+AJoFZ?=%;~Rj`?dM@T-W;|Lk+#dRxL$nJ^jG#xFyG2k9CERXNAh| z`tdXz29ap+lsUrg-{txZ!j3W5Re>3w&n_czL3&k>;<1(4lH~qkF*2THu6(o0<5xnU z2e<$Ah)LG?CG*=q*O%4m&yv3m?ZL<-LJZFKk&4gUBIIQ9sp~Gb>Zggz3CE!}*U<;D z1%9o)d;1tC6Xp3BLJ_XJzOyV0;E{hUu|mPY#(7%D$)B{HYu_1^ZS^la7Z(@T!4d&m z+6c(M?~u@xxkhq?>O8@`AiMz7X#IfG!C`U6UDsc~l=to?CpJ;kmEAw;jRX5_WX)R& zB_iLN{GMH|b^D#e149`xD$-X!-#aT(s_Qy+K-4t_gCQYl)7}^IGBQO-HFUqpwYSS1 z^P~A&FJWV26Fw7{l|T3P(2ua&U}*}Fk9zLN-A#pDCw|LXA>4kKW$pn|68N#H)^_Cu zlR&$s=PL)Z2Y>4-X$EQtQMat9Q((|BTS?@9tQ88hGPRE1AlMAQNyx#N?%Y?N&?jW0 zx@Y`tjKUScIy@wg_7_s*3%$~Y<_$?A7bLW#;1N3$l>hP6LrhgVf}ri=mXOHOtl2J= z0T^MV1M8Cl0%PvMh?AO_(@RGyET26!6&z}bcsB%d?vI8)1aX(FHZ!sVr7Zw(w|7~_Xdf~@jrE8H-+B0<0tPnk)E8i9avYs zziIe!XRF?OU1Ou8;}E;MEJ|8L%5U|F6W#E@u)RWAGzJ9M7ccUB@7LT%tJnBk%rPHq zqan-nk}@_Fka426dr_4WG~3ne;T)kw5Ch~+=WPr#6Jr^a9r2e3lg7rzM%QI&W^v_P z?0+Du(_wSy*IO>l0GA91*&T{Rz%7Vz+S#uDT_SJ4C@H7uijo*~HPfGZ-&hhF7Gl1e zXn8%!PR;-@gsVXoAaVL9vXd|d*^tfl1Jit(2pt1mJ7#pKPvJr*h(b}I!fAVddmm9v zgTzIDD4(U)i$p`wTKd%X`0k7s74lInanT}l2oIcaQeN*>kofIZgeUwd91o{a6}mId zU0}GP(Sxw4N!9zt_mQvsNBJvCG~BgxWhHIGb(NO6HEPiNzR&31j=C%3Cwr_gJE9+? zN~q>`3q#u0=rP$*!^OsoO7y;cUx4KC zZ0?~TPhSCDDvR0zSQu77)JcX_bPYISKTQd22?chj78O>SNz#B+sB7@22>On%{`1=} zzqv%>Qbg=078VTv)-0HTik22lG8QR#0l8Z$Z%zlE+HGEsFHF5pPwRGv|5k`_X=$2l zlPUd|vr(99fQ7|tY`nW!EFq>9kj}-^|L6F4!IC6UjJa39@UKBSbed{EI9d#x_R~l@ zZt8UV@85duKX<($Tl?j`oC4au?5824b|X+Ecwlh2!Nw=0V|wnnTl_g~Ca{l3NGPB_ z`gdO7$1{ihItLg8G5~oM$dz&3zxQ$N8Xy|{WWq9_6ssVWfvO;Y5Y>_`3Xd>Br8kLQ z4Vw%kG!^KGqNA6}RHVi~qf5#MOVOXGl9~3@W z3zowJ;0S0P5{E|mr>Fb(CBBM?(`I-LD{Z9zX0j^=rkYSd|v4>HG0CcBg9*wG@QeUt(M!BKJK7p#Y3GE+IAnN?XA1ij+~mQ|%3g zPuLUyQHz@rBN|TEKV07lG6FvX9H0Ddpy;k~amc~Q)6tS1y;G-d?(Btw-}6g^M}5b` z+XQlwI+Z1drvjtR-FJ>&Ehfe>aA|i!%}kL+q!I`ZFL7>*V9OsEs0#B697w&58pU*i z^e9UE3wtoPY;Nns|Zws9Yu-qE>MH^%a=exK*MET<7fT!>{FHk!3iBeTQ1;d+*yG>RZ zC3P9^Q-jmY@?Unheca}M&ie@|CJ+mnCF4QbMKS@^wJ3uDad?J5*}F1 zE3g&8tdAq|*q0G9-Q$9N6IC;H-`h$=Cyi&wla>wa3lW7tfJlK%Bs<7U&SLn;7>}aK zG)lDlgD~wu2|e~ylv(U`heXq~V*Js9={S%a9ugH5hl&;MEa-C(#Y6abxgoXDYG+K3 zQ05m6n(6M|$J=PTB>ehS?3$~puF{?Uxm9EXl``aWrRjxB+4pa)C7Z3&a#AM9>vDja zYWTEdVs-p%__*+P_xIZ94{1NKZvLP4x<_&5_VI**N79bjFTeK#vzq>fq6o=dtkxNR z;}T{*n9`d}K9>1Ge(gjECPFP)WI}i0BU+PVV(tzw4l;rLMveDqlcwu-ccPqW<%A5v zJh1x;ltTg_nduw*$<>48K$J@gc%fy@&4Z5mfO-J@M_y_M@FK>=UH(209t+u@LMI0n z%ra;plFN3yCk3A0q8pTy`%1sM0hJ(!ifvz z%bU5(+Z7PqzKJ=V#vqx3RHG*O11@4U1UXXzo)Z-|ax$EU{*A~M+{Q>#TOm7 z_W0rqm!K5hf_!hlNKM$%fPbZH)k4@LU3v`&u~spA3xGK7_-5n6BY|ZVsDLPt^=7-# zYM-HdjuDABJ*Sf{H44@ZkId}cLXnZ4XOW^o>5MrPz#xpGa`|W)e?90rr_4`Xve{{l znp_b0IVQYdTh~oK7xZ_@3J$h%l=||1I*pj)=vG@4bWb-mKE=-m`+`U2^@oreXQ23` zf~N0Mbm zV@S{`fpu>o0o4Uj6|>N*ZG+w@ND4y(UQSnwgWvNX>h@4$T`xIv%F5pWr%(`{OIX92 zzZ^AdS_d46zCE&G_@CdHFq1}5#-J3NQNIYj^EJq4r`K0o;!}8nT^afO!)#6&`&2i+M-1l ze_6D71-EY?j24+&NgE8GoB<(FI!z+N#_xFnl(ReR&iGSN0ZWzyre5)eNm^R37H1y% zk246DlHiRZK@>`X^YQuk^pERYDS=~Wv_TGky3hTEI0OuGiGEk_;W1pgZ_r=j(###o zdM~~8g9yhVP*jloU#ZxPPpNpqvdZaSi4-@nz5z1+ulMEp{GLj420d$Lm(L@TUvB1L+fetz?UH~a9H6Z z88gS!3U>cU?r}iMfgnL-L9il0z0k1zvZ$kxl?dMm4H98HcsurH!bpIXX!)q}a>Uef zH{wsdsu<{IX@Lz^N$ss|j@t^tuh$8uzs87*yDobo^L+N;_{u#U19HVR<#TAT;ih#X zXGO#t8+B-w2j!jQojp3P4N`STLH`=mY!m?$XTot1E`k<+eE7}H%odvF1SS^fev}ym zhUUkS8)?pJ$olCq+_obhJot=hsA&l7^AYq1M;=Q@8($0PgxDD5N9haX==u^fo_i*? z$57X2EIHYG;4uY*arVM?4flsh{;ep{_t|+J*Nbcx(-v3cdSqkwjauax345`J5vXNB zHv%6#kTYvqD3v!%<15f$s}1d6nYumtg)k@lZS9QY?bsA-AbBKq?Zzjf%)paKepfTS zR$m>|Q=^xo%Gtu{f{d!#zHyDXBiE~wpHcf`72Q3|PYuP8fR|{J=;BHi$#k7BXYyx! z$uU9%O8J)m92Sc)80q4Smp}gi?dX1VYG~13vaOdg<+WE@p0iU$7{(ka_8cp<^(s&L zV2-iRp8B!?@ye;&(=9hk2WfZ|k(x&8gqi4i5Q7*}Mmr+9^Uci-kS{}e%zIZ0pTi1u z(`|94h%F=-;9gc=axw5EzUS>EN=P(2|AAZzUTpRDbiD^1=r>E)N{FfGeYhOB(2`PH z7+0%*lD7IGQ7rjAwLg=rdt6Ra12*NN-f34)OvzTy#RWuJW1~g1gVOMpt7UL+UV6yJ z<<5afTaG(ze1)T>0)h(a9K)PFH2ZzvL1$9M09ApbS4>nL zClzo@gH?|?(QAfizxm8~Z*h9P(semM9xFM&-(nHtZ23BZo<=qNBl3goR-z*ZTXnRoeY`^^s3w2{J_p zw8`5Dq|yd~D;#!um<|XOmS+;J_hh~?5gAjAs~lIK*PnQZkZ(kag7>K<&{d5=09dsc z;$`PFez#HVS_Do1hT4$V+Nz}>28P6ez^s{xhIETX5cYW)aIbEfo6BxZu1zj`HcSc4 zI?7kBz!;=;Z;y8c1tlS7hW5PK>mNypRp8ZLwyw1(iFgM>tB{RO`915?X_(qR8(UxN`?&&NyO+Yu=;s$$r` zqr29kBR4ETP{Noh#BCtq#R+ z`%nyYDejX7q(mes!C)a~&a77b(%`L~6cr$zD}LZ@O1H}HCI@W@i|XUQHHww-WlckU zOOx}J?>CwiK+==PaT+RPHJiL@g&Pl+PnLM58n3ALQhE*0$H{ry7B%qcg`l>adpjVz zSFbT+rN&_Balv3U1yUftU2&Md(=Ex4qw-3VlU)dp+0YsTTbRH*)stwJ-TO^LM|O`Fu0P<|zxJ_0S|jKk^suvWqt@~z>d!1KK?-LU^6TMS9tO1;{1swO3?Ad184UK9qXRHJQ;dqz;62X@%io3(~*wbraVWui6 zu`T2-VEtK?>+4+bay5D7c>dEq$?<%ZdJ3f9{5b&e2@ZKg{GXp_B|j)Dr-yg%QfZbP zMYhGX(WT4dFZBlsh(6`?LcvM&gHe(zy4n&7z6wjV z^r2k}e)JGVG*kD?EnrAiAOVIcL`EW8Y#Dg%ss0!7t7IU^VjPLj8k*ww;RG zI%gN-`3RK0D}H32lmM%sa%kN&wTrc$$OsE0EOoQTf-E~`P;;ljYEU!m+gc;HE;3eU{?HQ$$y_tcvE*R(6cstC8^}bP6&d_v2KP2jL5a zDn#K-0wR2zI|M%9xwW*%XUUP&Q1zv~Q^U-@$<}SY)ksyS9y1-7FB@SZo~b%vaU|2y zF<__tieH8hC>vpj25iCRuAmaCvc6(d1#p6?rNbt8hmWzf$FEl9%pSLl*Va)YkKV`RTePqCCu;BKxMrLREI|86d zAi(u$hGX`RMPa~D_nYtB4x!RKQWh?x!)x+)W>gTbgnqDF{8F5u@TYj+tUau(DhhHlzC zga@|XOGTxGBTD;CY>Z6mh&WvZE*jh;uZ<>pI80jV5(tP>5N=o`nB6-uL2D8)XqM(S z8RiA$QjJ{ue%}f`t+de;<5@9_%e3?ERE%`CNF=vn?j|Nae3lFJm=F@^o;*=gX2=GX!{wg^7_$635^nmC z{{fQjHZkyiCJE?+-U_7(QI9qwj|$9K3-4awcKAb+Y4t33=i~_D5Th%uQxJ90f9!hC zbNU?Q&$uEdkF}JTg)V`lUN#FiEhP2wWVp-#T=jpvgUe;?I9a&Q{bQmhBCW2V!D-UX z>@>itSfp-MP66SDNG$`16r@yb7km9-qH%&3J6d2msBS>nNbz@fclg3Z&%FK3zw&mV zxh}aJ*BK0iyG}Gj$j>toTXg*0VRwEo>RO@1I%pBatS+5DLL~`2BsZvB1mUZTUV+qYuYPt-}@+kl}jb$aadiEo(UTP*2U(_gw{%4{{MwB#kDy3F>_UG6ua!%S!P3_f|>pbepmO*ba>7B@)TAnR|Z&1U*!W zIa@1uH-(W&p09)O*=)7Rebus#ec0bE9HGAP&H8{I5VoqZV?TsR9}lSQuYF% z0gQ&r)-O{r2IKqG^t5n5f%+I=t3}X&p5|56W7E*C$^_1zE z=R7eG)`*&dSPf3*20}2C6~6l$SwZo4WkF?G018TQ;k~XPD%v=wWG=X-k}C$eZ>jzh z!HB967i!nsWs5QW4PDK&gIxGL=~%cJ=-S+>M=y#Y|&#c-=BN#7P|uE zxY-k)=ORg>L(zP^C-$hCZAN5Xu8KcldAt73@>~C1Vf;9938o}b|5NiMre9gWHYjpL zsFDyv2B#(0%h#8xVOLVY`=A=eOj|?@>U{0#t>aaCE?A@Da zMsawSYNKRJ26b|LIT+kGX^xdj~u zCop4%4!WIQBRruxta9ily}%Gbg@ihGgw)Rf%hb5ojs2ChznC0oB?PU6>FN)|i;BM{ z)96ndq)4#7;rmz?_#pXwKIl4@9vb>fsN12(6L@wYi)e2rfxG`K63H}cci4@JAHp^I z)0PXY-qCG$P&~yTt@8N|I{Q^!o{^RTH0#{m+F8fe^svS=P7iaJU4gF<{x}iueC&Na z8QjJBRTv7x2_o__OarK)E~R{s_|-Zgx#h2tr}9ZkRjR;(FrOp~CRu75k{WIV({xTE zpdQY@rbaxV{qWXHj_ZeNotHBo!dkZb zC$m+$ji9t_ub$j{rWQCM2w6;qOm5P8v0Qe5^@IAp`Ue)KQqvg-qrBGF{e)P*Y(E|z zosM4ghQZGu_qUT&->d%?NuSa zE{L(@-I9>7MnmuL5R2eBc&yDfo%)`rDu_3+2`_^%GH}n_lbpw+TS(CH2XxXL3Uvi} zDw>$O|IPK7+x@Wg-LR_SLSmsfR2YlI_VF@qO&eLywEOodeg+1y#w|1(uw*aISj5ZALp6cDPUx*TxN{8BI(WaIm9DlfW|H0v*&SGPsrsd%FT@H&3JeO zC$^-&!Qos7U)VTAuJ`bECCScgc}Ci~!v?r1${3Al>_mDyWrs`(_j|YMnK%f+`up)- z3gMkkw&C*9TdQIfuC=1I^_o{&!0|mwZGd9zOy`1l5QUCD>$8YZ_>hK`ne^T5Y*zsJ z-tT~nfIUF8vtssThDBRhlAus*nj=qKXDj+ZyObX`1R++bjp5;?PUFr;DK&QWriwJL zQ_;lj^|QYZmvKlDoDkb0bN2~(r^lJvE(L7giRTL8{8)Bxv^}_hdUGx$M_Lx%{4JvCu!6jcpXVzP4sfBwS$5i%^ku6Yq$_H%mr|obU8f z#W2`oEul(_rv7XRyF&=Qojbi_<_TD7xwl7lS|1n~lTpnp2N)%-sUVOOS0gdmQLf0W z8lHnfknhi^ZDYD$n%A`Hjg+eOk~mM?+Vdc)`yB4DQ;oWx+dkPR@t$twwa>GGQ3*0Kv``?9&44_$Xqr% z%EeBye{JSW7$@#+fbd(?cMi?{eFW_)d4yRtC0ezEr^ZQd-1*(TKB3Z=jgjDC{xoCZcO0|=6Jm&!{`^-#eG5rwRVX$zWfkP4JH@6ktJzF&9= zk(&vcFx3{tsQQL%u==$%%zTPWhEmzj4w2E|E&T5KWC)1&z6|D)XxmWsCT-HYQ}3AT zd-JS{$JmGOn*Ff;gRZn6Tx7Cf?vTy`!A0I*pIEUea)}MBoa*hCgL1z2c@*3lPw+PI z0yTv4pPr7E=HG%$S%wgn@?Ge0JxzN8xkcX;PKy_PBLHei^2+>2?L_c$Fe%QTLsG{p ze3?vRrN+jdsRVyw%D)7{v3E^p6+QiZbblPJlN^Q9!QhqlD+A1v3=g9Z2V=qSN9&hF%7$`` zr*ioKo};-&)P!@~ue7ij@h#gmrRK8f3yX?^ROA9|bSm6<(Kw43%EO3x6fO9M!~s27o&15S`>RI5m8n3{gomW4ft<_1_n#juCiz!9OR}WFh?C^oj|J zAQijSCMKwbi38Kgm(ludvz0I-G|BtIBg~O749CC1yo?eCL$mybjotHG=tA)$h5=-Z zjRS)H{%=(=;MaJlNAUV_@llMU;)i2C=FSqgJW=YS$?dM`OoL53x1gwmS7775j)+S$ zERP$+dIIjIK}Kl$_pUet%WPtIx=+7DEQ?iYsO|myI~zGZS*!-068dC(91g{^WfNmBRJqScUUW#^k+>0d$Np7o$8oWb7rj%2#|z#TwRkphgr)C*{p zVslynTg;hNg3WMHFo;H0h%p}_dBE;6Jm}k5;8o^TRMaC2v1RxH%X8ct>7iYoKRj){ z^}b}5`zOQcJEzZ`Ri(MKtnEHGg*jof?~nmLYK_Nv3Y-(6kQ)d{tD}|WhkVLjt4X4k zb$38VkA{WCg5l3qS8>UI2DJdN&z%pNIj9O*%+SYM`KiksfCSX~T4I+tMC{cw3mT~; zqN2)I%SWJgt#kEJrBXD(Y1MEtC_&g+udwg1d-L+SH~cIk`QwH`)dUZzjWTSSI;HQ9 zcyl`6qlrITPzF_k_uOf)8Q!mUL<>ia;6{aUSzJbU73Ye~Z*8K?pD=O!DD@pr-1XxM zrqlcd5!*YrlnC9eh!*@II}G`-oW(+GlW>AG<;!HSvVMe_Ezd7AzBA3gg0HFJVjg%J ziP_jxg{pwn@X>-jlS_+T+@}4?F=JY$72szsjN(j42p*jpp;S`0YV{Y+(yBrB z_u4aj9;`pLTe1RPBexf?Vk2S?L)3%AaB85h60)-Jb>U2dfcRpSd+f+~T5Yq}EM-Ii zqFBNUsPdV`n-l~&^d_2`M~(`cXn?b!r8)5N6N;FdA^1~?v*m^iSm~PJZ~z;UL5Ynd zxx`OK-Bvj-O#G9fZ1@!X3h;MZs_YQ;thR$EjP_*{{as0HF_?7%l92 z9&TKPB@$vDG?6MxX@y(8gjDeb{q(#P`cQ$y=;TJSoCZot^0^%YGBzB*4$?FYA9hbpU}34W?fL=RF#T}%(P23`J-#Ie=*Lm%@!-*X z<}6}jH%~;X;OVT!Ud0-T*sv_*WcU`68rFY-N%$^DjKawxH9*-OOEVlv(3Oy^& zpif%R^tz;Fxu&GvYl=FBdIl3<3&VuL6}Xqw{ zMj%QL*$3U|jPgXEtIz&FZf9aP(u_;~~zicr^dd95*h2S0H-Qusnw+R8%!*TebNs(&|Xl%fl zrvnMz9RvdxM2}z^#CWs(lgTffyXQfPyXRwsQ|^QhNi_{hlrs{GxgX<)Md1jiiwVL< za6;5*DpT->x3C^@&j&sLzI7Mbc$?&^9EhjHo!B$MH)Pd6yD(Wk)TcbtbXfNt4UH0# z9eZ#-QZ|c|opP-!QCPN+$>0k7cd}B{0@x6pM6Ynw4l)pFUNzClu~({Kn6S8F_fe%oLG;VsHKQfiO@!HS8mr^a%4&jE<4RmV=LXoB z9(}qE+kmR;s>sNvC-gJ3Ii7_qxFwidVw3}Sf42=~V2a=TX~5Ay0y)1gnbPyqjl>Ge zQQ}!LG_S-Pq;xxpHfB*)pLKPna_|E_-j|y(o8sofp(b{I-uj*O++y zd~m=kIBRwui1^@HjG1)#*1&V5bQm*#z)@#aVYpqb?L$2$bC~JyuH`xlZSK-A{>(iH zHYx;4FRe?3ON=bIEB8}MPsEI6FhOa75_!@MxD4{xnJ-z;b1m}Vjhf{`8nz&|x~C_d z_;Mn(I6gb7swCZLAA%$n5a@OiRV;D-Ci%QFEDcuG3ONX)l2eZET?MY(;r4t6Yjc>5 zqh8w(lP{Ro}!qb#Z zS<@mLDC2+#61zCllmkT~56b+_`e8?RG7YzP{^GPcO)hoY7JQs<k)1;z74DSHdT?ul?C6QgPcApZtO-a)-SS2PZv9QGmQO zI>tu^vkOKkmT6`woui&r{m_`HF#ztLt#3s;F z)S6=|r-1(!?w?-R{AcRlvKU-px2r@as6=BgGQ-SIl+MFeJQs(S33+)UWYo8q;fGwR zDx%iD&0s7R_GJzI^=#?jhb>S-nA(un@F;108~Jv>hwO$1uEe~l++gl-DtVT_nI-}PQ4)@ zfF(nIz-6>Jb&INWx)los#?qWa)>Kxeh7~mk`Z68b_Ll6?TVBU6X_(Gpr&$CFZrqNU z3{oVQV2UPNHmHy~v4*8q=wAIu?_YsH06{O0`i5(gn&+x zd~|(j;C`1*>QYS_gpQ&944Ryz1B&gj=km7|_X$5D_S{Sg|FiE8`rO|G z9l~Z!f9O{gUcOEP5V?LyHTycw>2ZxKFQ87@EHO}+)yoM>0v{V!a~PT3pVErHmz*=n zaXT(#y@U44gi55XD$4fzPl2y!EgB9@G)=K`v1F_z)_2=oEQuw|{H~ixdh4&VN*peC zG@U*Rl$n5GJ`t8%1o%>i<%w1I<&~FEEej~IEMo+=3egJsC%s`bbJ`n5?OQ(Cnex|G z@daATj5(5zbbX4+sbS#dW_6p|a`{!&=S{2xX8Z*hXKhe;`97^vT4Zd(NU02$8Hz!E z-&jYCK)Jz8RXe4Un9o_f1AA-Q&0N8m{Gm-{r-bJH%I^1#^OaXC}vUu8`?Z=^sB6xz6qk2))BqAOhLyWQA2to5VqI~n958e{I-<4eX|-UFe{up zCSYDgd1PZ!_2QOz;wu-EirN0B71FRJHaSsCmzf%}Mb5*wWFl_waaVkcD>>f?f;dwSKTiOWUiPyPA9z>JyguX>uB%SL;E1b;gIB z0)QY4RvT=BWeUcrt!{0qzX zT?L~E`KgDlCzprNMkx{m#*$~e7ze!oPq%C*UDVw$SvT}%K_L76+QaknlqZXkSHVJx zG@w^`f|fRWwZ>r4^6^6d?)}n`%sX*J0!nPf=zD)poW@#_F4;}=+Xx6VRU&>u^w#?c z=ie_4bCVVLRRGBVe3&iYv<8NFYj+}F0Ad{*BU-DJX5OY99N8k`_qD1&)X6r_wwm2S zU69Ij-}&1_pI=LeD_>H^rESAfXvdi{7dDx5(q%k%wK{$eOhx=cq z3f^n~n^*ENP@@CSE$$E5^N1M*2&qrJtN2;&58;26F_+i2ZQ6OPQp-*^Fj_QCu&GCJ zQBm2z!Gni&?A&?v{(bxUe)j37BHqVlXUqHWbRtDBlUcsxPu5}Q4m+h*nNipv(5F-h z>5@v)lZON$xbgE?tRo58RqL)RF)UlfnQ5P3>-ULjeyA5Tcta!=u56#*8D^Inz92~w zP=`=K`Nxmb`45NNNd z280zHh!nTENB9Q1{z0XHNCOb|9Vn5D3Z8+wSXO&GnSTuDdn`Fqz-SRu7UcQh!2@Dt zG~!CHBW*B=8C^p`Q_D^=Y%uL4mfzX7Xjr zQ9T)!>RlLUA1M>8>TOl6htnU}Q(K&dOFRSQ54}Al&f)gxjB?ALKLUxN)R*$?S~lL{ zvy3{i^v_=12US^HDx)k)davW}=*(l>>0WIb>+r%dHsvSqm{bL)VV-D|9Pt>K^Vdwg z;>=nrDBQt=4I0?rzZp%F&0!vJp4=<@4;u7mWRidI{(H21*)kFBZBpAvtVESgT7C<~ zqx}uWl?~Us%6F)hfyWBP<2j`Kk|l=zE}#SHp{xyMwxY}unO?}LkW4eTI=|{+UW0a; zZ1I$;o*9vmViKDPH9b6VF8SA|&TTK?_G$HmX8=%ee}?zK=K@Pghvr}4F&xd;sXCc) zG}-TP;(fhl)tQ$ZkOJtRamOA1LpS{3M(JafyYGN<>V$`%kmN8T8KxrZ;v2SZ+4Azj z&pwMpanP8tV`X|#OrTcOwir&4WV$HqPM+Am+q)5*=ys_F7_!%Wr_S{Ezdu87zqz2&*(CF z5}4q?Em)M2oJvcVE)nw+b*b3eJA2$X6(`a6f4Bgg zq>5LYmk1i7W1`y8AX)ZuF=>y6T$=t}E)x{nNOp%r;EgSp4GfIw z{5)Ii#Yjy7hblFV73=Qlr=E}&5GSLOPANFdF>Kf{dg95arI8t|4J7d($Ydl*ec-Tk z-UEQ3$Y13>EZYCX?}sVRX}CKBJpPt_CLA$@Bso*fcz(RQHiNCQ*ez$HC)7b_N zbD-*d+O+93ea0=+BQt~c?LX*OH}!!d(EZP6dWL24U*_;YT?;+|p9iw0R7Jk(MhJBQ zvS*TsaDHr%hYav2wqy{~Fk6O#8{oSzCI9o`hcqN>DE03@P$V4UF4#I7% zRm-dERM=`ke+|_GtgYe^IwMYj)GdGjaIqj@fgsYmdgTflIA}0WVrMF^DEl-%F8+bI z`1nmOTiCs+;IONbC!hEWefYuq-?1dmT5y;HseE_eb1z+g{SBgS)b^K+gTua>7z9_= z*1CR|kdQdQIE$c0Aev=kJ(d<&;Xc_3I+|8sZ+X1~blbLRH16^V^brppCK!cFsAqlS z^?%7~d-B=mq-4|NM_4-L&yY;Zw6d01kV@Huq)16kVUQI{oJKnQF}J{O2apuIcYQ0l z9Qe0i{{hsYLnq|Z?yaaSdn7hG_Em>Jfhp{yz@f-z44<*BV_24iE zGQpqWiMV|HILV(p%w}o{GNN>7x_)q|tZpRWv`tIDmG$?%{P%tU2S5VC2Z;oNr?%^e zQTw|bIwZX3xa?_CB*jxBM~#*!F0#aSZr?6y11W|mu)^PcrmvWd^7?Z|wL=SFOp(U6 z9Kl}7!;VXlN4y9xUZCZN!y)4=O#w7I_283nGB+QNxO$CRM7zjB$ z`^-}W|4@BCsQ(&pnB&q*FQbbt8cx%v&yWVMXljpLsROwi-vtM<$VypCB(mhuK8mS` zO7oAGn#Ob>k5CQBddI59=W_P!rK5RAm?s>i^Di7Oop-iw*(yLuYulE3_3o|B;3`!| zq8(XkaFy@E<5MlUy;{{_?|4K`;nB5pfCup!n5d|`L2(vb?g{bns~CJQ@_o%SQb{H3 zGz}c4<^{b?0A`reNH%)%iNDgjZ@<-`gYv%)9OeKpe)z+gG-=Xg>Xy+>OgtnDn{4x7 zaDYc3O^$SFq}KzFDGlXM+{CAw%p9aECJJ$xHVh6VCaYa56I7cZIzTmmPNEh?mWii_g&^GuQJG1SQ4D9 zCDWs;Dc~@9CTMnpJHYm6$B!M8)Q0Wbx6#7SKWo%`{a*_XbHJ*>Mmm$PnM?x*4-q|W z=Nt8b16_Sdl5oU?bnth^GYGC=v45G{?LJ2~$MY%vAh{#jWe$!2M;0auRtx}zz1dLP z+N;+&!c#zGp`xHd3{a(B7HXxsk|L1Gr@gZL_gn_%LcWiW__GTcG&NG^t5aFrI*NOu z;4r2yx)I|0>(+ilAOGh=NqKB)Xyd;g9OeLjc>0;==;oVmmVHnH1hHXo_^d!ixL5jj zhQX@o$AdqbPn%UyQIW+eVNfyyLAI*!S<=(PSWQU&S+jZ-ZQ*MoP6Ly+TlWmQVE9Es zZcPltvVCXgylN(R#FkqxEP7N9%D` z9*rg#vkksD#GjY)>k)oo@9@6{y@#!#U*^{geoYVYcO0S4c}eSZwAhisuMPZ4Y3M_Z zx-qPVYB$uElle8D0{x-y4m!mceiiHY<_EpExSsFeobTGcliWI=Eqa*wn$fJeKLK!< z$ut0%7~(pQ==U}R{XJK|NA-Lsw{;#{{7AMf?(hNAl?a$6y;GGa3+O@7h#KeNv+_;Itkmf8^@9YE-o}HusIjR)PX+8}x-A z4KeWj3qA*S{T98=E&{Ivn16xd`n^s%^}b8l#b zzbtO#o+XF)6L=yA#%5C!uVd~VllpKo?stYhm;JuqZ(uGOKbj~x z8Y8B6>lI)%pgJ}*bURIy9OitD@qm+&9Mr&bgu2i94P5_`MpUhlxW&zo9Q9d$M$!*H z)sn*?-AIUFvh_1fR+rc&&wf1If>OhnY?JCCz0Ez$U@i$-jlq*TMyr#F&A8SQn`9k) z65k)A0sJ!!bo+}=R(hOle~u^g#LTc+90mw4>&Y6UZ}xAt-;-NN{QuQ9h#3t`UQ>?@ z+a%4`_kB+zlY*mL3!3F8bDrs1r9Dy4GtJlFz2{V`m0)5zqaIS28$^cQZ9uG^j9M9N z6U7-Vq*mUwt+=}zn5ag&H98jg3ii0|r9yTns*y)=DU+*JAxZxgo^9Vc{7K znN-c-9OfDuP7&v5%6$%QVC6Rkj^qZOy|FR5yB@9*%G95Ky1-#4ruEtP#WsMYsAsERF6$b6UOAP z^+=gZ8nNu`$5NjA1`UMPqNdiP-lH{#wDo8ON9)lFj@F|Ej}ys0Kh^h;rT<;`zi3=j zPQWpBR$RX!&UqrSm02PFwkX78PT^Nh6DN6={>~Kry*VfNy{23X12r?m-!XV|TX<4% zU(fSvsvaZz0+_A;Hs=(AB3oa7uD*_W_LIBj3HmwK>IcV>sc&F*h>4!k#LKU>O<+nB zK{cg`E6ED_eZeW3poMypLf!MMW}dr918qSA=dxcXv$5xF2`5;n2VT(3ht1aKJza-Y zk2?Er0!Nl@(o;?V9L*9A&Dm1pO)Wv@=)uU=e_I!%j#Bhz>^d-0ul!8?xw%^W8C{vB zMRTx9IGJmOI%e8b%1nK}IkxlMq0eFN(SEG6P0}3Ovrv%u&a#1IPCY8fO!7MYJlXp1 zZ`!^;SO0FtEnENlK+v_O*zWnwpmCWKbWiqcHe0RKw;C+l-`CoHwu^nEhZ~a|Q?%H$ z5B5Ua3Yl#KN>i_8?$xZSy&+VNIrsCnL7i>8zWtgDg4C-0b7rivf>yA7MP~=y=R(^v zthJpl%Qhxiw(**2`+iE$IL@|RXNv8;;LqVYnWsu}gd%vsD|1#8m~*j4bB=yx>eo!$ z!!`F>b3%OI{A?sij_th%Y!kc4Ml$EWV@}XHvxB}LN^00w?Ofa6iuHBOc$oL* zXo+U{S*Gn=i|QGh;-G8VrAba>Jg+(6$h193sQ2bJ)0{KspK}7AYwES;+P<4}0(u?; zJ1j`Gth1@Yxgmf)TmL<$o_h}de98$dx+x+4cA`W+rHK|{vz%bF8@SNAAg+*d66ZI% zJ*$!Pob0tSPk@xBXz_iAO-k80*Q^keW$x*L22|X-AT^y*&muG6Tx0{{oFKsEgg95I z!?+AmPIRnJ)X$NnZ!Rn7VeF*Nd?rUf!1FdSpA#fTQ-Z#;DCqaOHmR||cAZ5wz%=Dr zvqJc6rfAYT+x85LY^&4$bGD5~%(Y3MSwYvDVv{s?=yNUzI@dazTAFE_+y_EPh%B3W z$+8j3Y#TW>Af0U^^G51s2aV}0+x<|Zex7FLb2PT=35#Oxwg#Jr+<_(3oVOpufm|j#HZK^MCosuE&BqReq9FyL=k5;wPdbEP0^=Ji0>(PS8{}*5Ym}W~g TrhT-X00000NkvXXu0mjfGTp(M literal 0 HcmV?d00001 diff --git a/tests/selenium/selenium_settings.ini.php52.sample b/tests/selenium/selenium_settings.ini.php52.sample new file mode 100644 index 0000000000..ad21037e5b --- /dev/null +++ b/tests/selenium/selenium_settings.ini.php52.sample @@ -0,0 +1,23 @@ +[browsers] + +firefox = "*firefox" +iexploreproxy = "*iexploreproxy" +chrome = "*chrome" + +[SeleniumSettings] + +host = "localhost" +port = "4444" +wikiUrl = "http://localhost/mediawiki/latest_trunk/trunk/phase3" +username = "Wikiadmin" +userPassword = "Wikiadminpw" +testBrowser = "firefox" +startserver = +stopserver = +jUnitLogFile = +runAgainstGrid = false + +[testSuite] + +SimpleSeleniumTestSuite = "maintenance/tests/selenium/suites/SimpleSeleniumTestSuite.php" +WikiEditorTestSuite = "extensions/WikiEditor/selenium/WikiEditorTestSuite.php" diff --git a/tests/selenium/selenium_settings.ini.sample b/tests/selenium/selenium_settings.ini.sample new file mode 100644 index 0000000000..bacc0a9049 --- /dev/null +++ b/tests/selenium/selenium_settings.ini.sample @@ -0,0 +1,32 @@ +[SeleniumSettings] + +; Set up the available browsers that Selenium can control. +browsers[firefox] = "*firefox" +browsers[iexplorer] = "*iexploreproxy" +browsers[chrome] = "*chrome" + +; The simple configurations above usually work on Linux, but Windows and +; Mac OS X hosts may need to specify a full path: +;browsers[firefox] = "*firefox /Applications/Firefox.app/Contents/MacOS/firefox-bin" +;browsers[firefox] = "*firefox C:\Program Files\Mozilla Firefox\firefox.exe" + +host = "localhost" +port = "4444" +wikiUrl = "http://localhost/deployment" +username = "wikiuser" +userPassword = "wikipass" +testBrowser = "firefox" +startserver = +stopserver = +jUnitLogFile = +runAgainstGrid = false + +; To let the test runner start and stop the selenium server, it needs the full +; path to selenium-server.jar from the selenium-remote-control package. +seleniumserverexecpath = "/opt/local/selenium-remote-control-1.0.3/selenium-server-1.0.3/selenium-server.jar" + +[SeleniumTests] + +testSuite[SimpleSeleniumTestSuite] = "maintenance/tests/selenium/suites/SimpleSeleniumTestSuite.php" +testSuite[WikiEditorTestSuite] = "extensions/WikiEditor/selenium/WikiEditorTestSuite.php" + diff --git a/tests/selenium/selenium_settings_grid.ini.sample b/tests/selenium/selenium_settings_grid.ini.sample new file mode 100644 index 0000000000..eca60b0a20 --- /dev/null +++ b/tests/selenium/selenium_settings_grid.ini.sample @@ -0,0 +1,14 @@ +[SeleniumSettings] + +host = "grid.tesla.usability.wikimedia.org" +port = "4444" +wikiUrl = "http://208.80.152.253:5001" +username = "wikiuser" +userPassword = "wikipass" +testBrowser = "Safari on OS X Snow Leopard" +jUnitLogFile = +runAgainstGrid = true + +[testSuite] + +SimpleSeleniumTestSuite = "maintenance/tests/selenium/suites/SimpleSeleniumTestSuite.php" \ No newline at end of file diff --git a/tests/selenium/suites/AddContentToNewPageTestCase.php b/tests/selenium/suites/AddContentToNewPageTestCase.php new file mode 100644 index 0000000000..a4dc5b7e31 --- /dev/null +++ b/tests/selenium/suites/AddContentToNewPageTestCase.php @@ -0,0 +1,182 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + + +class AddContentToNewPageTestCase extends SeleniumTestCase { + + + // Add bold text and verify output + public function testAddBoldText() { + + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "//*[@id='mw-editbutton-bold']" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify bold text displayed on mediawiki preview + $this->assertTrue($this->isElementPresent( "//div[@id='wikiPreview']/p/b" )); + $this->assertTrue($this->isTextPresent( "Bold text" )); + } + + // Add italic text and verify output + public function testAddItalicText() { + + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "//*[@id='mw-editbutton-italic']" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify italic text displayed on mediawiki preview + $this->assertTrue($this->isElementPresent("//div[@id='wikiPreview']/p/i")); + $this->assertTrue($this->isTextPresent( "Italic text" )); + } + + // Add internal link for a new page and verify output in the preview + public function testAddInternalLinkNewPage() { + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "//*[@id='mw-editbutton-link']" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify internal link displayed on mediawiki preview + $source = $this->getText( "//*[@id='wikiPreview']/p/a" ); + $correct = strstr( $source, "Link title" ); + $this->assertEquals( $correct, true ); + + $this->click( "link=Link title" ); + $this->waitForPageToLoad( "600000" ); + + // Verify internal link open as a new page - editing mode + $source = $this->getText( "firstHeading" ); + $correct = strstr( $source, "Editing Link title" ); + $this->assertEquals( $correct, true ); + } + + // Add external link and verify output in the preview + public function testAddExternalLink() { + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "//*[@id='mw-editbutton-extlink']" ); + $this->type( "wpTextbox1", "[http://www.google.com Google]" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify external links displayed on mediawiki preview + $source = $this->getText( "//*[@id='wikiPreview']/p/a" ); + $correct = strstr( $source, "Google" ); + $this->assertEquals( $correct, true ); + + $this->click( "link=Google" ); + $this->waitForPageToLoad( "600000" ); + + // Verify external link opens + $source = $this->getTitle(); + $correct = strstr( $source, "Google" ); + $this->assertEquals( $correct, true); + } + + // Add level 2 headline and verify output in the preview + public function testAddLevel2HeadLine() { + $blnElementPresent = False; + $blnTextPresent = False; + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "mw-editbutton-headline" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + $this->assertTrue($this->isElementPresent( "//div[@id='wikiPreview']/h2" )); + + // Verify level 2 headline displayed on mediawiki preview + $source = $this->getText( "//*[@id='Headline_text']" ); + $correct = strstr( $source, "Headline text" ); + $this->assertEquals( $correct, true ); + } + + // Add text with ignore wiki format and verify output the preview + public function testAddNoWikiFormat() { + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "//*[@id='mw-editbutton-nowiki']" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify ignore wiki format text displayed on mediawiki preview + $source = $this->getText( "//div[@id='wikiPreview']/p" ); + $correct = strstr( $source, "Insert non-formatted text here" ); + $this->assertEquals( $correct, true ); + } + + // Add signature and verify output in the preview + public function testAddUserSignature() { + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "mw-editbutton-signature" ); + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify signature displayed on mediawiki preview + $source = $this->getText( "//*[@id='wikiPreview']/p/a" ); + $username = $this->getText( "//*[@id='pt-userpage']/a" ); + $correct = strstr( $source, $username ); + $this->assertEquals( $correct, true ); + } + + // Add horizontal line and verify output in the preview + public function testHorizontalLine() { + $this->getExistingPage(); + $this->clickEditLink(); + $this->loadWikiEditor(); + $this->clearWikiEditor(); + $this->click( "mw-editbutton-hr" ); + + $this->clickShowPreviewBtn(); + $this->waitForPageToLoad( "600000" ); + + // Verify horizontal line displayed on mediawiki preview + $this->assertTrue( $this->isElementPresent( "//div[@id='wikiPreview']/hr" )); + $this->deletePage( "new" ); + } +} diff --git a/tests/selenium/suites/AddNewPageTestCase.php b/tests/selenium/suites/AddNewPageTestCase.php new file mode 100644 index 0000000000..5199def56f --- /dev/null +++ b/tests/selenium/suites/AddNewPageTestCase.php @@ -0,0 +1,65 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + + +class AddNewPageTestCase extends SeleniumTestCase { + + // Verify adding a new page + public function testAddNewPage() { + $newPage = "new"; + $displayName = "New"; + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "600000" ); + + // Verify 'Search results' text available + $source = $this->gettext( "firstHeading" ); + $correct = strstr( $source, "Search results" ); + $this->assertEquals( $correct, true); + + // Verify 'Create the page "" on this wiki' text available + $source = $this->gettext( "//div[@id='bodyContent']/div[4]/p/b" ); + $correct = strstr ( $source, "Create the page \"New\" on this wiki!" ); + $this->assertEquals( $correct, true ); + + $this->click( "link=".$displayName ); + $this->waitForPageToLoad( "600000" ); + + $this->assertTrue($this->isElementPresent( "link=Create" )); + $this->type( "wpTextbox1", "add new test page" ); + $this->click( "wpSave" ); + + // Verify new page added + $source = $this->gettext( "firstHeading" ); + $correct = strstr ( $source, $displayName ); + $this->assertEquals( $correct, true ); + } +} diff --git a/tests/selenium/suites/CreateAccountTestCase.php b/tests/selenium/suites/CreateAccountTestCase.php new file mode 100644 index 0000000000..55486cbcba --- /dev/null +++ b/tests/selenium/suites/CreateAccountTestCase.php @@ -0,0 +1,114 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +Class CreateAccountTestCase extends SeleniumTestCase { + + // Change these values before run the test + private $userName = "yourname4000"; + private $password = "yourpass4000"; + + // Verify 'Log in/create account' link existance in Main page. + public function testMainPageLink() { + + $this->click( "link=Log out" ); + $this->waitForPageToLoad( "30000" ); + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + $this->assertTrue($this->isElementPresent( "link=Log in / create account" )); + } + + // Verify 'Create an account' link existance in 'Log in / create account' Page. + public function testCreateAccountPageLink() { + + $this->click( "link=Log out" ); + $this->waitForPageToLoad( "30000" ); + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + + // click Log in / create account link to open Log in / create account' page + $this->click( "link=Log in / create account" ); + $this->waitForPageToLoad( "30000" ); + $this->assertTrue($this->isElementPresent( "link=Create an account" )); + } + + // Verify Create account + public function testCreateAccount() { + + $this->click( "link=Log out" ); + $this->waitForPageToLoad( "30000" ); + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + + $this->click( "link=Log in / create account" ); + $this->waitForPageToLoad( "30000" ); + + $this->click( "link=Create an account" ); + $this->waitForPageToLoad( "30000" ); + + // Verify for blank user name + $this->type( "wpName2", "" ); + $this->click( "wpCreateaccount" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Login error\n You have not specified a valid user name.", + $this->getText( "//div[@id='bodyContent']/div[4]" )); + + // Verify for invalid user name + $this->type( "wpName2", "@" ); + $this->click("wpCreateaccount" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Login error\n You have not specified a valid user name.", + $this->getText( "//div[@id='bodyContent']/div[4]" )); + + // start of test for blank password + $this->type( "wpName2", $this->userName); + $this->type( "wpPassword2", "" ); + $this->click( "wpCreateaccount" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Login error\n Passwords must be at least 1 character.", + $this->getText("//div[@id='bodyContent']/div[4]" )); + + $this->type( "wpName2", $this->userName ); + $this->type( "wpPassword2", $this->password ); + $this->click( "wpCreateaccount" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Login error\n The passwords you entered do not match.", + $this->getText( "//div[@id='bodyContent']/div[4]" )); + + $this->type( "wpName2", $this->userName ); + $this->type( "wpPassword2", $this->password ); + $this->type( "wpRetype", $this->password ); + $this->click( "wpCreateaccount" ); + $this->waitForPageToLoad( "30000 "); + + // Verify successful account creation for valid combination of 'Username', 'Password', 'Retype password' + $this->assertEquals( "Welcome, ".ucfirst( $this->userName )."!", + $this->getText( "Welcome,_".ucfirst( $this->userName )."!" )); + } +} + diff --git a/tests/selenium/suites/DeletePageAdminTestCase.php b/tests/selenium/suites/DeletePageAdminTestCase.php new file mode 100644 index 0000000000..93e163877c --- /dev/null +++ b/tests/selenium/suites/DeletePageAdminTestCase.php @@ -0,0 +1,89 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + + +class DeletePageAdminTestCase extends SeleniumTestCase { + + // Verify adding a new page + public function testDeletePage() { + + + $newPage = "new"; + $displayName = "New"; + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=".$displayName ); + $this->waitForPageToLoad( "60000" ); + $this->type( "wpTextbox1", $newPage." text" ); + $this->click( "wpSave" ); + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click( "link=Log out" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=Log in / create account" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "wpName1", $this->selenium->getUser() ); + $this->type( "wpPassword1", $this->selenium->getPass() ); + $this->click( "wpLoginAttempt" ); + $this->waitForPageToLoad( "30000" ); + $this->type( "searchInput", "new" ); + $this->click( "searchGoButton"); + $this->waitForPageToLoad( "30000" ); + + // Verify 'Delete' link displayed + $source = $this->gettext( "link=Delete" ); + $correct = strstr ( $source, "Delete" ); + $this->assertEquals($correct, true ); + + $this->click( "link=Delete" ); + $this->waitForPageToLoad( "30000" ); + + // Verify 'Delete' button available + $this->assertTrue($this->isElementPresent( "wpConfirmB" )); + + $this->click( "wpConfirmB" ); + $this->waitForPageToLoad( "30000" ); + + // Verify 'Action complete' text displayed + $source = $this->gettext( "firstHeading" ); + $correct = strstr ( $source, "Action complete" ); + $this->assertEquals( $correct, true ); + + // Verify ' has been deleted. See deletion log for a record of recent deletions.' text displayed + $source = $this->gettext( "//div[@id='bodyContent']/p[1]" ); + $correct = strstr ( $source, "\"New\" has been deleted. See deletion log for a record of recent deletions." ); + $this->assertEquals( $correct, true ); + } +} diff --git a/tests/selenium/suites/EmailPasswordTestCase.php b/tests/selenium/suites/EmailPasswordTestCase.php new file mode 100644 index 0000000000..b504c24f64 --- /dev/null +++ b/tests/selenium/suites/EmailPasswordTestCase.php @@ -0,0 +1,81 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class EmailPasswordTestCase extends SeleniumTestCase { + + // change user name for each and every test (with in 24 hours) + private $userName = "test1"; + + public function testEmailPasswordButton() { + + $this->click( "link=Log out" ); + $this->waitForPageToLoad( "30000" ); + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + + // click Log in / create account link to open Log in / create account' page + $this->click( "link=Log in / create account" ); + $this->waitForPageToLoad( "30000" ); + $this->assertTrue($this->isElementPresent( "wpMailmypassword" )); + } + + // Verify Email password functionality + public function testEmailPasswordMessages() { + + $this->click( "link=Log out" ); + $this->waitForPageToLoad( "30000" ); + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + + // click Log in / create account link to open Log in / create account' page + $this->click( "link=Log in / create account" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "wpName1", "" ); + $this->click( "wpMailmypassword" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Login error\n You have not specified a valid user name.", + $this->getText("//div[@id='bodyContent']/div[4]")); + + $this->type( "wpName1", $this->userName ); + $this->click( "wpMailmypassword" ); + $this->waitForPageToLoad( "30000" ); + + // Can not run on localhost + $this->assertEquals( "A new password has been sent to the e-mail address registered for ".ucfirst($this->userName).". Please log in again after you receive it.", + $this->getText("//div[@id='bodyContent']/div[4]" )); + + $this->type( "wpName1", $this->userName ); + $this->click( "wpMailmypassword" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Login error\n A password reminder has already been sent, within the last 24 hours. To prevent abuse, only one password reminder will be sent per 24 hours.", + $this->getText( "//div[@id='bodyContent']/div[4]" )); + } +} + diff --git a/tests/selenium/suites/MediaWikExtraTestSuite.php b/tests/selenium/suites/MediaWikExtraTestSuite.php new file mode 100644 index 0000000000..205cb33244 --- /dev/null +++ b/tests/selenium/suites/MediaWikExtraTestSuite.php @@ -0,0 +1,20 @@ +setLoginBeforeTests( true ); + parent::setUp(); + } + public function addTests() { + $testFiles = array( + 'maintenance/tests/selenium/suites/MyContributionsTestCase.php', + 'maintenance/tests/selenium/suites/MyWatchListTestCase.php', + 'maintenance/tests/selenium/suites/UserPreferencesTestCase.php', + 'maintenance/tests/selenium/suites/MovePageTestCase.php', + 'maintenance/tests/selenium/suites/PageSearchTestCase.php', + 'maintenance/tests/selenium/suites/EmailPasswordTestCase.php', + 'maintenance/tests/selenium/suites/CreateAccountTestCase.php' + ); + parent::addTestFiles( $testFiles ); + } +} diff --git a/tests/selenium/suites/MediaWikiEditorConfig.php b/tests/selenium/suites/MediaWikiEditorConfig.php new file mode 100644 index 0000000000..f0b3a7e0ca --- /dev/null +++ b/tests/selenium/suites/MediaWikiEditorConfig.php @@ -0,0 +1,47 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class MediaWikiEditorConfig { + + public static function getSettings(&$includeFiles, &$globalConfigs) { + $includes = array( + //files that needed to be included would go here + 'maintenance/tests/selenium/suites/MediaWikiCommonFunction.php' + ); + $configs = array( + 'wgPageLoadTime' => "600000" + ); + $includeFiles = array_merge( $includeFiles, $includes ); + $globalConfigs = array_merge( $globalConfigs, $configs); + return true; + } +} + + + diff --git a/tests/selenium/suites/MediaWikiEditorTestSuite.php b/tests/selenium/suites/MediaWikiEditorTestSuite.php new file mode 100644 index 0000000000..06046365f1 --- /dev/null +++ b/tests/selenium/suites/MediaWikiEditorTestSuite.php @@ -0,0 +1,18 @@ +setLoginBeforeTests( true ); + parent::setUp(); + } + public function addTests() { + $testFiles = array( + 'maintenance/tests/selenium/suites/AddNewPageTestCase.php', + 'maintenance/tests/selenium/suites/AddContentToNewPageTestCase.php', + 'maintenance/tests/selenium/suites/PreviewPageTestCase.php', + 'maintenance/tests/selenium/suites/SavePageTestCase.php', + ); + parent::addTestFiles( $testFiles ); + } +} + diff --git a/tests/selenium/suites/MediawikiCoreSmokeTestCase.php b/tests/selenium/suites/MediawikiCoreSmokeTestCase.php new file mode 100644 index 0000000000..7b9525afb2 --- /dev/null +++ b/tests/selenium/suites/MediawikiCoreSmokeTestCase.php @@ -0,0 +1,69 @@ +login(); + $this->open( $this->getUrl() . + '/index.php?title=Special:Upload' ); + $this->type( 'wpUploadFile', dirname( __FILE__ ) . + "\\..\\data\\Wikipedia-logo-v2-de.png" ); + $this->check( 'wpIgnoreWarning' ); + $this->click( 'wpUpload' ); + $this->waitForPageToLoad( 30000 ); + + $this->assertSeleniumHTMLContains( + '//h1[@class="firstHeading"]', "Wikipedia-logo-v2-de.png" ); + + /* + $this->open( $this->getUrl() . '/index.php?title=Image:' + . ucfirst( $this->filename ) . '&action=delete' ); + $this->type( 'wpReason', 'Remove test file' ); + $this->click( 'mw-filedelete-submit' ); + $this->waitForPageToLoad( 10000 ); + + // Todo: This message is localized + $this->assertSeleniumHTMLContains( '//div[@id="bodyContent"]/p', + ucfirst( $this->filename ) . '.*has been deleted.' ); + */ + } + + +} diff --git a/tests/selenium/suites/MediawikiCoreSmokeTestSuite.php b/tests/selenium/suites/MediawikiCoreSmokeTestSuite.php new file mode 100644 index 0000000000..76287b23f2 --- /dev/null +++ b/tests/selenium/suites/MediawikiCoreSmokeTestSuite.php @@ -0,0 +1,19 @@ +setLoginBeforeTests( false ); + parent::setUp(); + } + public function addTests() { + $testFiles = array( + 'maintenance/tests/selenium/suites/MediawikiCoreSmokeTestCase.php' + ); + parent::addTestFiles( $testFiles ); + } + + +} diff --git a/tests/selenium/suites/MovePageTestCase.php b/tests/selenium/suites/MovePageTestCase.php new file mode 100644 index 0000000000..fa7fc5d1f3 --- /dev/null +++ b/tests/selenium/suites/MovePageTestCase.php @@ -0,0 +1,117 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class MovePageTestCase extends SeleniumTestCase { + + // Verify move(rename) wiki page + public function testMovePage() { + + $newPage = "mypage99"; + $displayName = "Mypage99"; + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=".$displayName ); + $this->waitForPageToLoad( "60000" ); + $this->type( "wpTextbox1", $newPage." text" ); + $this->click( "wpSave" ); + $this->waitForPageToLoad( "60000" ); + + // Verify link 'Move' available + $this->assertTrue($this->isElementPresent( "link=Move" )); + + $this->click( "link=Move" ); + $this->waitForPageToLoad( "30000" ); + + // Verify correct page name displayed under 'Move Page' field + $this->assertEquals($displayName, + $this->getText("//table[@id='mw-movepage-table']/tbody/tr[1]/td[2]/strong/a")); + $movePageName = $this->getText( "//table[@id='mw-movepage-table']/tbody/tr[1]/td[2]/strong/a" ); + + // Verify 'To new title' field has current page name as the default name + $newTitle = $this->getValue( "wpNewTitle" ); + $correct = strstr( $movePageName , $newTitle ); + $this->assertEquals( $correct, true ); + + $this->type( "wpNewTitle", $displayName ); + $this->click( "wpMove" ); + $this->waitForPageToLoad( "30000" ); + + // Verify warning message for the same source and destination titles + $this->assertEquals( "Source and destination titles are the same; cannot move a page over itself.", + $this->getText("//div[@id='bodyContent']/p[4]/strong" )); + + // Verify warning message for the blank title + $this->type( "wpNewTitle", "" ); + $this->click( "wpMove" ); + $this->waitForPageToLoad( "30000" ); + + // Verify warning message for the blank title + $this->assertEquals( "The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. It may contain one or more characters which cannot be used in titles.", + $this->getText( "//div[@id='bodyContent']/p[4]/strong" )); + + // Verify warning messages for the invalid titles + $this->type( "wpNewTitle", "# < > [ ] | { }" ); + $this->click( "wpMove" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. It may contain one or more characters which cannot be used in titles.", + $this->getText( "//div[@id='bodyContent']/p[4]/strong" )); + + $this->type( "wpNewTitle", $displayName."move" ); + $this->click( "wpMove" ); + $this->waitForPageToLoad( "30000" ); + + // Verify move success message displayed correctly + $this->assertEquals( "\"".$displayName."\" has been moved to \"".$displayName."move"."\"", + $this->getText( "//div[@id='bodyContent']/p[1]/b" )); + + $this->type( "searchInput", $newPage."move" ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + + // Verify search using new page name + $this->assertEquals( $displayName."move", $this->getText( "firstHeading" )); + + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + + // Verify search using old page name + $redirectPageName = $this->getText( "//*[@id='contentSub']" ); + $this->assertEquals( "(Redirected from ".$displayName.")" , $redirectPageName ); + + // newpage delete + $this->deletePage( $newPage."move" ); + $this->deletePage( $newPage ); + } +} + diff --git a/tests/selenium/suites/MyContributionsTestCase.php b/tests/selenium/suites/MyContributionsTestCase.php new file mode 100644 index 0000000000..fbbb542b3e --- /dev/null +++ b/tests/selenium/suites/MyContributionsTestCase.php @@ -0,0 +1,76 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class MyContributionsTestCase extends SeleniumTestCase { + + // Verify user contributions + public function testRecentChangesAvailability() { + + $newPage = "mypage999"; + $displayName = "Mypage999"; + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + + $this->type( "searchInput", $newPage); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "60000" ); + $this->click( "link=".$displayName ); + $this->waitForPageToLoad( "600000" ); + $this->type( "wpTextbox1", $newPage." text" ); + $this->click( "wpSave" ); + $this->waitForPageToLoad( "60000" ); + + // Verify My contributions Link available + $this->assertTrue($this->isElementPresent( "link=My contributions" )); + + $this->click( "link=My contributions" ); + $this->waitForPageToLoad( "30000" ); + + // Verify recent page adding available on My Contributions list + $this->assertEquals( $displayName, $this->getText( "link=".$displayName )); + + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + + $this->click( "link=Edit" ); + $this->waitForPageToLoad( "30000" ); + $this->type( "wpTextbox1", $newPage." text changed" ); + $this->click( "wpSave" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=My contributions" ); + $this->waitForPageToLoad( "30000" ); + + // Verify recent page changes available on My Contributions + $this->assertTrue($this->isTextPresent($displayName." ‎ (top)")); + $this->deletePage($newPage); + } +} + diff --git a/tests/selenium/suites/MyWatchListTestCase.php b/tests/selenium/suites/MyWatchListTestCase.php new file mode 100644 index 0000000000..b2cc326f26 --- /dev/null +++ b/tests/selenium/suites/MyWatchListTestCase.php @@ -0,0 +1,73 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + + +class MyWatchListTestCase extends SeleniumTestCase { + + // Verify user watchlist + public function testMyWatchlist() { + + $newPage = "mypage"; + $displayName = "Mypage"; + $wikiText = "watch page"; + + $this->open( $this->getUrl().'/index.php?title=Main_Page' ); + + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=".$displayName ); + $this->waitForPageToLoad( "600000" ); + + $this->click( "wpWatchthis" ); + $this->type( "wpTextbox1",$wikiText ); + $this->click( "wpSave" ); + $this->waitForPageToLoad( "30000" ); + + // Verify link 'My Watchlist' available + $this->assertTrue( $this->isElementPresent( "link=My watchlist" ) ); + + $this->click( "link=My watchlist" ); + $this->waitForPageToLoad( "30000" ); + + // Verify newly added page to the watchlist is available + $watchList = $this->getText( "//*[@id='bodyContent']" ); + $this->assertContains( $displayName, $watchList ); + + $this->type( "searchInput", $newPage ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "30000" ); + $this->click("link=Edit"); + $this->waitForPageToLoad( "30000" ); + $this->click( "wpWatchthis" ); + $this->click( "wpSave" ); + $this->deletePage( $newPage ); + } +} + diff --git a/tests/selenium/suites/PageDeleteTestSuite.php b/tests/selenium/suites/PageDeleteTestSuite.php new file mode 100644 index 0000000000..2e535c11af --- /dev/null +++ b/tests/selenium/suites/PageDeleteTestSuite.php @@ -0,0 +1,16 @@ +setLoginBeforeTests( true ); + parent::setUp(); + } + public function addTests() { + $testFiles = array( + 'maintenance/tests/selenium/suites/DeletePageAdminTestCase.php' + ); + parent::addTestFiles( $testFiles ); + } + + +} diff --git a/tests/selenium/suites/PageSearchTestCase.php b/tests/selenium/suites/PageSearchTestCase.php new file mode 100644 index 0000000000..d30e32b831 --- /dev/null +++ b/tests/selenium/suites/PageSearchTestCase.php @@ -0,0 +1,105 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class PageSearchTestCase extends SeleniumTestCase { + + // Verify the functionality of the 'Go' button + public function testPageSearchBtnGo() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->type( "searchInput", "calcey qa" ); + $this->click( "searchGoButton" ); + $this->waitForPageToLoad( "600000" ); + + // Verify no page matched with the entered search text + $source = $this->gettext( "//div[@id='bodyContent']/div[4]/p/b" ); + $correct = strstr ( $source, "Create the page \"Calcey qa\" on this wiki!" ); + $this->assertEquals( $correct, true ); + + $this->click( "link=Calcey qa" ); + $this->waitForPageToLoad( "600000" ); + + $this->type( "wpTextbox1", "Calcey QA team" ); + $this->click( "wpSave" ); + $this->waitForPageToLoad( "600000" ); + + } + + // Verify the functionality of the 'Search' button + public function testPageSearchBtnSearch() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->type( "searchInput", "Calcey web" ); + $this->click( "mw-searchButton" ); + $this->waitForPageToLoad( "30000" ); + + // Verify no page is available as the search text + $source = $this->gettext( "//div[@id='bodyContent']/div[4]/p[2]/b" ); + $correct = strstr ( $source, "Create the page \"Calcey web\" on this wiki!" ); + $this->assertEquals( $correct, true ); + + $this->click( "link=Calcey web" ); + $this->waitForPageToLoad( "600000" ); + + $this->type( "wpTextbox1", "Calcey web team" ); + $this->click( "wpSave" ); + $this->waitForPageToLoad( "600000" ); + + // Verify saved page is opened when the exact page name is given + $this->type( "searchInput", "Calcey web" ); + $this->click( "mw-searchButton" ); + $this->waitForPageToLoad( "30000" ); + + // Verify exact page matched with the entered search text using 'Search' button + $source = $this->getText( "//*[@id='bodyContent']/div[4]/p/b" ); + $correct = strstr( $source, "There is a page named \"Calcey web\" on this wiki." ); + $this->assertEquals( $correct, true ); + + // Verify resutls available when partial page name is entered as the search text + $this->type( "searchInput", "Calcey" ); + $this->click( "mw-searchButton" ); + $this->waitForPageToLoad( "30000" ); + + // Verify text avaialble in the search result under the page titles + if($this->isElementPresent( "Page_title_matches" )) { + $textPageTitle = $this->getText( "//*[@id='bodyContent']/div[4]/ul[1]/li[1]/div[1]/a" ); + $this->assertContains( 'Calcey', $textPageTitle ); + } + + // Verify text avaialble in the search result under the page text + if($this->isElementPresent( "Page_text_matches" )) { + $textPageText = $this->getText( "//*[@id='bodyContent']/div[4]/ul[2]/li[2]/div[2]/span" ); + $this->assertContains( 'Calcey', $textPageText ); + } + $this->deletePage("Calcey QA"); + $this->deletePage("Calcey web"); + } +} diff --git a/tests/selenium/suites/PreviewPageTestCase.php b/tests/selenium/suites/PreviewPageTestCase.php new file mode 100644 index 0000000000..5cce3337cf --- /dev/null +++ b/tests/selenium/suites/PreviewPageTestCase.php @@ -0,0 +1,53 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class PreviewPageTestCase extends SeleniumTestCase { + + // Verify adding a new page + public function testPreviewPage() { + $wikiText = "Adding this page to test the \n Preview button functionality"; + $newPage = "Test Preview Page"; + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->getNewPage( $newPage ); + $this->type( "wpTextbox1", $wikiText."" ); + $this->assertTrue($this->isElementPresent( "//*[@id='wpPreview']" )); + + $this->click( "wpPreview" ); + + // Verify saved page available + $source = $this->gettext( "firstHeading" ); + $correct = strstr( $source, "Test Preview Page" ); + $this->assertEquals( $correct, true); + + // Verify page content previewed succesfully + $contentOfPreviewPage = $this->getText( "//*[@id='content']" ); + $this->assertContains( $wikiText, $contentOfPreviewPage ); + } +} diff --git a/tests/selenium/suites/SavePageTestCase.php b/tests/selenium/suites/SavePageTestCase.php new file mode 100644 index 0000000000..f04c474761 --- /dev/null +++ b/tests/selenium/suites/SavePageTestCase.php @@ -0,0 +1,58 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class SavePageTestCase extends SeleniumTestCase { + + // Verify adding a new page + public function testSavePage() { + $wikiText = "Adding this page to test the Save button functionality"; + $newPage = "Test Save Page"; + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->getNewPage($newPage); + $this->type("wpTextbox1", $wikiText); + + // verify 'Save' button available + $this->assertTrue($this->isElementPresent( "wpSave" )); + $this->click( "wpSave" ); + + // Verify saved page available + $source = $this->gettext( "firstHeading" ); + $correct = strstr( $source, "Test Save Page" ); + + // Verify Saved page name displayed correctly + $this->assertEquals( $correct, true ); + + // Verify page content saved succesfully + $contentOfSavedPage = $this->getText( "//*[@id='content']" ); + $this->assertContains( $wikiText, $contentOfSavedPage ); + $this->deletePage( $newPage ); + } +} diff --git a/tests/selenium/suites/SimpleSeleniumConfig.php b/tests/selenium/suites/SimpleSeleniumConfig.php new file mode 100644 index 0000000000..cffa83c4ea --- /dev/null +++ b/tests/selenium/suites/SimpleSeleniumConfig.php @@ -0,0 +1,15 @@ + 'chick' + ); + $includeFiles = array_merge( $includeFiles, $includes ); + $globalConfigs = array_merge( $globalConfigs, $configs); + return true; + } +} \ No newline at end of file diff --git a/tests/selenium/suites/SimpleSeleniumTestCase.php b/tests/selenium/suites/SimpleSeleniumTestCase.php new file mode 100644 index 0000000000..8f01b43791 --- /dev/null +++ b/tests/selenium/suites/SimpleSeleniumTestCase.php @@ -0,0 +1,30 @@ +open( $this->getUrl() . + '/index.php?title=Selenium&action=edit' ); + $this->type( "wpTextbox1", "This is a basic test" ); + $this->click( "wpPreview" ); + $this->waitForPageToLoad( 10000 ); + + // check result + $source = $this->getText( "//div[@id='wikiPreview']/p" ); + $correct = strstr( $source, "This is a basic test" ); + $this->assertEquals( $correct, true ); + } + + /* + * All this test really does is verify that a global var was set. + * It depends on $wgDefaultSkin = 'chick'; being set + */ + public function testGlobalVariableForDefaultSkin() { + $this->open( $this->getUrl() . '/index.php?&action=purge' ); + $bodyClass = $this->getAttribute( "//body/@class" ); + $this-> assertContains('skin-chick', $bodyClass, 'Chick skin not set'); + } + +} diff --git a/tests/selenium/suites/SimpleSeleniumTestSuite.php b/tests/selenium/suites/SimpleSeleniumTestSuite.php new file mode 100644 index 0000000000..a04f33ed93 --- /dev/null +++ b/tests/selenium/suites/SimpleSeleniumTestSuite.php @@ -0,0 +1,26 @@ +setLoginBeforeTests( false ); + parent::setUp(); + } + public function addTests() { + $testFiles = array( + 'maintenance/tests/selenium/suites/SimpleSeleniumTestCase.php' + ); + parent::addTestFiles( $testFiles ); + } + + +} diff --git a/tests/selenium/suites/UserPreferencesTestCase.php b/tests/selenium/suites/UserPreferencesTestCase.php new file mode 100644 index 0000000000..c8b0d319d6 --- /dev/null +++ b/tests/selenium/suites/UserPreferencesTestCase.php @@ -0,0 +1,179 @@ + + * http://citizendium.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @addtogroup Testing + * + */ + +class UserPreferencesTestCase extends SeleniumTestCase { + + // Verify user information + public function testUserInfoDisplay() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click( "link=My preferences" ); + $this->waitForPageToLoad( "30000" ); + + // Verify correct username displayed in User Preferences + $this->assertEquals( $this->getText( "//li[@id='pt-userpage']/a" ), + $this->getText( "//table[@id='mw-htmlform-info']/tbody/tr[1]/td[2]" )); + + // Verify existing Signature Displayed correctly + $this->assertEquals( $this->selenium->getUser(), + $this->getTable( "mw-htmlform-signature.0.1" ) ); + } + + // Verify change password + public function testChangePassword() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click( "link=My preferences" ); + $this->waitForPageToLoad( "30000" ); + + $this->click( "link=Change password" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "wpPassword", "12345" ); + $this->type( "wpNewPassword", "54321" ); + $this->type( "wpRetype", "54321" ); + $this->click( "//input[@value='Change password']" ); + $this->waitForPageToLoad( "30000" ); + + $this->assertEquals( "Preferences", $this->getText( "firstHeading" )); + + $this->click( "link=Change password" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "wpPassword", "54321" ); + $this->type( "wpNewPassword", "12345" ); + $this->type( "wpRetype", "12345" ); + $this->click( "//input[@value='Change password']" ); + $this->waitForPageToLoad( "30000" ); + $this->assertEquals( "Preferences", $this->getText( "firstHeading" )); + + $this->click( "link=Change password" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "wpPassword", "54321" ); + $this->type( "wpNewPassword", "12345" ); + $this->type( "wpRetype", "12345" ); + $this->click( "//input[@value='Change password']" ); + $this->waitForPageToLoad( "30000" ); + } + + // Verify successful preferences save + public function testSuccessfullSave() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click( "link=My preferences" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "mw-input-realname", "Test User" ); + $this->click( "prefcontrol" ); + $this->waitForPageToLoad( "30000" ); + + // Verify "Your preferences have been saved." message + $this->assertEquals( "Your preferences have been saved.", + $this->getText( "//div[@id='bodyContent']/div[4]/strong/p" )); + $this->type( "mw-input-realname", "" ); + $this->click( "prefcontrol" ); + $this->waitForPageToLoad( "30000" ); + + } + + // Verify change signature + public function testChangeSignature() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click( "link=My preferences" ); + $this->waitForPageToLoad( "30000" ); + + $this->type( "mw-input-nickname", "TestSignature" ); + $this->click( "prefcontrol" ); + $this->waitForPageToLoad( "30000" ); + + // Verify change user signature + $this->assertEquals( "TestSignature", $this->getText( "link=TestSignature" )); + $this->type( "mw-input-nickname", "Test" ); + $this->click( "prefcontrol" ); + $this->waitForPageToLoad("30000"); + } + + // Verify change date format + public function testChangeDateFormatTimeZone() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + + $this->click( "link=My preferences" ); + $this->waitForPageToLoad( "30000" ); + $this->click( "link=Date and time" ); + $this->waitForPageToLoad( "30000" ); + + $this->click( "mw-input-date-dmy" ); + $this->select( "mw-input-timecorrection", "label=Asia/Colombo" ); + $this->click( "prefcontrol" ); + $this->waitForPageToLoad( "30000" ); + + // Verify Date format and time zome saved + $this->assertEquals( "Your preferences have been saved.", + $this->getText( "//div[@id='bodyContent']/div[4]/strong/p" )); + } + + // Verify restoring all default settings + public function testSetAllDefault() { + + $this->open( $this->getUrl() . + '/index.php?title=Main_Page&action=edit' ); + $this->click( "link=My preferences" ); + $this->waitForPageToLoad( "30000" ); + + // Verify restoring all default settings + $this->assertEquals( "Restore all default settings", + $this->getText( "link=Restore all default settings" )); + + $this->click("//*[@id='preferences']/div/a"); + $this->waitForPageToLoad("30000"); + + // Verify 'This can not be undone' warning message displayed + $this->assertTrue($this->isElementPresent("//input[@value='Restore all default settings']")); + + // Verify 'Restore all default settings' button available + $this->assertEquals("You can use this page to reset your preferences to the site defaults. This cannot be undone.", + $this->getText("//div[@id='bodyContent']/p")); + + $this->click("//input[@value='Restore all default settings']"); + $this->waitForPageToLoad("30000"); + + // Verify preferences saved successfully + $this->assertEquals("Your preferences have been saved.", + $this->getText("//div[@id='bodyContent']/div[4]/strong/p")); + } +} + -- 2.20.1