diff options
Diffstat (limited to 'tests/phpunit')
51 files changed, 6834 insertions, 0 deletions
diff --git a/tests/phpunit/Makefile b/tests/phpunit/Makefile new file mode 100644 index 000000000000..26c39f321f24 --- /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 <target> [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 000000000000..ce78270dc0a6 --- /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 000000000000..76f38c1dcc9e --- /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 000000000000..81beb2bc4a99 --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,65 @@ +<?php +/** + * Bootstrapping for MediaWiki PHPUnit tests + * This file is included by phpunit and is NOT in the global scope. + * + * @file + */ + +if ( !defined( 'MW_PHPUNIT_TEST' ) ) { + echo <<<EOF +You are running these tests directly from phpunit. You may not have all globals correctly set. +Running phpunit.php instead is recommended. +EOF; + require_once ( dirname( __FILE__ ) . "/phpunit.php" ); +} + +// Output a notice when running with older versions of PHPUnit +if ( !version_compare( PHPUnit_Runner_Version::id(), "3.4.1", ">" ) ) { + echo <<<EOF +******************************************************************************** + +These tests run best with version PHPUnit 3.4.2 or better. Earlier versions may +show failures because earlier versions of PHPUnit do not properly implement +dependencies. + +******************************************************************************** + +EOF; +} + +global $wgLocalisationCacheConf, $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; +global $wgMessageCache, $messageMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry; +$wgLocalisationCacheConf['storeClass'] = 'LCStore_Null'; +$wgMainCacheType = CACHE_NONE; +$wgMessageCacheType = CACHE_NONE; +$wgParserCacheType = CACHE_NONE; +$wgUseDatabaseMessages = false; # Set for future resets + +# The message cache was already created in Setup.php +$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', + array( $messageMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry ) ); + +/* Classes */ + +abstract class MediaWikiTestSetup extends PHPUnit_Framework_TestCase { + protected $suite; + public $regex = ''; + public $runDisabled = false; + + function __construct( PHPUnit_Framework_TestSuite $suite = null ) { + if ( null !== $suite ) { + $this->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 000000000000..987a0f5be752 --- /dev/null +++ b/tests/phpunit/includes/CdbTest.php @@ -0,0 +1,84 @@ +<?php + +/** + * Test the CDB reader/writer + */ + +class CdbTest extends PHPUnit_Framework_TestCase { + + public function setUp() { + if ( !CdbReader::haveExtension() ) { + $this->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 000000000000..3c15f69755ce --- /dev/null +++ b/tests/phpunit/includes/ExternalStoreTest.php @@ -0,0 +1,32 @@ +<?php +/** + * External Store tests + */ + +class ExternalStoreTest extends PHPUnit_Framework_TestCase { + private $saved_wgExternalStores; + + function setUp() { + global $wgExternalStores; + $this->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 000000000000..f8790113531a --- /dev/null +++ b/tests/phpunit/includes/ExtraParserTest.php @@ -0,0 +1,31 @@ +<?php +/** + * Parser-related tests that don't suit for parserTests.txt + */ + +class ExtraParserTest extends PHPUnit_Framework_TestCase { + + function setUp() { + global $wgMemc; + global $wgContLang; + global $wgShowDBErrorBacktrace; + + $wgShowDBErrorBacktrace = true; + if ( $wgContLang === null ) $wgContLang = new Language; + $wgMemc = new FakeMemCachedClient; + } + + // Bug 8689 - Long numeric lines kill the parser + function testBug8689() { + global $wgLang; + global $wgUser; + $longLine = '1.' . str_repeat( '1234567890', 100000 ) . "\n"; + + if ( $wgLang === null ) $wgLang = new Language; + $parser = new Parser(); + $t = Title::newFromText( 'Unit test' ); + $options = ParserOptions::newFromUser( $wgUser ); + $this->assertEquals( "<p>$longLine</p>", + $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 000000000000..d9116dc0ce89 --- /dev/null +++ b/tests/phpunit/includes/GlobalTest.php @@ -0,0 +1,380 @@ +<?php + +class GlobalTest extends PHPUnit_Framework_TestCase { + function setUp() { + global $wgReadOnlyFile, $wgContLang, $wgLang; + $this->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 000000000000..a48c7bb4f499 --- /dev/null +++ b/tests/phpunit/includes/HttpTest.php @@ -0,0 +1,526 @@ +<?php + +class MockCookie extends Cookie { + public function canServeDomain( $arg ) { return parent::canServeDomain( $arg ); } + public function canServePath( $arg ) { return parent::canServePath( $arg ); } + public function isUnExpired() { return parent::isUnExpired(); } +} + +/** + * @group Broken + */ +class HttpTest extends PHPUnit_Framework_TestCase { + static $content; + static $headers; + static $has_curl; + static $has_fopen; + static $has_proxy = false; + static $proxy = "http://hulk:8080/"; + var $test_geturl = array( + "http://www.example.com/", + "http://pecl.php.net/feeds/pkg_apc.rss", + "http://toolserver.org/~jan/poll/dev/main.php?page=wiki_output&id=3", + "http://meta.wikimedia.org/w/index.php?title=Interwiki_map&action=raw", + "http://www.mediawiki.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:MediaWiki_hooks&format=php", + ); + var $test_requesturl = array( "http://en.wikipedia.org/wiki/Special:Export/User:MarkAHershberger" ); + + var $test_posturl = array( "http://www.comp.leeds.ac.uk/cgi-bin/Perl/environment-example" => "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 000000000000..ee092997bdba --- /dev/null +++ b/tests/phpunit/includes/IPTest.php @@ -0,0 +1,298 @@ +<?php +/* + * Tests for IP validity functions. Ported from /t/inc/IP.t by avar. + */ + +class IPTest extends PHPUnit_Framework_TestCase { + // not sure it should be tested with boolean false. hashar 20100924 + public function testisIPAddress() { + $this->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 000000000000..4de4e63d4fc3 --- /dev/null +++ b/tests/phpunit/includes/ImageFunctionsTest.php @@ -0,0 +1,48 @@ +<?php + +class ImageFunctionsTest extends PHPUnit_Framework_TestCase { + function testFitBoxWidth() { + $vals = array( + array( + 'width' => 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 000000000000..944195f4db25 --- /dev/null +++ b/tests/phpunit/includes/LanguageConverterTest.php @@ -0,0 +1,128 @@ +<?php + +class LanguageConverterTest extends PHPUnit_Framework_TestCase { + protected $lang = null; + protected $lc = null; + + function setUp() { + global $wgMemc, $wgRequest, $wgUser, $wgContLang; + + $wgUser = new User; + $wgRequest = new FauxRequest( array() ); + $wgMemc = new FakeMemCachedClient; + $wgContLang = Language::factory( 'tg' ); + $this->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 000000000000..0008a7772d44 --- /dev/null +++ b/tests/phpunit/includes/LicensesTest.php @@ -0,0 +1,14 @@ +<?php + +class LicensesTest extends PHPUnit_Framework_TestCase { + + function testLicenses() { + $str = " +* Free licenses: +** GFDL|Debian disagrees +"; + + $lc = new Licenses( array( 'licenses' => $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 000000000000..32aa51579c04 --- /dev/null +++ b/tests/phpunit/includes/LocalFileTest.php @@ -0,0 +1,99 @@ +<?php + +/** + * These tests should work regardless of $wgCapitalLinks + */ + +class LocalFileTest extends PHPUnit_Framework_TestCase { + function setUp() { + global $wgContLang, $wgCapitalLinks; + + $wgContLang = new Language; + $wgCapitalLinks = true; + $info = array( + 'name' => '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 000000000000..22d736369b8e --- /dev/null +++ b/tests/phpunit/includes/MessageTest.php @@ -0,0 +1,50 @@ +<?php + +class MessageTest extends PHPUnit_Framework_TestCase { + + function setUp() { + global $wgLanguageCode, $wgLang, $wgContLang, $wgMessageCache; + + $wgLanguageCode = 'en'; # For mainpage to be 'Main Page' + //Note that a Stub Object is not enough for this test + $wgContLang = $wgLang = Language::factory( $wgLanguageCode ); + $wgMessageCache = new MessageCache( false, false, 3600 ); + } + + function testExists() { + $this->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 000000000000..63d98e7c9389 --- /dev/null +++ b/tests/phpunit/includes/ParserOptionsTest.php @@ -0,0 +1,36 @@ +<?php + +class ParserOptionsTest extends PHPUnit_Framework_TestCase { + + private $popts; + private $pcache; + + function setUp() { + ParserTest::setUp(); //reuse setup from parser tests + global $wgContLang, $wgUser; + $wgContLang = new StubContLang; + $this->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 000000000000..5ad7d9373d31 --- /dev/null +++ b/tests/phpunit/includes/ResourceLoaderFileModuleTest.php @@ -0,0 +1,15 @@ +<?php + +class ResourceLoaderFileModuleTest extends PHPUnit_Framework_TestCase { + /* Provider Methods */ + + public function provide() { + + } + + /* Test Methods */ + + public function test() { + + } +} diff --git a/tests/phpunit/includes/ResourceLoaderTest.php b/tests/phpunit/includes/ResourceLoaderTest.php new file mode 100644 index 000000000000..8de178a951dd --- /dev/null +++ b/tests/phpunit/includes/ResourceLoaderTest.php @@ -0,0 +1,71 @@ +<?php + +class ResourceLoaderTest extends PHPUnit_Framework_TestCase { + + protected static $resourceLoaderRegisterModulesHook; + + /* Hook Methods */ + + /** + * ResourceLoaderRegisterModules hook + */ + public static function resourceLoaderRegisterModules( &$resourceLoader ) { + self::$resourceLoaderRegisterModulesHook = true; + return true; + } + + /* Provider Methods */ + public function provideValidModules() { + return array( + array( 'TEST.validModule1', new ResourceLoaderTestModule() ), + ); + } + + /* Test Methods */ + + /** + * Ensures that the ResourceLoaderRegisterModules hook is called when a new ResourceLoader object is constructed + * @covers ResourceLoader::__construct + */ + public function testCreatingNewResourceLoaderCallsRegistrationHook() { + self::$resourceLoaderRegisterModulesHook = false; + $resourceLoader = new ResourceLoader(); + $this->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 000000000000..a9405b6ef78f --- /dev/null +++ b/tests/phpunit/includes/RevisionTest.php @@ -0,0 +1,114 @@ +<?php + +class RevisionTest extends PHPUnit_Framework_TestCase { + var $saveGlobals = array(); + + function setUp() { + global $wgContLang; + $wgContLang = Language::factory( 'en' ); + $globalSet = array( + 'wgLegacyEncoding' => 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 000000000000..117ac1ca49f6 --- /dev/null +++ b/tests/phpunit/includes/SampleTest.php @@ -0,0 +1,95 @@ +<?php + +class TestSample extends PHPUnit_Framework_TestCase { + + /** + * Anything that needs to happen before your tests should go here. + */ + function setUp() { + global $wgContLang; + /* For example, we need to set $wgContLang for creating a new Title */ + $wgContLang = Language::factory( 'en' ); + } + + /** + * Anything cleanup you need to do should go here. + */ + function tearDown() { + } + + /** + * Name tests so that PHPUnit can turn them into sentances when + * they run. While MediaWiki isn't strictly an Agile Programming + * project, you are encouraged to use the naming described under + * "Agile Documentation" at + * http://www.phpunit.de/manual/3.4/en/other-uses-for-tests.html + */ + function testTitleObjectStringConversion() { + $title = Title::newFromText("text"); + $this->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 000000000000..a12f9013f042 --- /dev/null +++ b/tests/phpunit/includes/SanitizerTest.php @@ -0,0 +1,72 @@ +<?php + +class SanitizerTest extends PHPUnit_Framework_TestCase { + + function setUp() { + AutoLoader::loadClass( 'Sanitizer' ); + } + + function testDecodeNamedEntities() { + $this->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( + '<div>Hello world</div>', + Sanitizer::removeHTMLtags( '<div>Hello world</div />' ), + 'Self-closing closing div' + ); + } +} + diff --git a/tests/phpunit/includes/SeleniumConfigurationTest.php b/tests/phpunit/includes/SeleniumConfigurationTest.php new file mode 100644 index 000000000000..16a916ed6c7c --- /dev/null +++ b/tests/phpunit/includes/SeleniumConfigurationTest.php @@ -0,0 +1,228 @@ +<?php + +class SeleniumConfigurationTest extends PHPUnit_Framework_TestCase { + + /* + * The file where the test temporarity stores the selenium config. + * This should be cleaned up as part of teardown. + */ + private $tempFileName; + + /* + * String containing the a sample selenium settings + */ + private $testConfig0 = +' +[SeleniumSettings] +browsers[firefox] = "*firefox" +browsers[iexplorer] = "*iexploreproxy" +browsers[chrome] = "*chrome" +host = "localhost" +port = "foobarr" +wikiUrl = "http://localhost/deployment" +username = "xxxxxxx" +userPassword = "" +testBrowser = "chrome" +startserver = +stopserver = +jUnitLogFile = +runAgainstGrid = false + +[SeleniumTests] +testSuite[SimpleSeleniumTestSuite] = "maintenance/tests/selenium/SimpleSeleniumTestSuite.php" +testSuite[TestSuiteName] = "testSuitePath" +'; + /* + * Array of expected browsers from $testConfig0 + */ + private $testBrowsers0 = array( 'firefox' => '*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 000000000000..9bfab9ede2d5 --- /dev/null +++ b/tests/phpunit/includes/SiteConfigurationTest.php @@ -0,0 +1,311 @@ +<?php + +function getSiteParams( $conf, $wiki ) { + $site = null; + $lang = null; + foreach ( $conf->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 000000000000..820e7130fd16 --- /dev/null +++ b/tests/phpunit/includes/TimeAdjustTest.php @@ -0,0 +1,49 @@ +<?php + +class TimeAdjustTest extends PHPUnit_Framework_TestCase { + static $offset; + + public function setUp() { + global $wgLocalTZoffset; + self::$offset = $wgLocalTZoffset; + + $this->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 000000000000..fa83b9a66971 --- /dev/null +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -0,0 +1,652 @@ +<?php + +/** + * @group Database + * @group Destructive + */ +class TitlePermissionTest extends PHPUnit_Framework_TestCase { + static $title; + static $user; + static $anonUser; + static $userUser; + static $altUser; + static $userName; + static $altUserName; + + function setUp() { + global $wgLocaltimezone, $wgLocalTZoffset, $wgMemc, $wgContLang, $wgLang, $wgMessageCache; + + if(!$wgMemc) { + $wgMemc = new FakeMemCachedClient; + } + $wgMessageCache = new MessageCache( $wgMemc, true, 3600 ); + $wgContLang = $wgLang = Language::factory( 'en' ); + + self::$userName = "Useruser"; + self::$altUserName = "Altuseruser"; + date_default_timezone_set( $wgLocaltimezone ); + $wgLocalTZoffset = date( "Z" ) / 60; + + self::$title = Title::makeTitle( NS_MAIN, "Main Page" ); + if ( !isset( self::$userUser ) || !( self::$userUser instanceOf User ) ) { + self::$userUser = User::newFromName( self::$userName ); + + if ( !self::$userUser->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 000000000000..5b42c1c575e7 --- /dev/null +++ b/tests/phpunit/includes/TitleTest.php @@ -0,0 +1,17 @@ +<?php + +class TitleTest extends PHPUnit_Framework_TestCase { + + function testLegalChars() { + $titlechars = Title::legalChars(); + + foreach ( range( 1, 255 ) as $num ) { + $chr = chr( $num ); + if ( strpos( "#[]{}<>|", $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 000000000000..838229928978 --- /dev/null +++ b/tests/phpunit/includes/UploadFromUrlTest.php @@ -0,0 +1,353 @@ +<?php + +require_once dirname( __FILE__ ) . '/api/ApiSetup.php'; + +/** + * @group Broken + */ +class UploadFromUrlTest extends ApiTestSetup { + + public function setUp() { + global $wgEnableUploads, $wgAllowCopyUploads, $wgAllowAsyncCopyUploads; + parent::setUp(); + + $wgEnableUploads = true; + $wgAllowCopyUploads = true; + $wgAllowAsyncCopyUploads = true; + wfSetupSession(); + + ini_set( 'log_errors', 1 ); + ini_set( 'error_reporting', 1 ); + ini_set( 'display_errors', 1 ); + + if ( wfLocalFile( 'UploadFromUrlTest.png' )->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 000000000000..cf8e4d3cc7d4 --- /dev/null +++ b/tests/phpunit/includes/UploadTest.php @@ -0,0 +1,90 @@ +<?php +/** + * @group Upload + */ +class UploadTest extends PHPUnit_Framework_TestCase { + protected $upload; + + + function setUp() { + global $wgContLang; + parent::setUp(); + $wgContLang = Language::factory( 'en' ); + $this->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 000000000000..c1f34193ed91 --- /dev/null +++ b/tests/phpunit/includes/UserIsValidEmailAddrTest.php @@ -0,0 +1,64 @@ +<?php + +class UserIsValidEmailAddrTest extends PHPUnit_Framework_TestCase { + + private function checkEmail( $addr, $expected = true, $msg = '') { + $this->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 000000000000..f01c0f1cc47d --- /dev/null +++ b/tests/phpunit/includes/XmlTest.php @@ -0,0 +1,179 @@ +<?php + +class XmlTest extends PHPUnit_Framework_TestCase { + + public function testExpandAttributes() { + $this->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( + '<element>', + Xml::element( 'element', null, null ), + 'Opening element with no attributes' + ); + } + + function testElementEmpty() { + $this->assertEquals( + '<element />', + Xml::element( 'element', null, '' ), + 'Terminated empty element' + ); + } + + function testElementInputCanHaveAValueOfZero() { + $this->assertEquals( + '<input name="name" value="0" />', + Xml::input( 'name', false, 0 ), + 'Input with a value of 0 (bug 23797)' + ); + } + function testElementEscaping() { + $this->assertEquals( + '<element>hello <there> you & you</element>', + Xml::element( 'element', null, 'hello <there> 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( + '<element key="value" <>="<>">', + Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ), + 'Element attributes, keys are not escaped' + ); + } + + function testOpenElement() { + $this->assertEquals( + '<element k="v">', + Xml::openElement( 'element', array( 'k' => 'v' ) ), + 'openElement() shortcut' + ); + } + + function testCloseElement() { + $this->assertEquals( '</element>', Xml::closeElement( 'element' ), 'closeElement() shortcut' ); + } + + # + # textarea + # + function testTextareaNoContent() { + $this->assertEquals( + '<textarea name="name" id="name" cols="40" rows="5"></textarea>', + Xml::textarea( 'name', '' ), + 'textarea() with not content' + ); + } + + function testTextareaAttribs() { + $this->assertEquals( + '<textarea name="name" id="name" cols="20" rows="10"><txt></textarea>', + Xml::textarea( 'name', '<txt>', 20, 10 ), + 'textarea() with custom attribs' + ); + } + + # + # input and label + # + function testLabelCreation() { + $this->assertEquals( + '<label for="id">name</label>', + Xml::label( 'name', 'id' ), + 'label() with no attribs' + ); + } + function testLabelAttributeCanOnlyBeClass() { + $this->assertEquals( + '<label for="id">name</label>', + Xml::label( 'name', 'id', array( 'generated' => true ) ), + 'label() can not be given a generated attribute' + ); + $this->assertEquals( + '<label for="id" class="nice">name</label>', + Xml::label( 'name', 'id', array( 'class' => 'nice' ) ), + 'label() can get a class attribute' + ); + $this->assertEquals( + '<label for="id" class="nice">name</label>', + 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 000000000000..f004f3c6bea0 --- /dev/null +++ b/tests/phpunit/includes/api/ApiSetup.php @@ -0,0 +1,65 @@ +<?php + +abstract class ApiTestSetup extends PHPUnit_Framework_TestCase { + protected static $user; + protected static $sysopUser; + protected static $apiUrl; + + function setUp() { + global $wgServer, $wgContLang, $wgAuth, $wgMemc, $wgRequest; + + self::$apiUrl = $wgServer . wfScript( 'api' ); + + $wgMemc = new FakeMemCachedClient; + $wgContLang = Language::factory( 'en' ); + $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); + $wgRequest = new FauxRequest( array() ); + self::setupUser(); + } + + protected function doApiRequest( $params, $data = null ) { + $_SESSION = isset( $data[2] ) ? $data[2] : array(); + + $req = new FauxRequest( $params, true, $_SESSION ); + $module = new ApiMain( $req, true ); + $module->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 000000000000..5df834dbbcd5 --- /dev/null +++ b/tests/phpunit/includes/api/ApiTest.php @@ -0,0 +1,227 @@ +<?php + +require_once( dirname( __FILE__ ) . '/ApiSetup.php' ); + +class MockApi extends ApiBase { + public function execute() { } + public function getVersion() { } + + public function __construct() { } + + public function getAllowedParams() { + return array( + 'filename' => 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 000000000000..a2da6f8927e3 --- /dev/null +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -0,0 +1,671 @@ +<?php + +/** + * @group Database + * @group Destructive + */ + +/** + * n.b. Ensure that you can write to the images/ directory as the + * user that will run tests. + */ + +// Note for reviewers: this intentionally duplicates functionality already in "ApiSetup" and so on. +// This framework works better IMO and has less strangeness (such as test cases inheriting from "ApiSetup"...) +// (and in the case of the other Upload tests, this flat out just actually works... ) + +// TODO: refactor into several files +// TODO: port the other Upload tests, and other API tests to this framework + +require_once( dirname( __FILE__ ) . '/RandomImageGenerator.php' ); +require_once( dirname( __FILE__ ) . '/../../../../../includes/User.php' ); + +/* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */ +class ApiTestUser { + public $username; + public $password; + public $email; + public $groups; + public $user; + + function __construct( $username, $realname = 'Real Name', $email = 'sample@sample.com', $groups = array() ) { + $this->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 000000000000..0742a0af4e52 --- /dev/null +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -0,0 +1,237 @@ +<?php + +require_once dirname( __FILE__ ) . '/ApiSetup.php'; + +/** + * @group Database + * @group Destructive + */ +class ApiWatchTest extends ApiTestSetup { + + function setUp() { + parent::setUp(); + } + + function testLogin() { + $data = $this->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 000000000000..6bb9d00feebf --- /dev/null +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -0,0 +1,287 @@ +<?php + +/* + * RandomImageGenerator -- does what it says on the tin. + * Requires Imagick, the ImageMagick library for PHP, or the command line equivalent (usually 'convert'). + * + * Because MediaWiki tests the uniqueness of media upload content, and filenames, it is sometimes useful to generate + * files that are guaranteed (or at least very likely) to be unique in both those ways. + * This generates a number of filenames with random names and random content (colored circles) + * + * It is also useful to have fresh content because our tests currently run in a "destructive" mode, and don't create a fresh new wiki for each + * test run. + * Consequently, if we just had a few static files we kept re-uploading, we'd get lots of warnings about matching content or filenames, + * and even if we deleted those files, we'd get warnings about archived files. + * + * This can also be used with a cronjob to generate random files all the time -- I use it to have a constant, never ending supply when I'm + * testing interactively. + * + * @file + * @author Neil Kandalgaonkar <neilk@wikimedia.org> + */ + +/** + * 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 000000000000..cab1ec7de65b --- /dev/null +++ b/tests/phpunit/includes/api/generateRandomImages.php @@ -0,0 +1,25 @@ +<?php + +require("RandomImageGenerator.php"); + +$getOptSpec = array( + 'dictionaryFile::', + 'minWidth::', + 'maxWidth::', + 'minHeight::', + 'maxHeight::', + 'circlesToDraw::', + + 'number::', + 'format::' +); +$options = getopt( null, $getOptSpec ); + +$format = isset( $options['format'] ) ? $options['format'] : 'jpg'; +unset( $options['format'] ); + +$number = isset( $options['number'] ) ? intval( $options['number'] ) : 10; +unset( $options['number'] ); + +$randomImageGenerator = new RandomImageGenerator( $options ); +$randomImageGenerator->writeImages( $number, $format ); diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php new file mode 100644 index 000000000000..4cd3daf04a70 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -0,0 +1,87 @@ +<?php + +class MockDatabaseSqlite extends DatabaseSqliteStandalone { + var $lastQuery; + + function __construct( ) { + parent::__construct( ':memory:' ); + } + + function query( $sql, $fname = '', $tempIgnore = false ) { + $this->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 000000000000..b184331ff464 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseTest.php @@ -0,0 +1,95 @@ +<?php + +/** + * @group Database + */ +class DatabaseTest extends PHPUnit_Framework_TestCase { + var $db; + + function setUp() { + $this->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 000000000000..3825a8fabf50 --- /dev/null +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -0,0 +1,73 @@ +<?php + +require_once( dirname( __FILE__ ) . '/ParserHelpers.php' ); +require_once( dirname(dirname(dirname( __FILE__ ))) . '/bootstrap.php' ); + +class MediaWikiParserTest extends MediaWikiTestSetup { + public $count; // Number of tests in the suite. + public $backend; // ParserTestSuiteBackend instance + public $articles = array(); // Array of test articles defined by the tests + + public function __construct() { + $suite = new PHPUnit_Framework_TestSuite('Parser Tests'); + parent::__construct($suite); + $this->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 000000000000..43da34218b09 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserHelpers.php @@ -0,0 +1,132 @@ +<?php + +class PHPUnitParserTest extends ParserTest { + function showTesting( $desc ) { + /* Do nothing since we don't want to show info during PHPUnit testing. */ + } + + public function showSuccess( $desc ) { + PHPUnit_Framework_Assert::assertTrue( true, $desc ); + return true; + } + + public function showFailure( $desc, $expected, $got ) { + PHPUnit_Framework_Assert::assertEquals( $expected, $got, $desc ); + return false; + } +} + +class ParserUnitTest extends PHPUnit_Framework_TestCase { + private $test = ""; + private $suite; + + public function __construct( $suite, $test = null ) { + $this->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 000000000000..daf9747fe656 --- /dev/null +++ b/tests/phpunit/includes/search/SearchDbTest.php @@ -0,0 +1,37 @@ +<?php + +require_once( dirname( __FILE__ ) . '/SearchEngineTest.php' ); + +/** + * @group Search + * @group Destructive + */ +class SearchDbTest extends SearchEngineTest { + var $db; + + function setUp() { + // Get a database connection or skip test + $this->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 000000000000..b97afb672801 --- /dev/null +++ b/tests/phpunit/includes/search/SearchEngineTest.php @@ -0,0 +1,195 @@ +<?php + +require_once dirname(dirname(dirname(__FILE__))). '/bootstrap.php'; + +/** + * This class is not directly tested. Instead it is extended by SearchDbTest. + * @group Search + * @group Stub + * @group Destructive + */ +class SearchEngineTest extends MediaWikiTestSetup { + var $db, $search, $pageList; + + /* + * Checks for database type & version. + * Will skip current test if DB does not support search. + */ + function setUp() { + // Search tests require MySQL or SQLite with FTS + # Get database type and version + $dbType = $this->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 000000000000..108b28027090 --- /dev/null +++ b/tests/phpunit/includes/search/SearchUpdateTest.php @@ -0,0 +1,80 @@ +<?php + +class MockSearch extends SearchEngine { + public static $id; + public static $title; + public static $text; + + public function __construct( $db ) { + } + + public function update( $id, $title, $text ) { + self::$id = $id; + self::$title = $title; + self::$text = $text; + } +} + +/** + * @group Search + */ +class SearchUpdateTest extends PHPUnit_Framework_TestCase { + static $searchType; + + function update( $text, $title = 'Test', $id = 1 ) { + $u = new SearchUpdate( $id, $title, $text ); + $u->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( '<div>TeSt</div>' ), + 'HTML stripped, text lowercased' + ); + + $this->assertEquals( + 'foo bar boz quux', + $this->updateText( <<<EOT +<table style="color:red; font-size:100px"> + <tr class="scary"><td><div>foo</div></td><tr>bar</td></tr> + <tr><td>boz</td><tr>quux</td></tr> +</table> +EOT + ), 'Stripping HTML tables' ); + + $this->assertEquals( + 'a b', + $this->updateText( 'a > b' ), + 'Handle unclosed tags' + ); + + $text = str_pad( "foo <barbarbar \n", 10000, 'x' ); + + $this->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 000000000000..aca7ace6e527 --- /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 000000000000..bf01e14ab739 --- /dev/null +++ b/tests/phpunit/languages/LanguageBe_taraskTest.php @@ -0,0 +1,31 @@ +<?php +require_once dirname(dirname(__FILE__)). '/bootstrap.php'; + +class LanguageBeTaraskTest extends MediaWikiTestSetup { + private $lang; + + function setUp() { + $this->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 000000000000..8e30259b17a8 --- /dev/null +++ b/tests/phpunit/languages/LanguageTest.php @@ -0,0 +1,61 @@ +<?php +require_once dirname(dirname(__FILE__)). '/bootstrap.php'; + +class LanguageTest extends MediaWikiTestSetup { + private $lang; + + function setUp() { + $this->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 000000000000..94d4ebd2d603 --- /dev/null +++ b/tests/phpunit/phpunit.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +<?php +/** + * Bootstrapping for MediaWiki PHPUnit tests + * + * @file + */ + +/* Configuration */ + +// Evaluate the include path relative to this file +$IP = dirname( dirname( dirname( dirname( __FILE__ ) ) ) ); + +// Set a flag which can be used to detect when other scripts have been entered through this entry point or not +define( 'MW_PHPUNIT_TEST', true ); + +// Start up MediaWiki in command-line mode +require_once( "$IP/maintenance/commandLine.inc" ); + +// Assume UTC for testing purposes +$wgLocaltimezone = 'UTC'; + +require_once( 'PHPUnit/Runner/Version.php' ); +if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '>=' ) ) { + # 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 000000000000..e6eb3e0cfc00 --- /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 000000000000..cb25963bdac9 --- /dev/null +++ b/tests/phpunit/suite.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit bootstrap="./bootstrap.php" + colors="false" + backupGlobals="false" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + stopOnFailure="false" + strict="true"> + <testsuites> + <testsuite name="includes"> + <directory>./includes</directory> + </testsuite> + <testsuite name="languages"> + <directory>./languages</directory> + </testsuite> + <testsuite name="skins"> + <directory>./skins</directory> + </testsuite> + <testsuite name="uploadfromurl"> + <file>./suites/UploadFromUrlTestSuite.php</file> + </testsuite> + <testsuite name="extensions"> + <file>./suites/ExtensionsTestSuite.php</file> + </testsuite> + </testsuites> + <groups> + <exclude> + <group>Utility</group> + <group>Broken</group> + <group>Stub</group> + </exclude> + </groups> +</phpunit> diff --git a/tests/phpunit/suites/ExtensionsTestSuite.php b/tests/phpunit/suites/ExtensionsTestSuite.php new file mode 100644 index 000000000000..4bf35427993e --- /dev/null +++ b/tests/phpunit/suites/ExtensionsTestSuite.php @@ -0,0 +1,33 @@ +<?php +/** + * This test suite runs unit tests registered by extensions. + * See http://www.mediawiki.org/wiki/Manual:Hooks/UnitTestsList for details of how to register your tests. + */ + +class ExtensionsTestSuite extends PHPUnit_Framework_TestSuite { + public function __construct() { + parent::__construct(); + $files = array(); + wfRunHooks( 'UnitTestsList', array( &$files ) ); + foreach ( $files as $file ) { + $this->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 000000000000..3f5b1fe53ea8 --- /dev/null +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -0,0 +1,181 @@ +<?php + +require_once( dirname( dirname( __FILE__ ) ) . '/includes/UploadFromUrlTest.php' ); + +class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { + public static function addTables( &$tables ) { + $tables[] = 'user_properties'; + $tables[] = 'filearchive'; + $tables[] = 'logging'; + $tables[] = 'updatelog'; + $tables[] = 'iwlinks'; + + return true; + } + + 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 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; + } +} |