summaryrefslogtreecommitdiff
path: root/tests/phpunit/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2014-03-28 05:41:12 +0100
committerPierre Schmitz <pierre@archlinux.de>2014-03-28 05:41:12 +0100
commit9441dde8bfb95277df073717ed7817dced40f948 (patch)
treeba31c10147e91bda378b77c8cae90c5651af9bfe /tests/phpunit/includes
parentd417de70fcf39e0a7a15ba780b597914d16ca0f7 (diff)
Update to MediaWiki 1.22.5
Diffstat (limited to 'tests/phpunit/includes')
-rw-r--r--tests/phpunit/includes/ArticleTablesTest.php32
-rw-r--r--tests/phpunit/includes/ArticleTest.php92
-rw-r--r--tests/phpunit/includes/BlockTest.php354
-rw-r--r--tests/phpunit/includes/CdbTest.php88
-rw-r--r--tests/phpunit/includes/CollationTest.php111
-rw-r--r--tests/phpunit/includes/DiffHistoryBlobTest.php44
-rw-r--r--tests/phpunit/includes/EditPageTest.php491
-rw-r--r--tests/phpunit/includes/ExternalStoreTest.php81
-rw-r--r--tests/phpunit/includes/ExtraParserTest.php157
-rw-r--r--tests/phpunit/includes/FallbackTest.php73
-rw-r--r--tests/phpunit/includes/FauxRequestTest.php15
-rw-r--r--tests/phpunit/includes/FauxResponseTest.php74
-rw-r--r--tests/phpunit/includes/FormOptionsInitializationTest.php84
-rw-r--r--tests/phpunit/includes/FormOptionsTest.php91
-rw-r--r--tests/phpunit/includes/GlobalFunctions/GlobalTest.php652
-rw-r--r--tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php31
-rw-r--r--tests/phpunit/includes/GlobalFunctions/README2
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php111
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php120
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php182
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php36
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php111
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php40
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php146
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php91
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php30
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php134
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php119
-rw-r--r--tests/phpunit/includes/HTMLCheckMatrixTest.php102
-rw-r--r--tests/phpunit/includes/HashRingTest.php53
-rw-r--r--tests/phpunit/includes/HooksTest.php158
-rw-r--r--tests/phpunit/includes/HtmlFormatterTest.php81
-rw-r--r--tests/phpunit/includes/HtmlTest.php631
-rw-r--r--tests/phpunit/includes/HttpTest.php216
-rw-r--r--tests/phpunit/includes/IPTest.php595
-rw-r--r--tests/phpunit/includes/LanguageConverterTest.php148
-rw-r--r--tests/phpunit/includes/LicensesTest.php22
-rw-r--r--tests/phpunit/includes/LinkerTest.php71
-rw-r--r--tests/phpunit/includes/LinksUpdateTest.php175
-rw-r--r--tests/phpunit/includes/LocalFileTest.php107
-rw-r--r--tests/phpunit/includes/LocalisationCacheTest.php31
-rw-r--r--tests/phpunit/includes/MWExceptionHandlerTest.php73
-rw-r--r--tests/phpunit/includes/MWFunctionTest.php30
-rw-r--r--tests/phpunit/includes/MWNamespaceTest.php571
-rw-r--r--tests/phpunit/includes/MessageTest.php130
-rw-r--r--tests/phpunit/includes/OutputPageTest.php133
-rw-r--r--tests/phpunit/includes/PathRouterTest.php263
-rw-r--r--tests/phpunit/includes/PreferencesTest.php91
-rw-r--r--tests/phpunit/includes/RecentChangeTest.php280
-rw-r--r--tests/phpunit/includes/RequestContextTest.php73
-rw-r--r--tests/phpunit/includes/ResourceLoaderTest.php149
-rw-r--r--tests/phpunit/includes/RevisionStorageTest.php546
-rw-r--r--tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php81
-rw-r--r--tests/phpunit/includes/RevisionTest.php481
-rw-r--r--tests/phpunit/includes/SampleTest.php105
-rw-r--r--tests/phpunit/includes/SanitizerTest.php302
-rw-r--r--tests/phpunit/includes/SanitizerValidateEmailTest.php103
-rw-r--r--tests/phpunit/includes/SiteConfigurationTest.php335
-rw-r--r--tests/phpunit/includes/StringUtilsTest.php147
-rw-r--r--tests/phpunit/includes/TemplateCategoriesTest.php53
-rw-r--r--tests/phpunit/includes/TestUser.php59
-rw-r--r--tests/phpunit/includes/TimeAdjustTest.php41
-rw-r--r--tests/phpunit/includes/TimestampTest.php304
-rw-r--r--tests/phpunit/includes/TitleMethodsTest.php300
-rw-r--r--tests/phpunit/includes/TitlePermissionTest.php742
-rw-r--r--tests/phpunit/includes/TitleTest.php485
-rw-r--r--tests/phpunit/includes/UIDGeneratorTest.php98
-rw-r--r--tests/phpunit/includes/UserMailerTest.php14
-rw-r--r--tests/phpunit/includes/UserTest.php237
-rw-r--r--tests/phpunit/includes/WebRequestTest.php310
-rw-r--r--tests/phpunit/includes/WikiPageTest.php1074
-rw-r--r--tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php53
-rw-r--r--tests/phpunit/includes/XmlJsTest.php24
-rw-r--r--tests/phpunit/includes/XmlSelectTest.php173
-rw-r--r--tests/phpunit/includes/XmlTest.php402
-rw-r--r--tests/phpunit/includes/XmlTypeCheckTest.php30
-rw-r--r--tests/phpunit/includes/ZipDirectoryReaderTest.php85
-rw-r--r--tests/phpunit/includes/api/ApiAccountCreationTest.php159
-rw-r--r--tests/phpunit/includes/api/ApiBlockTest.php95
-rw-r--r--tests/phpunit/includes/api/ApiEditPageTest.php417
-rw-r--r--tests/phpunit/includes/api/ApiOptionsTest.php420
-rw-r--r--tests/phpunit/includes/api/ApiParseTest.php29
-rw-r--r--tests/phpunit/includes/api/ApiPurgeTest.php40
-rw-r--r--tests/phpunit/includes/api/ApiTest.php259
-rw-r--r--tests/phpunit/includes/api/ApiTestCase.php253
-rw-r--r--tests/phpunit/includes/api/ApiTestCaseUpload.php149
-rw-r--r--tests/phpunit/includes/api/ApiUploadTest.php561
-rw-r--r--tests/phpunit/includes/api/ApiWatchTest.php148
-rw-r--r--tests/phpunit/includes/api/PrefixUniquenessTest.php25
-rw-r--r--tests/phpunit/includes/api/RandomImageGenerator.php468
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatPhpTest.php17
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatTestBase.php22
-rw-r--r--tests/phpunit/includes/api/generateRandomImages.php46
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryBasicTest.php395
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryContinue2Test.php68
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryContinueTest.php313
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php209
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php39
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryTest.php66
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryTestBase.php150
-rw-r--r--tests/phpunit/includes/api/words.txt1000
-rw-r--r--tests/phpunit/includes/cache/GenderCacheTest.php104
-rw-r--r--tests/phpunit/includes/cache/MessageCacheTest.php128
-rw-r--r--tests/phpunit/includes/cache/ProcessCacheLRUTest.php237
-rw-r--r--tests/phpunit/includes/content/ContentHandlerTest.php451
-rw-r--r--tests/phpunit/includes/content/CssContentTest.php87
-rw-r--r--tests/phpunit/includes/content/JavaScriptContentTest.php287
-rw-r--r--tests/phpunit/includes/content/TextContentTest.php458
-rw-r--r--tests/phpunit/includes/content/WikitextContentHandlerTest.php227
-rw-r--r--tests/phpunit/includes/content/WikitextContentTest.php404
-rw-r--r--tests/phpunit/includes/db/DatabaseMysqlBaseTest.php209
-rw-r--r--tests/phpunit/includes/db/DatabaseSQLTest.php721
-rw-r--r--tests/phpunit/includes/db/DatabaseSqliteTest.php421
-rw-r--r--tests/phpunit/includes/db/DatabaseTest.php234
-rw-r--r--tests/phpunit/includes/db/DatabaseTestHelper.php166
-rw-r--r--tests/phpunit/includes/db/ORMRowTest.php226
-rw-r--r--tests/phpunit/includes/db/ORMTableTest.php146
-rw-r--r--tests/phpunit/includes/db/TestORMRowTest.php215
-rw-r--r--tests/phpunit/includes/debug/MWDebugTest.php72
-rw-r--r--tests/phpunit/includes/filebackend/FileBackendTest.php2306
-rw-r--r--tests/phpunit/includes/filerepo/FileRepoTest.php55
-rw-r--r--tests/phpunit/includes/filerepo/StoreBatchTest.php134
-rw-r--r--tests/phpunit/includes/installer/InstallDocFormatterTest.php65
-rw-r--r--tests/phpunit/includes/installer/OracleInstallerTest.php48
-rw-r--r--tests/phpunit/includes/jobqueue/JobQueueTest.php329
-rw-r--r--tests/phpunit/includes/json/FormatJsonTest.php161
-rw-r--r--tests/phpunit/includes/libs/CSSJanusTest.php606
-rw-r--r--tests/phpunit/includes/libs/CSSMinTest.php133
-rw-r--r--tests/phpunit/includes/libs/GenericArrayObjectTest.php263
-rw-r--r--tests/phpunit/includes/libs/IEUrlExtensionTest.php126
-rw-r--r--tests/phpunit/includes/libs/JavaScriptMinifierTest.php170
-rw-r--r--tests/phpunit/includes/logging/LogFormatterTest.php207
-rw-r--r--tests/phpunit/includes/logging/LogTests.i18n.php15
-rw-r--r--tests/phpunit/includes/media/BitmapMetadataHandlerTest.php167
-rw-r--r--tests/phpunit/includes/media/BitmapScalingTest.php137
-rw-r--r--tests/phpunit/includes/media/ExifBitmapTest.php139
-rw-r--r--tests/phpunit/includes/media/ExifRotationTest.php248
-rw-r--r--tests/phpunit/includes/media/ExifTest.php46
-rw-r--r--tests/phpunit/includes/media/FakeDimensionFile.php28
-rw-r--r--tests/phpunit/includes/media/FormatMetadataTest.php59
-rw-r--r--tests/phpunit/includes/media/GIFMetadataExtractorTest.php107
-rw-r--r--tests/phpunit/includes/media/GIFTest.php120
-rw-r--r--tests/phpunit/includes/media/IPTCTest.php81
-rw-r--r--tests/phpunit/includes/media/JpegMetadataExtractorTest.php109
-rw-r--r--tests/phpunit/includes/media/JpegTest.php33
-rw-r--r--tests/phpunit/includes/media/MediaHandlerTest.php53
-rw-r--r--tests/phpunit/includes/media/PNGMetadataExtractorTest.php155
-rw-r--r--tests/phpunit/includes/media/PNGTest.php123
-rw-r--r--tests/phpunit/includes/media/SVGMetadataExtractorTest.php112
-rw-r--r--tests/phpunit/includes/media/TiffTest.php39
-rw-r--r--tests/phpunit/includes/media/XMPTest.php166
-rw-r--r--tests/phpunit/includes/media/XMPValidateTest.php46
-rw-r--r--tests/phpunit/includes/normal/CleanUpTest.php409
-rw-r--r--tests/phpunit/includes/objectcache/BagOStuffTest.php149
-rw-r--r--tests/phpunit/includes/parser/MagicVariableTest.php242
-rw-r--r--tests/phpunit/includes/parser/MediaWikiParserTest.php120
-rw-r--r--tests/phpunit/includes/parser/NewParserTest.php969
-rw-r--r--tests/phpunit/includes/parser/ParserMethodsTest.php95
-rw-r--r--tests/phpunit/includes/parser/ParserOutputTest.php59
-rw-r--r--tests/phpunit/includes/parser/ParserPreloadTest.php80
-rw-r--r--tests/phpunit/includes/parser/PreprocessorTest.php239
-rw-r--r--tests/phpunit/includes/parser/TagHooksTest.php82
-rw-r--r--tests/phpunit/includes/parser/TidyTest.php44
-rw-r--r--tests/phpunit/includes/search/SearchEngineTest.php176
-rw-r--r--tests/phpunit/includes/search/SearchUpdateTest.php74
-rw-r--r--tests/phpunit/includes/site/MediaWikiSiteTest.php90
-rw-r--r--tests/phpunit/includes/site/SiteListTest.php197
-rw-r--r--tests/phpunit/includes/site/SiteSQLStoreTest.php134
-rw-r--r--tests/phpunit/includes/site/SiteTest.php296
-rw-r--r--tests/phpunit/includes/site/TestSites.php100
-rw-r--r--tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php79
-rw-r--r--tests/phpunit/includes/specials/SpecialPreferencesTest.php60
-rw-r--r--tests/phpunit/includes/specials/SpecialRecentchangesTest.php125
-rw-r--r--tests/phpunit/includes/specials/SpecialSearchTest.php139
-rw-r--r--tests/phpunit/includes/upload/UploadBaseTest.php147
-rw-r--r--tests/phpunit/includes/upload/UploadFromUrlTest.php350
-rw-r--r--tests/phpunit/includes/upload/UploadStashTest.php76
177 files changed, 35617 insertions, 0 deletions
diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php
new file mode 100644
index 00000000..469d1d19
--- /dev/null
+++ b/tests/phpunit/includes/ArticleTablesTest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ArticleTablesTest extends MediaWikiLangTestCase {
+
+ public function testbug14404() {
+ global $wgContLang, $wgLanguageCode, $wgLang;
+
+ $title = Title::newFromText( 'Bug 14404' );
+ $page = WikiPage::factory( $title );
+ $user = new User();
+ $user->mRights = array( 'createpage', 'edit', 'purge' );
+ $wgLanguageCode = 'es';
+ $wgContLang = Language::factory( 'es' );
+
+ $wgLang = Language::factory( 'fr' );
+ $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', 0, false, $user );
+ $templates1 = $title->getTemplateLinksFrom();
+
+ $wgLang = Language::factory( 'de' );
+ $page = WikiPage::factory( $title ); // In order to force the rerendering of the same wikitext
+
+ // We need an edit, a purge is not enough to regenerate the tables
+ $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', EDIT_UPDATE, false, $user );
+ $templates2 = $title->getTemplateLinksFrom();
+
+ $this->assertEquals( $templates1, $templates2 );
+ $this->assertEquals( $templates1[0]->getFullText(), 'Historial' );
+ }
+}
diff --git a/tests/phpunit/includes/ArticleTest.php b/tests/phpunit/includes/ArticleTest.php
new file mode 100644
index 00000000..b4d6dca6
--- /dev/null
+++ b/tests/phpunit/includes/ArticleTest.php
@@ -0,0 +1,92 @@
+<?php
+
+class ArticleTest extends MediaWikiTestCase {
+
+ /**
+ * @var Title
+ */
+ private $title;
+ /**
+ * @var Article
+ */
+ private $article;
+
+ /** creates a title object and its article object */
+ protected function setUp() {
+ parent::setUp();
+ $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
+ $this->article = new Article( $this->title );
+ }
+
+ /** cleanup title object and its article object */
+ protected function tearDown() {
+ parent::tearDown();
+ $this->title = null;
+ $this->article = null;
+ }
+
+ public function testImplementsGetMagic() {
+ $this->assertEquals( false, $this->article->mLatest, "Article __get magic" );
+ }
+
+ /**
+ * @depends testImplementsGetMagic
+ */
+ public function testImplementsSetMagic() {
+ $this->article->mLatest = 2;
+ $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" );
+ }
+
+ /**
+ * @depends testImplementsSetMagic
+ */
+ public function testImplementsCallMagic() {
+ $this->article->mLatest = 33;
+ $this->article->mDataLoaded = true;
+ $this->assertEquals( 33, $this->article->getLatest(), "Article __call magic" );
+ }
+
+ public function testGetOrSetOnNewProperty() {
+ $this->article->ext_someNewProperty = 12;
+ $this->assertEquals( 12, $this->article->ext_someNewProperty,
+ "Article get/set magic on new field" );
+
+ $this->article->ext_someNewProperty = -8;
+ $this->assertEquals( -8, $this->article->ext_someNewProperty,
+ "Article get/set magic on update to new field" );
+ }
+
+ /**
+ * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class)
+ */
+ public function testStaticFunctions() {
+ $this->hideDeprecated( 'Article::getAutosummary' );
+ $this->hideDeprecated( 'WikiPage::getAutosummary' );
+ $this->hideDeprecated( 'CategoryPage::getAutosummary' ); // Inherited from Article
+
+ $this->assertEquals( WikiPage::selectFields(), Article::selectFields(),
+ "Article static functions" );
+ $this->assertEquals( true, is_callable( "Article::onArticleCreate" ),
+ "Article static functions" );
+ $this->assertEquals( true, is_callable( "Article::onArticleDelete" ),
+ "Article static functions" );
+ $this->assertEquals( true, is_callable( "ImagePage::onArticleEdit" ),
+ "Article static functions" );
+ $this->assertTrue( is_string( CategoryPage::getAutosummary( '', '', 0 ) ),
+ "Article static functions" );
+ }
+
+ public function testWikiPageFactory() {
+ $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
+ $page = WikiPage::factory( $title );
+ $this->assertEquals( 'WikiFilePage', get_class( $page ) );
+
+ $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
+ $page = WikiPage::factory( $title );
+ $this->assertEquals( 'WikiCategoryPage', get_class( $page ) );
+
+ $title = Title::makeTitle( NS_MAIN, 'SomePage' );
+ $page = WikiPage::factory( $title );
+ $this->assertEquals( 'WikiPage', get_class( $page ) );
+ }
+}
diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php
new file mode 100644
index 00000000..21de0985
--- /dev/null
+++ b/tests/phpunit/includes/BlockTest.php
@@ -0,0 +1,354 @@
+<?php
+
+/**
+ * @group Database
+ * @group Blocking
+ */
+class BlockTest extends MediaWikiLangTestCase {
+
+ private $block, $madeAt;
+
+ /* variable used to save up the blockID we insert in this test suite */
+ private $blockId;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => Language::factory( 'en' )
+ ) );
+ }
+
+ function addDBData() {
+
+ $user = User::newFromName( 'UTBlockee' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTBlockeePassword' );
+
+ $user->saveSettings();
+ }
+
+ // Delete the last round's block if it's still there
+ $oldBlock = Block::newFromTarget( 'UTBlockee' );
+ if ( $oldBlock ) {
+ // An old block will prevent our new one from saving.
+ $oldBlock->delete();
+ }
+
+ $this->block = new Block( 'UTBlockee', $user->getID(), 0,
+ 'Parce que', 0, false, time() + 100500
+ );
+ $this->madeAt = wfTimestamp( TS_MW );
+
+ $this->block->insert();
+ // save up ID for use in assertion. Since ID is an autoincrement,
+ // its value might change depending on the order the tests are run.
+ // ApiBlockTest insert its own blocks!
+ $newBlockId = $this->block->getId();
+ if ( $newBlockId ) {
+ $this->blockId = $newBlockId;
+ } else {
+ throw new MWException( "Failed to insert block for BlockTest; old leftover block remaining?" );
+ }
+
+ $this->addXffBlocks();
+ }
+
+ /**
+ * debug function : dump the ipblocks table
+ */
+ function dumpBlocks() {
+ $v = $this->db->select( 'ipblocks', '*' );
+ print "Got " . $v->numRows() . " rows. Full dump follow:\n";
+ foreach ( $v as $row ) {
+ print_r( $row );
+ }
+ }
+
+ public function testInitializerFunctionsReturnCorrectBlock() {
+ // $this->dumpBlocks();
+
+ $this->assertTrue( $this->block->equals( Block::newFromTarget( 'UTBlockee' ) ), "newFromTarget() returns the same block as the one that was made" );
+
+ $this->assertTrue( $this->block->equals( Block::newFromID( $this->blockId ) ), "newFromID() returns the same block as the one that was made" );
+ }
+
+ /**
+ * per bug 26425
+ */
+ public function testBug26425BlockTimestampDefaultsToTime() {
+ // delta to stop one-off errors when things happen to go over a second mark.
+ $delta = abs( $this->madeAt - $this->block->mTimestamp );
+ $this->assertLessThan( 2, $delta, "If no timestamp is specified, the block is recorded as time()" );
+ }
+
+ /**
+ * This is the method previously used to load block info in CheckUser etc
+ * passing an empty value (empty string, null, etc) as the ip parameter bypasses IP lookup checks.
+ *
+ * This stopped working with r84475 and friends: regression being fixed for bug 29116.
+ *
+ * @dataProvider provideBug29116Data
+ */
+ public function testBug29116LoadWithEmptyIp( $vagueTarget ) {
+ $this->hideDeprecated( 'Block::load' );
+
+ $uid = User::idFromName( 'UTBlockee' );
+ $this->assertTrue( ( $uid > 0 ), 'Must be able to look up the target user during tests' );
+
+ $block = new Block();
+ $ok = $block->load( $vagueTarget, $uid );
+ $this->assertTrue( $ok, "Block->load() with empty IP and user ID '$uid' should return a block" );
+
+ $this->assertTrue( $this->block->equals( $block ), "Block->load() returns the same block as the one that was made when given empty ip param " . var_export( $vagueTarget, true ) );
+ }
+
+ /**
+ * CheckUser since being changed to use Block::newFromTarget started failing
+ * because the new function didn't accept empty strings like Block::load()
+ * had. Regression bug 29116.
+ *
+ * @dataProvider provideBug29116Data
+ */
+ public function testBug29116NewFromTargetWithEmptyIp( $vagueTarget ) {
+ $block = Block::newFromTarget( 'UTBlockee', $vagueTarget );
+ $this->assertTrue( $this->block->equals( $block ), "newFromTarget() returns the same block as the one that was made when given empty vagueTarget param " . var_export( $vagueTarget, true ) );
+ }
+
+ public static function provideBug29116Data() {
+ return array(
+ array( null ),
+ array( '' ),
+ array( false )
+ );
+ }
+
+ public function testBlockedUserCanNotCreateAccount() {
+ $username = 'BlockedUserToCreateAccountWith';
+ $u = User::newFromName( $username );
+ $u->setPassword( 'NotRandomPass' );
+ $u->addToDatabase();
+ unset( $u );
+
+ // Sanity check
+ $this->assertNull(
+ Block::newFromTarget( $username ),
+ "$username should not be blocked"
+ );
+
+ // Reload user
+ $u = User::newFromName( $username );
+ $this->assertFalse(
+ $u->isBlockedFromCreateAccount(),
+ "Our sandbox user should be able to create account before being blocked"
+ );
+
+ // Foreign perspective (blockee not on current wiki)...
+ $block = new Block(
+ /* $address */ $username,
+ /* $user */ 14146,
+ /* $by */ 0,
+ /* $reason */ 'crosswiki block...',
+ /* $timestamp */ wfTimestampNow(),
+ /* $auto */ false,
+ /* $expiry */ $this->db->getInfinity(),
+ /* anonOnly */ false,
+ /* $createAccount */ true,
+ /* $enableAutoblock */ true,
+ /* $hideName (ipb_deleted) */ true,
+ /* $blockEmail */ true,
+ /* $allowUsertalk */ false,
+ /* $byName */ 'MetaWikiUser'
+ );
+ $block->insert();
+
+ // Reload block from DB
+ $userBlock = Block::newFromTarget( $username );
+ $this->assertTrue(
+ (bool)$block->prevents( 'createaccount' ),
+ "Block object in DB should prevents 'createaccount'"
+ );
+
+ $this->assertInstanceOf(
+ 'Block',
+ $userBlock,
+ "'$username' block block object should be existent"
+ );
+
+ // Reload user
+ $u = User::newFromName( $username );
+ $this->assertTrue(
+ (bool)$u->isBlockedFromCreateAccount(),
+ "Our sandbox user '$username' should NOT be able to create account"
+ );
+ }
+
+ public function testCrappyCrossWikiBlocks() {
+ // Delete the last round's block if it's still there
+ $oldBlock = Block::newFromTarget( 'UserOnForeignWiki' );
+ if ( $oldBlock ) {
+ // An old block will prevent our new one from saving.
+ $oldBlock->delete();
+ }
+
+ // Foreign perspective (blockee not on current wiki)...
+ $block = new Block(
+ /* $address */ 'UserOnForeignWiki',
+ /* $user */ 14146,
+ /* $by */ 0,
+ /* $reason */ 'crosswiki block...',
+ /* $timestamp */ wfTimestampNow(),
+ /* $auto */ false,
+ /* $expiry */ $this->db->getInfinity(),
+ /* anonOnly */ false,
+ /* $createAccount */ true,
+ /* $enableAutoblock */ true,
+ /* $hideName (ipb_deleted) */ true,
+ /* $blockEmail */ true,
+ /* $allowUsertalk */ false,
+ /* $byName */ 'MetaWikiUser'
+ );
+
+ $res = $block->insert( $this->db );
+ $this->assertTrue( (bool)$res['id'], 'Block succeeded' );
+
+ // Local perspective (blockee on current wiki)...
+ $user = User::newFromName( 'UserOnForeignWiki' );
+ $user->addToDatabase();
+ // Set user ID to match the test value
+ $this->db->update( 'user', array( 'user_id' => 14146 ), array( 'user_id' => $user->getId() ) );
+ $user = null; // clear
+
+ $block = Block::newFromID( $res['id'] );
+ $this->assertEquals( 'UserOnForeignWiki', $block->getTarget()->getName(), 'Correct blockee name' );
+ $this->assertEquals( '14146', $block->getTarget()->getId(), 'Correct blockee id' );
+ $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' );
+ $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' );
+ $this->assertEquals( 0, $block->getBy(), 'Correct blocker id' );
+ }
+
+ protected function addXffBlocks() {
+ static $inited = false;
+
+ if ( $inited ) {
+ return;
+ }
+
+ $inited = true;
+
+ $blockList = array(
+ array( 'target' => '70.2.0.0/16',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range Hardblock',
+ 'ACDisable' => false,
+ 'isHardblock' => true,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '2001:4860:4001::/48',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range6 Hardblock',
+ 'ACDisable' => false,
+ 'isHardblock' => true,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '60.2.0.0/16',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range Softblock with AC Disabled',
+ 'ACDisable' => true,
+ 'isHardblock' => false,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '50.2.0.0/16',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range Softblock',
+ 'ACDisable' => false,
+ 'isHardblock' => false,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '50.1.1.1',
+ 'type' => Block::TYPE_IP,
+ 'desc' => 'Exact Softblock',
+ 'ACDisable' => false,
+ 'isHardblock' => false,
+ 'isAutoBlocking' => false,
+ ),
+ );
+
+ foreach ( $blockList as $insBlock ) {
+ $target = $insBlock['target'];
+
+ if ( $insBlock['type'] === Block::TYPE_IP ) {
+ $target = User::newFromName( IP::sanitizeIP( $target ), false )->getName();
+ } elseif ( $insBlock['type'] === Block::TYPE_RANGE ) {
+ $target = IP::sanitizeRange( $target );
+ }
+
+ $block = new Block();
+ $block->setTarget( $target );
+ $block->setBlocker( 'testblocker@global' );
+ $block->mReason = $insBlock['desc'];
+ $block->mExpiry = 'infinity';
+ $block->prevents( 'createaccount', $insBlock['ACDisable'] );
+ $block->isHardblock( $insBlock['isHardblock'] );
+ $block->isAutoblocking( $insBlock['isAutoBlocking'] );
+ $block->insert();
+ }
+ }
+
+ public static function providerXff() {
+ return array(
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Hardblock'
+ ),
+ array( 'xff' => '1.2.3.4, 50.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Softblock with AC Disabled'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 50.1.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Exact Softblock'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 50.2.1.1, 50.1.1.1, 2.3.4.5',
+ 'count' => 3,
+ 'result' => 'Exact Softblock'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 50.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Hardblock'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Hardblock'
+ ),
+ array( 'xff' => '50.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Softblock with AC Disabled'
+ ),
+ array( 'xff' => '1.2.3.4, 50.1.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Exact Softblock'
+ ),
+ array( 'xff' => '1.2.3.4, <$A_BUNCH-OF{INVALID}TEXT\>, 60.2.1.1, 2.3.4.5',
+ 'count' => 1,
+ 'result' => 'Range Softblock with AC Disabled'
+ ),
+ array( 'xff' => '1.2.3.4, 50.2.1.1, 2001:4860:4001:802::1003, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range6 Hardblock'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providerXff
+ */
+ public function testBlocksOnXff( $xff, $exCount, $exResult ) {
+ $list = array_map( 'trim', explode( ',', $xff ) );
+ $xffblocks = Block::getBlocksForIPList( $list, true );
+ $this->assertEquals( $exCount, count( $xffblocks ), 'Number of blocks for ' . $xff );
+ $block = Block::chooseBlock( $xffblocks, $list );
+ $this->assertEquals( $exResult, $block->mReason, 'Correct block type for XFF header ' . $xff );
+ }
+}
diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php
new file mode 100644
index 00000000..e3d9da7c
--- /dev/null
+++ b/tests/phpunit/includes/CdbTest.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * Test the CDB reader/writer
+ */
+class CdbTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !CdbReader::haveExtension() ) {
+ $this->markTestSkipped( 'Native CDB support is not available' );
+ }
+ }
+
+ /**
+ * @group medium
+ */
+ public function testCdb() {
+ $dir = wfTempDir();
+ if ( !is_writable( $dir ) ) {
+ $this->markTestSkipped( "Temp dir isn't writable" );
+ }
+
+ $phpcdbfile = $this->getNewTempFile();
+ $dbacdbfile = $this->getNewTempFile();
+
+ $w1 = new CdbWriter_PHP( $phpcdbfile );
+ $w2 = new CdbWriter_DBA( $dbacdbfile );
+
+ $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( $phpcdbfile ),
+ md5_file( $dbacdbfile ),
+ 'same hash'
+ );
+
+ $r1 = new CdbReader_PHP( $phpcdbfile );
+ $r2 = new CdbReader_DBA( $dbacdbfile );
+
+ 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 );
+ }
+ }
+
+ 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/CollationTest.php b/tests/phpunit/includes/CollationTest.php
new file mode 100644
index 00000000..43bb3941
--- /dev/null
+++ b/tests/phpunit/includes/CollationTest.php
@@ -0,0 +1,111 @@
+<?php
+class CollationTest extends MediaWikiLangTestCase {
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'intl' ) ) {
+ $this->markTestSkipped( 'These tests require intl extension' );
+ }
+ }
+
+ /**
+ * Test to make sure, that if you
+ * have "X" and "XY", the binary
+ * sortkey also has "X" being a
+ * prefix of "XY". Our collation
+ * code makes this assumption.
+ *
+ * @param $lang String Language code for collator
+ * @param $base String Base string
+ * @param $extended String String containing base as a prefix.
+ *
+ * @dataProvider prefixDataProvider
+ */
+ public function testIsPrefix( $lang, $base, $extended ) {
+ $cp = Collator::create( $lang );
+ $cp->setStrength( Collator::PRIMARY );
+ $baseBin = $cp->getSortKey( $base );
+ // Remove sortkey terminator
+ $baseBin = rtrim( $baseBin, "\0" );
+ $extendedBin = $cp->getSortKey( $extended );
+ $this->assertStringStartsWith( $baseBin, $extendedBin, "$base is not a prefix of $extended" );
+ }
+
+ function prefixDataProvider() {
+ return array(
+ array( 'en', 'A', 'AA' ),
+ array( 'en', 'A', 'AAA' ),
+ array( 'en', 'Д', 'ДЂ' ),
+ array( 'en', 'Д', 'ДA' ),
+ // 'Ʒ' should expand to 'Z ' (note space).
+ array( 'fi', 'Z', 'Ʒ' ),
+ // 'Þ' should expand to 'th'
+ array( 'sv', 't', 'Þ' ),
+ // Javanese is a limited use alphabet, so should have 3 bytes
+ // per character, so do some tests with it.
+ array( 'en', 'ꦲ', 'ꦲꦤ' ),
+ array( 'en', 'ꦲ', 'ꦲД' ),
+ array( 'en', 'A', 'Aꦲ' ),
+ );
+ }
+
+ /**
+ * Opposite of testIsPrefix
+ *
+ * @dataProvider notPrefixDataProvider
+ */
+ public function testNotIsPrefix( $lang, $base, $extended ) {
+ $cp = Collator::create( $lang );
+ $cp->setStrength( Collator::PRIMARY );
+ $baseBin = $cp->getSortKey( $base );
+ // Remove sortkey terminator
+ $baseBin = rtrim( $baseBin, "\0" );
+ $extendedBin = $cp->getSortKey( $extended );
+ $this->assertStringStartsNotWith( $baseBin, $extendedBin, "$base is a prefix of $extended" );
+ }
+
+ function notPrefixDataProvider() {
+ return array(
+ array( 'en', 'A', 'B' ),
+ array( 'en', 'AC', 'ABC' ),
+ array( 'en', 'Z', 'Ʒ' ),
+ array( 'en', 'A', 'ꦲ' ),
+ );
+ }
+
+ /**
+ * Test correct first letter is fetched.
+ *
+ * @param $collation String Collation name (aka uca-en)
+ * @param $string String String to get first letter of
+ * @param $firstLetter String Expected first letter.
+ *
+ * @dataProvider firstLetterProvider
+ */
+ public function testGetFirstLetter( $collation, $string, $firstLetter ) {
+ $col = Collation::factory( $collation );
+ $this->assertEquals( $firstLetter, $col->getFirstLetter( $string ) );
+ }
+
+ function firstLetterProvider() {
+ return array(
+ array( 'uppercase', 'Abc', 'A' ),
+ array( 'uppercase', 'abc', 'A' ),
+ array( 'identity', 'abc', 'a' ),
+ array( 'uca-en', 'abc', 'A' ),
+ array( 'uca-en', ' ', ' ' ),
+ array( 'uca-en', 'Êveryone', 'E' ),
+ array( 'uca-vi', 'Êveryone', 'Ê' ),
+ // Make sure thorn is not a first letter.
+ array( 'uca-sv', 'The', 'T' ),
+ array( 'uca-sv', 'Å', 'Å' ),
+ array( 'uca-hu', 'dzsdo', 'Dzs' ),
+ array( 'uca-hu', 'dzdso', 'Dz' ),
+ array( 'uca-hu', 'CSD', 'Cs' ),
+ array( 'uca-root', 'CSD', 'C' ),
+ array( 'uca-fi', 'Ǥ', 'G' ),
+ array( 'uca-fi', 'Ŧ', 'T' ),
+ array( 'uca-fi', 'Ʒ', 'Z' ),
+ array( 'uca-fi', 'Ŋ', 'N' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/DiffHistoryBlobTest.php b/tests/phpunit/includes/DiffHistoryBlobTest.php
new file mode 100644
index 00000000..a4d5b91a
--- /dev/null
+++ b/tests/phpunit/includes/DiffHistoryBlobTest.php
@@ -0,0 +1,44 @@
+<?php
+
+class DiffHistoryBlobTest extends MediaWikiTestCase {
+ protected function setUp() {
+ if ( !extension_loaded( 'xdiff' ) ) {
+ $this->markTestSkipped( 'The xdiff extension is not available' );
+
+ return;
+ }
+ if ( !function_exists( 'xdiff_string_rabdiff' ) ) {
+ $this->markTestSkipped( 'The version of xdiff extension is lower than 1.5.0' );
+
+ return;
+ }
+ if ( !extension_loaded( 'hash' ) ) {
+ $this->markTestSkipped( 'The hash extension is not available' );
+
+ return;
+ }
+ parent::setUp();
+ }
+
+ /**
+ * Test for DiffHistoryBlob::xdiffAdler32()
+ * @dataProvider provideXdiffAdler32
+ */
+ public function testXdiffAdler32( $input ) {
+ $xdiffHash = substr( xdiff_string_rabdiff( $input, '' ), 0, 4 );
+ $dhb = new DiffHistoryBlob;
+ $myHash = $dhb->xdiffAdler32( $input );
+ $this->assertSame( bin2hex( $xdiffHash ), bin2hex( $myHash ),
+ "Hash of " . addcslashes( $input, "\0..\37!@\@\177..\377" ) );
+ }
+
+ public static function provideXdiffAdler32() {
+ return array(
+ array( '', 'Empty string' ),
+ array( "\0", 'Null' ),
+ array( "\0\0\0", "Several nulls" ),
+ array( "Hello", "An ASCII string" ),
+ array( str_repeat( "x", 6000 ), "A string larger than xdiff's NMAX (5552)" )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/EditPageTest.php b/tests/phpunit/includes/EditPageTest.php
new file mode 100644
index 00000000..87272a4c
--- /dev/null
+++ b/tests/phpunit/includes/EditPageTest.php
@@ -0,0 +1,491 @@
+<?php
+
+/**
+ * @group Editing
+ *
+ * @group Database
+ * ^--- tell jenkins this test needs the database
+ *
+ * @group medium
+ * ^--- tell phpunit that these test cases may take longer than 2 seconds.
+ */
+class EditPageTest extends MediaWikiLangTestCase {
+
+ /**
+ * @dataProvider provideExtractSectionTitle
+ */
+ public function testExtractSectionTitle( $section, $title ) {
+ $extracted = EditPage::extractSectionTitle( $section );
+ $this->assertEquals( $title, $extracted );
+ }
+
+ public static function provideExtractSectionTitle() {
+ return array(
+ array(
+ "== Test ==\n\nJust a test section.",
+ "Test"
+ ),
+ array(
+ "An initial section, no header.",
+ false
+ ),
+ array(
+ "An initial section with a fake heder (bug 32617)\n\n== Test == ??\nwtf",
+ false
+ ),
+ array(
+ "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
+ "Section"
+ ),
+ array(
+ "== Section== \t\r\n followed by whitespace (bug 35051)",
+ 'Section',
+ ),
+ );
+ }
+
+ protected function forceRevisionDate( WikiPage $page, $timestamp ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'revision',
+ array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ),
+ array( 'rev_id' => $page->getLatest() ) );
+
+ $page->clear();
+ }
+
+ /**
+ * User input text is passed to rtrim() by edit page. This is a simple
+ * wrapper around assertEquals() which calls rrtrim() to normalize the
+ * expected and actual texts.
+ */
+ function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
+ return $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
+ }
+
+ /**
+ * Performs an edit and checks the result.
+ *
+ * @param String|Title $title The title of the page to edit
+ * @param String|null $baseText Some text to create the page with before attempting the edit.
+ * @param User|String|null $user The user to perform the edit as.
+ * @param array $edit An array of request parameters used to define the edit to perform.
+ * Some well known fields are:
+ * * wpTextbox1: the text to submit
+ * * wpSummary: the edit summary
+ * * wpEditToken: the edit token (will be inserted if not provided)
+ * * wpEdittime: timestamp of the edit's base revision (will be inserted if not provided)
+ * * wpStarttime: timestamp when the edit started (will be inserted if not provided)
+ * * wpSectionTitle: the section to edit
+ * * wpMinorEdit: mark as minor edit
+ * * wpWatchthis: whether to watch the page
+ * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
+ * Set to null to skip the check. Defaults to EditPage::AS_OK.
+ * @param String|null $expectedText The text expected to be on the page after the edit.
+ * Set to null to skip the check.
+ * @param String|null $message An optional message to show along with any error message.
+ *
+ * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
+ */
+ protected function assertEdit( $title, $baseText, $user = null, array $edit,
+ $expectedCode = EditPage::AS_OK, $expectedText = null, $message = null
+ ) {
+ if ( is_string( $title ) ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+ }
+
+ if ( is_string( $user ) ) {
+ $user = User::newFromName( $user );
+
+ if ( $user->getId() === 0 ) {
+ $user->addToDatabase();
+ }
+ }
+
+ $page = WikiPage::factory( $title );
+
+ if ( $baseText !== null ) {
+ $content = ContentHandler::makeContent( $baseText, $title );
+ $page->doEditContent( $content, "base text for test" );
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ //sanity check
+ $page->clear();
+ $currentText = ContentHandler::getContentText( $page->getContent() );
+
+ # EditPage rtrim() the user input, so we alter our expected text
+ # to reflect that.
+ $this->assertEditedTextEquals( $baseText, $currentText );
+ }
+
+ if ( $user == null ) {
+ $user = $GLOBALS['wgUser'];
+ } else {
+ $this->setMwGlobals( 'wgUser', $user );
+ }
+
+ if ( !isset( $edit['wpEditToken'] ) ) {
+ $edit['wpEditToken'] = $user->getEditToken();
+ }
+
+ if ( !isset( $edit['wpEdittime'] ) ) {
+ $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
+ }
+
+ if ( !isset( $edit['wpStarttime'] ) ) {
+ $edit['wpStarttime'] = wfTimestampNow();
+ }
+
+ $req = new FauxRequest( $edit, true ); // session ??
+
+ $ep = new EditPage( new Article( $title ) );
+ $ep->setContextTitle( $title );
+ $ep->importFormData( $req );
+
+ $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false;
+
+ // this is where the edit happens!
+ // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
+ // and throws exceptions like PermissionsError
+ $status = $ep->internalAttemptSave( $result, $bot );
+
+ if ( $expectedCode !== null ) {
+ // check edit code
+ $this->assertEquals( $expectedCode, $status->value,
+ "Expected result code mismatch. $message" );
+ }
+
+ $page = WikiPage::factory( $title );
+
+ if ( $expectedText !== null ) {
+ // check resulting page text
+ $content = $page->getContent();
+ $text = ContentHandler::getContentText( $content );
+
+ # EditPage rtrim() the user input, so we alter our expected text
+ # to reflect that.
+ $this->assertEditedTextEquals( $expectedText, $text,
+ "Expected article text mismatch. $message" );
+ }
+
+ return $page;
+ }
+
+ public function testCreatePage() {
+ $this->assertEdit(
+ 'EditPageTest_testCreatePage',
+ null,
+ null,
+ array(
+ 'wpTextbox1' => "Hello World!",
+ ),
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ "Hello World!",
+ "expected article being created"
+ )->doDeleteArticleReal( 'EditPageTest_testCreatePage' );
+
+ $this->assertEdit(
+ 'EditPageTest_testCreatePage',
+ null,
+ null,
+ array(
+ 'wpTextbox1' => "",
+ ),
+ EditPage::AS_BLANK_ARTICLE,
+ null,
+ "expected article not being created if empty"
+ );
+
+
+ $this->assertEdit(
+ 'MediaWiki:January',
+ null,
+ 'UTSysop',
+ array(
+ 'wpTextbox1' => "Not January",
+ ),
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ "Not January",
+ "expected MediaWiki: page being created"
+ )->doDeleteArticleReal( 'EditPageTest_testCreatePage' );
+
+ $this->assertEdit(
+ 'MediaWiki:EditPageTest_testCreatePage',
+ null,
+ 'UTSysop',
+ array(
+ 'wpTextbox1' => "",
+ ),
+ EditPage::AS_BLANK_ARTICLE,
+ null,
+ "expected not-registered MediaWiki: page not being created if empty"
+ );
+
+ $this->assertEdit(
+ 'MediaWiki:January',
+ null,
+ 'UTSysop',
+ array(
+ 'wpTextbox1' => "",
+ ),
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ "",
+ "expected registered MediaWiki: page being created even if empty"
+ )->doDeleteArticleReal( 'EditPageTest_testCreatePage' );
+
+ $this->assertEdit(
+ 'MediaWiki:Ipb-default-expiry',
+ null,
+ 'UTSysop',
+ array(
+ 'wpTextbox1' => "",
+ ),
+ EditPage::AS_BLANK_ARTICLE,
+ "",
+ "expected registered MediaWiki: page whose default content is empty not being created if empty"
+ );
+
+ $this->assertEdit(
+ 'MediaWiki:January',
+ null,
+ 'UTSysop',
+ array(
+ 'wpTextbox1' => "January",
+ ),
+ EditPage::AS_BLANK_ARTICLE,
+ null,
+ "expected MediaWiki: page not being created if text equals default message"
+ );
+ }
+
+ public function testUpdatePage() {
+ $text = "one";
+ $edit = array(
+ 'wpTextbox1' => $text,
+ 'wpSummary' => 'first update',
+ );
+
+ $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
+ EditPage::AS_SUCCESS_UPDATE, $text,
+ "expected successfull update with given text" );
+
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ $text = "two";
+ $edit = array(
+ 'wpTextbox1' => $text,
+ 'wpSummary' => 'second update',
+ );
+
+ $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
+ EditPage::AS_SUCCESS_UPDATE, $text,
+ "expected successfull update with given text" );
+ }
+
+ public static function provideSectionEdit() {
+ $text = 'Intro
+
+== one ==
+first section.
+
+== two ==
+second section.
+';
+
+ $sectionOne = '== one ==
+hello
+';
+
+ $newSection = '== new section ==
+
+hello
+';
+
+ $textWithNewSectionOne = preg_replace(
+ '/== one ==.*== two ==/ms',
+ "$sectionOne\n== two ==", $text
+ );
+
+ $textWithNewSectionAdded = "$text\n$newSection";
+
+ return array(
+ array( #0
+ $text,
+ '',
+ 'hello',
+ 'replace all',
+ 'hello'
+ ),
+
+ array( #1
+ $text,
+ '1',
+ $sectionOne,
+ 'replace first section',
+ $textWithNewSectionOne,
+ ),
+
+ array( #2
+ $text,
+ 'new',
+ 'hello',
+ 'new section',
+ $textWithNewSectionAdded,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideSectionEdit
+ */
+ public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
+ $edit = array(
+ 'wpTextbox1' => $text,
+ 'wpSummary' => $summary,
+ 'wpSection' => $section,
+ );
+
+ $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
+ EditPage::AS_SUCCESS_UPDATE, $expected,
+ "expected successfull update of section" );
+ }
+
+ public static function provideAutoMerge() {
+ $tests = array();
+
+ $tests[] = array( #0: plain conflict
+ "Elmo", # base edit user
+ "one\n\ntwo\n\nthree\n",
+ array( #adam's edit
+ 'wpStarttime' => 1,
+ 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
+ ),
+ array( #berta's edit
+ 'wpStarttime' => 2,
+ 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
+ ),
+ EditPage::AS_CONFLICT_DETECTED, # expected code
+ "ONE\n\ntwo\n\nthree\n", # expected text
+ 'expected edit conflict', # message
+ );
+
+ $tests[] = array( #1: successful merge
+ "Elmo", # base edit user
+ "one\n\ntwo\n\nthree\n",
+ array( #adam's edit
+ 'wpStarttime' => 1,
+ 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
+ ),
+ array( #berta's edit
+ 'wpStarttime' => 2,
+ 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
+ ),
+ EditPage::AS_SUCCESS_UPDATE, # expected code
+ "ONE\n\ntwo\n\nTHREE\n", # expected text
+ 'expected automatic merge', # message
+ );
+
+ $text = "Intro\n\n";
+ $text .= "== first section ==\n\n";
+ $text .= "one\n\ntwo\n\nthree\n\n";
+ $text .= "== second section ==\n\n";
+ $text .= "four\n\nfive\n\nsix\n\n";
+
+ // extract the first section.
+ $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
+
+ // generate expected text after merge
+ $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
+
+ $tests[] = array( #2: merge in section
+ "Elmo", # base edit user
+ $text,
+ array( #adam's edit
+ 'wpStarttime' => 1,
+ 'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
+ 'wpSection' => '1'
+ ),
+ array( #berta's edit
+ 'wpStarttime' => 2,
+ 'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
+ 'wpSection' => '1'
+ ),
+ EditPage::AS_SUCCESS_UPDATE, # expected code
+ $expected, # expected text
+ 'expected automatic section merge', # message
+ );
+
+ // see whether it makes a difference who did the base edit
+ $testsWithAdam = array_map( function ( $test ) {
+ $test[0] = 'Adam'; // change base edit user
+ return $test;
+ }, $tests );
+
+ $testsWithBerta = array_map( function ( $test ) {
+ $test[0] = 'Berta'; // change base edit user
+ return $test;
+ }, $tests );
+
+ return array_merge( $tests, $testsWithAdam, $testsWithBerta );
+ }
+
+ /**
+ * @dataProvider provideAutoMerge
+ */
+ public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
+ $expectedCode, $expectedText, $message = null
+ ) {
+ $this->checkHasDiff3();
+
+ //create page
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns );
+ $page = WikiPage::factory( $title );
+
+ if ( $page->exists() ) {
+ $page->doDeleteArticle( "clean slate for testing" );
+ }
+
+ $baseEdit = array(
+ 'wpTextbox1' => $text,
+ );
+
+ $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
+ $baseUser, $baseEdit, null, null, __METHOD__ );
+
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ $edittime = $page->getTimestamp();
+
+ // start timestamps for conflict detection
+ if ( !isset( $adamsEdit['wpStarttime'] ) ) {
+ $adamsEdit['wpStarttime'] = 1;
+ }
+
+ if ( !isset( $bertasEdit['wpStarttime'] ) ) {
+ $bertasEdit['wpStarttime'] = 2;
+ }
+
+ $starttime = wfTimestampNow();
+ $adamsTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime'] );
+ $bertasTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime'] );
+
+ $adamsEdit['wpStarttime'] = $adamsTime;
+ $bertasEdit['wpStarttime'] = $bertasTime;
+
+ $adamsEdit['wpSummary'] = 'Adam\'s edit';
+ $bertasEdit['wpSummary'] = 'Bertas\'s edit';
+
+ $adamsEdit['wpEdittime'] = $edittime;
+ $bertasEdit['wpEdittime'] = $edittime;
+
+ // first edit
+ $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
+ EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" );
+
+ // second edit
+ $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
+ $expectedCode, $expectedText, $message );
+ }
+}
diff --git a/tests/phpunit/includes/ExternalStoreTest.php b/tests/phpunit/includes/ExternalStoreTest.php
new file mode 100644
index 00000000..fcffcbc2
--- /dev/null
+++ b/tests/phpunit/includes/ExternalStoreTest.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * External Store tests
+ */
+
+class ExternalStoreTest extends MediaWikiTestCase {
+
+ public function testExternalFetchFromURL() {
+ $this->setMwGlobals( 'wgExternalStores', false );
+
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
+ 'Deny if wgExternalStores is not set to a non-empty array'
+ );
+
+ $this->setMwGlobals( 'wgExternalStores', array( 'FOO' ) );
+
+ $this->assertEquals(
+ ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
+ 'Hello',
+ 'Allow FOO://cluster1/200'
+ );
+ $this->assertEquals(
+ ExternalStore::fetchFromURL( 'FOO://cluster1/300/0' ),
+ 'Hello',
+ 'Allow FOO://cluster1/300/0'
+ );
+ # Assertions for r68900
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'ftp.example.org' ),
+ 'Deny domain ftp.example.org'
+ );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( '/example.txt' ),
+ 'Deny path /example.txt'
+ );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'http://' ),
+ 'Deny protocol http://'
+ );
+ }
+}
+
+class ExternalStoreFOO {
+
+ protected $data = array(
+ 'cluster1' => array(
+ '200' => 'Hello',
+ '300' => array(
+ 'Hello', 'World',
+ ),
+ ),
+ );
+
+ /**
+ * Fetch data from given URL
+ * @param $url String: an url of the form FOO://cluster/id or FOO://cluster/id/itemid.
+ * @return mixed
+ */
+ function fetchFromURL( $url ) {
+ // Based on ExternalStoreDB
+ $path = explode( '/', $url );
+ $cluster = $path[2];
+ $id = $path[3];
+ if ( isset( $path[4] ) ) {
+ $itemID = $path[4];
+ } else {
+ $itemID = false;
+ }
+
+ if ( !isset( $this->data[$cluster][$id] ) ) {
+ return null;
+ }
+
+ if ( $itemID !== false && is_array( $this->data[$cluster][$id] ) && isset( $this->data[$cluster][$id][$itemID] ) ) {
+ return $this->data[$cluster][$id][$itemID];
+ }
+
+ return $this->data[$cluster][$id];
+ }
+}
diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php
new file mode 100644
index 00000000..6c67beb1
--- /dev/null
+++ b/tests/phpunit/includes/ExtraParserTest.php
@@ -0,0 +1,157 @@
+<?php
+
+/**
+ * Parser-related tests that don't suit for parserTests.txt
+ */
+class ExtraParserTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $contLang = Language::factory( 'en' );
+ $this->setMwGlobals( array(
+ 'wgShowDBErrorBacktrace' => true,
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => $contLang,
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgMemc' => new EmptyBagOStuff,
+ 'wgAlwaysUseTidy' => false,
+ 'wgCleanSignatures' => true,
+ ) );
+
+ $this->options = ParserOptions::newFromUserAndLang( new User, $contLang );
+ $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) );
+ $this->parser = new Parser;
+
+ MagicWord::clearCache();
+ }
+
+ // Bug 8689 - Long numeric lines kill the parser
+ public function testBug8689() {
+ global $wgUser;
+ $longLine = '1.' . str_repeat( '1234567890', 100000 ) . "\n";
+
+ $t = Title::newFromText( 'Unit test' );
+ $options = ParserOptions::newFromUser( $wgUser );
+ $this->assertEquals( "<p>$longLine</p>",
+ $this->parser->parse( $longLine, $t, $options )->getText() );
+ }
+
+ /* Test the parser entry points */
+ public function testParse() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $parserOutput = $this->parser->parse( "Test\n{{Foo}}\n{{Bar}}", $title, $this->options );
+ $this->assertEquals( "<p>Test\nContent of <i>Template:Foo</i>\nContent of <i>Template:Bar</i>\n</p>", $parserOutput->getText() );
+ }
+
+ public function testPreSaveTransform() {
+ global $wgUser;
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->preSaveTransform( "Test\r\n{{subst:Foo}}\n{{Bar}}", $title, $wgUser, $this->options );
+
+ $this->assertEquals( "Test\nContent of ''Template:Foo''\n{{Bar}}", $outputText );
+ }
+
+ public function testPreprocess() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->preprocess( "Test\n{{Foo}}\n{{Bar}}", $title, $this->options );
+
+ $this->assertEquals( "Test\nContent of ''Template:Foo''\nContent of ''Template:Bar''", $outputText );
+ }
+
+ /**
+ * cleanSig() makes all templates substs and removes tildes
+ */
+ public function testCleanSig() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" );
+
+ $this->assertEquals( "{{SUBST:Foo}} ", $outputText );
+ }
+
+ /**
+ * cleanSig() should do nothing if disabled
+ */
+ public function testCleanSigDisabled() {
+ $this->setMwGlobals( 'wgCleanSignatures', false );
+
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" );
+
+ $this->assertEquals( "{{Foo}} ~~~~", $outputText );
+ }
+
+ /**
+ * cleanSigInSig() just removes tildes
+ * @dataProvider provideStringsForCleanSigInSig
+ */
+ public function testCleanSigInSig( $in, $out ) {
+ $this->assertEquals( Parser::cleanSigInSig( $in ), $out );
+ }
+
+ public static function provideStringsForCleanSigInSig() {
+ return array(
+ array( "{{Foo}} ~~~~", "{{Foo}} " ),
+ array( "~~~", "" ),
+ array( "~~~~~", "" ),
+ );
+ }
+
+ public function testGetSection() {
+ $outputText2 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 2 );
+ $outputText1 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1 );
+
+ $this->assertEquals( "=== Heading 2 ===\nSection 2", $outputText2 );
+ $this->assertEquals( "== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2", $outputText1 );
+ }
+
+ public function testReplaceSection() {
+ $outputText = $this->parser->replaceSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1, "New section 1" );
+
+ $this->assertEquals( "Section 0\nNew section 1\n\n== Heading 3 ==\nSection 3", $outputText );
+ }
+
+ /**
+ * Templates and comments are not affected, but noinclude/onlyinclude is.
+ */
+ public function testGetPreloadText() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->getPreloadText( "{{Foo}}<noinclude> censored</noinclude> information <!-- is very secret -->", $title, $this->options );
+
+ $this->assertEquals( "{{Foo}} information <!-- is very secret -->", $outputText );
+ }
+
+ static function statelessFetchTemplate( $title, $parser = false ) {
+ $text = "Content of ''" . $title->getFullText() . "''";
+ $deps = array();
+
+ return array(
+ 'text' => $text,
+ 'finalTitle' => $title,
+ 'deps' => $deps );
+ }
+
+ /**
+ * @group Database
+ */
+ public function testTrackingCategory() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $catName = wfMessage( 'broken-file-category' )->inContentLanguage()->text();
+ $cat = Title::makeTitleSafe( NS_CATEGORY, $catName );
+ $expected = array( $cat->getDBkey() );
+ $parserOutput = $this->parser->parse( "[[file:nonexistent]]", $title, $this->options );
+ $result = $parserOutput->getCategoryLinks();
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * @group Database
+ */
+ public function testTrackingCategorySpecial() {
+ // Special pages shouldn't have tracking cats.
+ $title = SpecialPage::getTitleFor( 'Contributions' );
+ $parserOutput = $this->parser->parse( "[[file:nonexistent]]", $title, $this->options );
+ $result = $parserOutput->getCategoryLinks();
+ $this->assertEmpty( $result );
+ }
+}
diff --git a/tests/phpunit/includes/FallbackTest.php b/tests/phpunit/includes/FallbackTest.php
new file mode 100644
index 00000000..f408f471
--- /dev/null
+++ b/tests/phpunit/includes/FallbackTest.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @covers Fallback
+ */
+class FallbackTest extends MediaWikiTestCase {
+
+ public function testFallbackMbstringFunctions() {
+
+ if ( !extension_loaded( 'mbstring' ) ) {
+ $this->markTestSkipped( "The mb_string functions must be installed to test the fallback functions" );
+ }
+
+ $sampleUTF = "Östergötland_coat_of_arms.png";
+
+ //mb_substr
+ $substr_params = array(
+ array( 0, 0 ),
+ array( 5, -4 ),
+ array( 33 ),
+ array( 100, -5 ),
+ array( -8, 10 ),
+ array( 1, 1 ),
+ array( 2, -1 )
+ );
+
+ foreach ( $substr_params as $param_set ) {
+ $old_param_set = $param_set;
+ array_unshift( $param_set, $sampleUTF );
+
+ $this->assertEquals(
+ call_user_func_array( 'mb_substr', $param_set ),
+ call_user_func_array( 'Fallback::mb_substr', $param_set ),
+ 'Fallback mb_substr with params ' . implode( ', ', $old_param_set )
+ );
+ }
+
+ //mb_strlen
+ $this->assertEquals(
+ mb_strlen( $sampleUTF ),
+ Fallback::mb_strlen( $sampleUTF ),
+ 'Fallback mb_strlen'
+ );
+
+ //mb_str(r?)pos
+ $strpos_params = array(
+ //array( 'ter' ),
+ //array( 'Ö' ),
+ //array( 'Ö', 3 ),
+ //array( 'oat_', 100 ),
+ //array( 'c', -10 ),
+ //Broken for now
+ );
+
+ foreach ( $strpos_params as $param_set ) {
+ $old_param_set = $param_set;
+ array_unshift( $param_set, $sampleUTF );
+
+ $this->assertEquals(
+ call_user_func_array( 'mb_strpos', $param_set ),
+ call_user_func_array( 'Fallback::mb_strpos', $param_set ),
+ 'Fallback mb_strpos with params ' . implode( ', ', $old_param_set )
+ );
+
+ $this->assertEquals(
+ call_user_func_array( 'mb_strrpos', $param_set ),
+ call_user_func_array( 'Fallback::mb_strrpos', $param_set ),
+ 'Fallback mb_strrpos with params ' . implode( ', ', $old_param_set )
+ );
+ }
+ }
+
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/FauxRequestTest.php b/tests/phpunit/includes/FauxRequestTest.php
new file mode 100644
index 00000000..9f3aa11d
--- /dev/null
+++ b/tests/phpunit/includes/FauxRequestTest.php
@@ -0,0 +1,15 @@
+<?php
+
+class FauxRequestTest extends MediaWikiTestCase {
+
+ public function testGetSetHeader() {
+ $value = 'test/test';
+
+ $request = new FauxRequest();
+ $request->setHeader( 'Content-Type', $value );
+
+ $this->assertEquals( $request->getHeader( 'Content-Type' ), $value );
+ $this->assertEquals( $request->getHeader( 'CONTENT-TYPE' ), $value );
+ $this->assertEquals( $request->getHeader( 'content-type' ), $value );
+ }
+}
diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php
new file mode 100644
index 00000000..f9ba1b3b
--- /dev/null
+++ b/tests/phpunit/includes/FauxResponseTest.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Tests for the FauxResponse class
+ *
+ * Copyright @ 2011 Alexandre Emsenhuber
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class FauxResponseTest extends MediaWikiTestCase {
+ var $response;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->response = new FauxResponse;
+ }
+
+ public function testCookie() {
+ $this->assertEquals( null, $this->response->getcookie( 'key' ), 'Non-existing cookie' );
+ $this->response->setcookie( 'key', 'val' );
+ $this->assertEquals( 'val', $this->response->getcookie( 'key' ), 'Existing cookie' );
+ }
+
+ public function testHeader() {
+ $this->assertEquals( null, $this->response->getheader( 'Location' ), 'Non-existing header' );
+
+ $this->response->header( 'Location: http://localhost/' );
+ $this->assertEquals( 'http://localhost/', $this->response->getheader( 'Location' ), 'Set header' );
+
+ $this->response->header( 'Location: http://127.0.0.1/' );
+ $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header' );
+
+ $this->response->header( 'Location: http://127.0.0.2/', false );
+ $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header with override disabled' );
+
+ $this->response->header( 'Location: http://localhost/' );
+ $this->assertEquals( 'http://localhost/', $this->response->getheader( 'LOCATION' ), 'Get header case insensitive' );
+ }
+
+ public function testResponseCode() {
+ $this->response->header( 'HTTP/1.1 200' );
+ $this->assertEquals( 200, $this->response->getStatusCode(), 'Header with no message' );
+
+ $this->response->header( 'HTTP/1.x 201' );
+ $this->assertEquals( 201, $this->response->getStatusCode(), 'Header with no message and protocol 1.x' );
+
+ $this->response->header( 'HTTP/1.1 202 OK' );
+ $this->assertEquals( 202, $this->response->getStatusCode(), 'Normal header' );
+
+ $this->response->header( 'HTTP/1.x 203 OK' );
+ $this->assertEquals( 203, $this->response->getStatusCode(), 'Normal header with no message and protocol 1.x' );
+
+ $this->response->header( 'HTTP/1.x 204 OK', false, 205 );
+ $this->assertEquals( 205, $this->response->getStatusCode(), 'Third parameter overrides the HTTP/... header' );
+
+ $this->response->header( 'Location: http://localhost/', false, 206 );
+ $this->assertEquals( 206, $this->response->getStatusCode(), 'Third parameter with another header' );
+ }
+}
diff --git a/tests/phpunit/includes/FormOptionsInitializationTest.php b/tests/phpunit/includes/FormOptionsInitializationTest.php
new file mode 100644
index 00000000..fb2304dc
--- /dev/null
+++ b/tests/phpunit/includes/FormOptionsInitializationTest.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * This file host two test case classes for the MediaWiki FormOptions class:
+ * - FormOptionsInitializationTest : tests initialization of the class.
+ * - FormOptionsTest : tests methods an on instance
+ *
+ * The split let us take advantage of setting up a fixture for the methods
+ * tests.
+ */
+
+/**
+ * Dummy class to makes FormOptions::$options public.
+ * Used by FormOptionsInitializationTest which need to verify the $options
+ * array is correctly set through the FormOptions::add() function.
+ */
+class FormOptionsExposed extends FormOptions {
+ public function getOptions() {
+ return $this->options;
+ }
+}
+
+/**
+ * Test class for FormOptions initialization
+ * Ensure the FormOptions::add() does what we want it to do.
+ *
+ * Generated by PHPUnit on 2011-02-28 at 20:46:27.
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsInitializationTest extends MediaWikiTestCase {
+ /**
+ * @var FormOptions
+ */
+ protected $object;
+
+
+ /**
+ * A new fresh and empty FormOptions object to test initialization
+ * with.
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->object = new FormOptionsExposed();
+ }
+
+ public function testAddStringOption() {
+ $this->object->add( 'foo', 'string value' );
+ $this->assertEquals(
+ array(
+ 'foo' => array(
+ 'default' => 'string value',
+ 'consumed' => false,
+ 'type' => FormOptions::STRING,
+ 'value' => null,
+ )
+ ),
+ $this->object->getOptions()
+ );
+ }
+
+ public function testAddIntegers() {
+ $this->object->add( 'one', 1 );
+ $this->object->add( 'negone', -1 );
+ $this->assertEquals(
+ array(
+ 'negone' => array(
+ 'default' => -1,
+ 'value' => null,
+ 'consumed' => false,
+ 'type' => FormOptions::INT,
+ ),
+ 'one' => array(
+ 'default' => 1,
+ 'value' => null,
+ 'consumed' => false,
+ 'type' => FormOptions::INT,
+ )
+ ),
+ $this->object->getOptions()
+ );
+ }
+}
diff --git a/tests/phpunit/includes/FormOptionsTest.php b/tests/phpunit/includes/FormOptionsTest.php
new file mode 100644
index 00000000..0a13cfec
--- /dev/null
+++ b/tests/phpunit/includes/FormOptionsTest.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * This file host two test case classes for the MediaWiki FormOptions class:
+ * - FormOptionsInitializationTest : tests initialization of the class.
+ * - FormOptionsTest : tests methods an on instance
+ *
+ * The split let us take advantage of setting up a fixture for the methods
+ * tests.
+ */
+
+/**
+ * Test class for FormOptions methods.
+ * Generated by PHPUnit on 2011-02-28 at 20:46:27.
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsTest extends MediaWikiTestCase {
+ /**
+ * @var FormOptions
+ */
+ protected $object;
+
+ /**
+ * Instanciates a FormOptions object to play with.
+ * FormOptions::add() is tested by the class FormOptionsInitializationTest
+ * so we assume the function is well tested already an use it to create
+ * the fixture.
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->object = new FormOptions;
+ $this->object->add( 'string1', 'string one' );
+ $this->object->add( 'string2', 'string two' );
+ $this->object->add( 'integer', 0 );
+ $this->object->add( 'intnull', 0, FormOptions::INTNULL );
+ }
+
+ /** Helpers for testGuessType() */
+ /* @{ */
+ private function assertGuessBoolean( $data ) {
+ $this->guess( FormOptions::BOOL, $data );
+ }
+ private function assertGuessInt( $data ) {
+ $this->guess( FormOptions::INT, $data );
+ }
+ private function assertGuessString( $data ) {
+ $this->guess( FormOptions::STRING, $data );
+ }
+
+ /** Generic helper */
+ private function guess( $expected, $data ) {
+ $this->assertEquals(
+ $expected,
+ FormOptions::guessType( $data )
+ );
+ }
+ /* @} */
+
+ /**
+ * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString
+ */
+ public function testGuessTypeDetection() {
+ $this->assertGuessBoolean( true );
+ $this->assertGuessBoolean( false );
+
+ $this->assertGuessInt( 0 );
+ $this->assertGuessInt( -5 );
+ $this->assertGuessInt( 5 );
+ $this->assertGuessInt( 0x0F );
+
+ $this->assertGuessString( 'true' );
+ $this->assertGuessString( 'false' );
+ $this->assertGuessString( '5' );
+ $this->assertGuessString( '0' );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testGuessTypeOnArrayThrowException() {
+ $this->object->guessType( array( 'foo' ) );
+ }
+ /**
+ * @expectedException MWException
+ */
+ public function testGuessTypeOnNullThrowException() {
+ $this->object->guessType( null );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php
new file mode 100644
index 00000000..6154df1d
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php
@@ -0,0 +1,652 @@
+<?php
+
+class GlobalTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $readOnlyFile = tempnam( wfTempDir(), "mwtest_readonly" );
+ unlink( $readOnlyFile );
+
+ $this->setMwGlobals( array(
+ 'wgReadOnlyFile' => $readOnlyFile,
+ 'wgUrlProtocols' => array(
+ 'http://',
+ 'https://',
+ 'mailto:',
+ '//',
+ 'file://', # Non-default
+ ),
+ ) );
+ }
+
+ protected function tearDown() {
+ global $wgReadOnlyFile;
+
+ if ( file_exists( $wgReadOnlyFile ) ) {
+ unlink( $wgReadOnlyFile );
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider provideForWfArrayDiff2
+ * @covers ::wfArrayDiff2
+ */
+ public function testWfArrayDiff2( $a, $b, $expected ) {
+ $this->assertEquals(
+ wfArrayDiff2( $a, $b ), $expected
+ );
+ }
+
+ // @todo Provide more tests
+ public static function provideForWfArrayDiff2() {
+ // $a $b $expected
+ return array(
+ array(
+ array( 'a', 'b' ),
+ array( 'a', 'b' ),
+ array(),
+ ),
+ array(
+ array( array( 'a' ), array( 'a', 'b', 'c' ) ),
+ array( array( 'a' ), array( 'a', 'b' ) ),
+ array( 1 => array( 'a', 'b', 'c' ) ),
+ ),
+ );
+ }
+
+ /**
+ * @covers ::wfRandom
+ */
+ public function testRandom() {
+ # This could hypothetically fail, but it shouldn't ;)
+ $this->assertFalse(
+ wfRandom() == wfRandom() );
+ }
+
+ /**
+ * @covers ::wfUrlencode
+ */
+ public function testUrlencode() {
+ $this->assertEquals(
+ "%E7%89%B9%E5%88%A5:Contributions/Foobar",
+ wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) );
+ }
+
+ /**
+ * @covers ::wfExpandIRI
+ */
+ public function testExpandIRI() {
+ $this->assertEquals(
+ "https://te.wikibooks.org/wiki/ఉబుంటు_వాడుకరి_మార్గదర్శని",
+ wfExpandIRI( "https://te.wikibooks.org/wiki/%E0%B0%89%E0%B0%AC%E0%B1%81%E0%B0%82%E0%B0%9F%E0%B1%81_%E0%B0%B5%E0%B0%BE%E0%B0%A1%E0%B1%81%E0%B0%95%E0%B0%B0%E0%B0%BF_%E0%B0%AE%E0%B0%BE%E0%B0%B0%E0%B1%8D%E0%B0%97%E0%B0%A6%E0%B0%B0%E0%B1%8D%E0%B0%B6%E0%B0%A8%E0%B0%BF" ) );
+ }
+
+ /**
+ * @covers ::wfReadOnly
+ */
+ public function testReadOnlyEmpty() {
+ global $wgReadOnly;
+ $wgReadOnly = null;
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ /**
+ * @covers ::wfReadOnly
+ */
+ public function testReadOnlySet() {
+ global $wgReadOnly, $wgReadOnlyFile;
+
+ $f = fopen( $wgReadOnlyFile, "wt" );
+ fwrite( $f, 'Message' );
+ fclose( $f );
+ $wgReadOnly = null; # Check on $wgReadOnlyFile
+
+ $this->assertTrue( wfReadOnly() );
+ $this->assertTrue( wfReadOnly() ); # Check cached
+
+ unlink( $wgReadOnlyFile );
+ $wgReadOnly = null; # Clean cache
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ public static function provideArrayToCGI() {
+ return array(
+ array( array(), '' ), // empty
+ array( array( 'foo' => 'bar' ), 'foo=bar' ), // string test
+ array( array( 'foo' => '' ), 'foo=' ), // empty string test
+ array( array( 'foo' => 1 ), 'foo=1' ), // number test
+ array( array( 'foo' => true ), 'foo=1' ), // true test
+ array( array( 'foo' => false ), '' ), // false test
+ array( array( 'foo' => null ), '' ), // null test
+ array( array( 'foo' => 'A&B=5+6@!"\'' ), 'foo=A%26B%3D5%2B6%40%21%22%27' ), // urlencoding test
+ array( array( 'foo' => 'bar', 'baz' => 'is', 'asdf' => 'qwerty' ), 'foo=bar&baz=is&asdf=qwerty' ), // multi-item test
+ array( array( 'foo' => array( 'bar' => 'baz' ) ), 'foo%5Bbar%5D=baz' ),
+ array( array( 'foo' => array( 'bar' => 'baz', 'qwerty' => 'asdf' ) ), 'foo%5Bbar%5D=baz&foo%5Bqwerty%5D=asdf' ),
+ array( array( 'foo' => array( 'bar', 'baz' ) ), 'foo%5B0%5D=bar&foo%5B1%5D=baz' ),
+ array( array( 'foo' => array( 'bar' => array( 'bar' => 'baz' ) ) ), 'foo%5Bbar%5D%5Bbar%5D=baz' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideArrayToCGI
+ * @covers ::wfArrayToCgi
+ */
+ public function testArrayToCGI( $array, $result ) {
+ $this->assertEquals( $result, wfArrayToCgi( $array ) );
+ }
+
+
+ /**
+ * @covers ::testWfArrayDiff2
+ */
+ public function testArrayToCGI2() {
+ $this->assertEquals(
+ "baz=bar&foo=bar",
+ wfArrayToCgi(
+ array( 'baz' => 'bar' ),
+ array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) );
+ }
+
+ public static function provideCgiToArray() {
+ return array(
+ array( '', array() ), // empty
+ array( 'foo=bar', array( 'foo' => 'bar' ) ), // string
+ array( 'foo=', array( 'foo' => '' ) ), // empty string
+ array( 'foo', array( 'foo' => '' ) ), // missing =
+ array( 'foo=bar&qwerty=asdf', array( 'foo' => 'bar', 'qwerty' => 'asdf' ) ), // multiple value
+ array( 'foo=A%26B%3D5%2B6%40%21%22%27', array( 'foo' => 'A&B=5+6@!"\'' ) ), // urldecoding test
+ array( 'foo%5Bbar%5D=baz', array( 'foo' => array( 'bar' => 'baz' ) ) ),
+ array( 'foo%5Bbar%5D=baz&foo%5Bqwerty%5D=asdf', array( 'foo' => array( 'bar' => 'baz', 'qwerty' => 'asdf' ) ) ),
+ array( 'foo%5B0%5D=bar&foo%5B1%5D=baz', array( 'foo' => array( 0 => 'bar', 1 => 'baz' ) ) ),
+ array( 'foo%5Bbar%5D%5Bbar%5D=baz', array( 'foo' => array( 'bar' => array( 'bar' => 'baz' ) ) ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCgiToArray
+ * @covers ::wfCgiToArray
+ */
+ public function testCgiToArray( $cgi, $result ) {
+ $this->assertEquals( $result, wfCgiToArray( $cgi ) );
+ }
+
+ public static function provideCgiRoundTrip() {
+ return array(
+ array( '' ),
+ array( 'foo=bar' ),
+ array( 'foo=' ),
+ array( 'foo=bar&baz=biz' ),
+ array( 'foo=A%26B%3D5%2B6%40%21%22%27' ),
+ array( 'foo%5Bbar%5D=baz' ),
+ array( 'foo%5B0%5D=bar&foo%5B1%5D=baz' ),
+ array( 'foo%5Bbar%5D%5Bbar%5D=baz' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCgiRoundTrip
+ * @covers ::wfArrayToCgi
+ */
+ public function testCgiRoundTrip( $cgi ) {
+ $this->assertEquals( $cgi, wfArrayToCgi( wfCgiToArray( $cgi ) ) );
+ }
+
+ /**
+ * @covers ::mimeTypeMatch
+ */
+ public 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 ) ) );
+ }
+
+ /**
+ * @covers ::wfNegotiateType
+ */
+ public 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 ) ) );
+ }
+
+ /**
+ * @covers ::wfDebug
+ * @covers ::wfDebugMem
+ */
+ public function testDebugFunctionTest() {
+
+ global $wgDebugLogFile, $wgDebugTimestamps;
+
+ $old_log_file = $wgDebugLogFile;
+ $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' );
+ # @todo FIXME: $wgDebugTimestamps should be tested
+ $old_wgDebugTimestamps = $wgDebugTimestamps;
+ $wgDebugTimestamps = false;
+
+ wfDebug( "This is a normal string" );
+ $this->assertEquals( "This is a normal string", file_get_contents( $wgDebugLogFile ) );
+ unlink( $wgDebugLogFile );
+
+ wfDebug( "This is nöt an ASCII string" );
+ $this->assertEquals( "This is nöt an ASCII string", file_get_contents( $wgDebugLogFile ) );
+ unlink( $wgDebugLogFile );
+
+ wfDebug( "\00305This has böth UTF and control chars\003" );
+ $this->assertEquals( " 05This has böth UTF and control chars ", file_get_contents( $wgDebugLogFile ) );
+ unlink( $wgDebugLogFile );
+
+ wfDebugMem();
+ $this->assertGreaterThan( 5000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) );
+ unlink( $wgDebugLogFile );
+
+ wfDebugMem( true );
+ $this->assertGreaterThan( 5000000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) );
+ unlink( $wgDebugLogFile );
+
+ $wgDebugLogFile = $old_log_file;
+ $wgDebugTimestamps = $old_wgDebugTimestamps;
+ }
+
+ /**
+ * @covers ::wfClientAcceptsGzip
+ */
+ public function testClientAcceptsGzipTest() {
+
+ $settings = array(
+ 'gzip' => true,
+ 'bzip' => false,
+ '*' => false,
+ 'compress, gzip' => true,
+ 'gzip;q=1.0' => true,
+ 'foozip' => false,
+ 'foo*zip' => false,
+ 'gzip;q=abcde' => true, //is this REALLY valid?
+ 'gzip;q=12345678.9' => true,
+ ' gzip' => true,
+ );
+
+ if ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
+ $old_server_setting = $_SERVER['HTTP_ACCEPT_ENCODING'];
+ }
+
+ foreach ( $settings as $encoding => $expect ) {
+ $_SERVER['HTTP_ACCEPT_ENCODING'] = $encoding;
+
+ $this->assertEquals( $expect, wfClientAcceptsGzip( true ),
+ "'$encoding' => " . wfBoolToStr( $expect ) );
+ }
+
+ if ( isset( $old_server_setting ) ) {
+ $_SERVER['HTTP_ACCEPT_ENCODING'] = $old_server_setting;
+ }
+ }
+
+ /**
+ * @covers ::swap
+ */
+ public function testSwapVarsTest() {
+ $var1 = 1;
+ $var2 = 2;
+
+ $this->assertEquals( $var1, 1, 'var1 is set originally' );
+ $this->assertEquals( $var2, 2, 'var1 is set originally' );
+
+ swap( $var1, $var2 );
+
+ $this->assertEquals( $var1, 2, 'var1 is swapped' );
+ $this->assertEquals( $var2, 1, 'var2 is swapped' );
+ }
+
+ /**
+ * @covers ::wfPercent
+ */
+ public function testWfPercentTest() {
+
+ $pcts = array(
+ array( 6 / 7, '0.86%', 2, false ),
+ array( 3 / 3, '1%' ),
+ array( 22 / 7, '3.14286%', 5 ),
+ array( 3 / 6, '0.5%' ),
+ array( 1 / 3, '0%', 0 ),
+ array( 10 / 3, '0%', -1 ),
+ array( 3 / 4 / 5, '0.1%', 1 ),
+ array( 6 / 7 * 8, '6.8571428571%', 10 ),
+ );
+
+ foreach ( $pcts as $pct ) {
+ if ( !isset( $pct[2] ) ) {
+ $pct[2] = 2;
+ }
+ if ( !isset( $pct[3] ) ) {
+ $pct[3] = true;
+ }
+
+ $this->assertEquals( wfPercent( $pct[0], $pct[2], $pct[3] ), $pct[1], $pct[1] );
+ }
+ }
+
+ /**
+ * test @see wfShorthandToInteger()
+ * @dataProvider provideShorthand
+ * @covers ::wfShorthandToInteger
+ */
+ public function testWfShorthandToInteger( $shorthand, $expected ) {
+ $this->assertEquals( $expected,
+ wfShorthandToInteger( $shorthand )
+ );
+ }
+
+ /** array( shorthand, expected integer ) */
+ public static function provideShorthand() {
+ return array(
+ # Null, empty ...
+ array( '', -1 ),
+ array( ' ', -1 ),
+ array( null, -1 ),
+
+ # Failures returns 0 :(
+ array( 'ABCDEFG', 0 ),
+ array( 'Ak', 0 ),
+
+ # Int, strings with spaces
+ array( 1, 1 ),
+ array( ' 1 ', 1 ),
+ array( 1023, 1023 ),
+ array( ' 1023 ', 1023 ),
+
+ # kilo, Mega, Giga
+ array( '1k', 1024 ),
+ array( '1K', 1024 ),
+ array( '1m', 1024 * 1024 ),
+ array( '1M', 1024 * 1024 ),
+ array( '1g', 1024 * 1024 * 1024 ),
+ array( '1G', 1024 * 1024 * 1024 ),
+
+ # Negatives
+ array( -1, -1 ),
+ array( -500, -500 ),
+ array( '-500', -500 ),
+ array( '-1k', -1024 ),
+
+ # Zeroes
+ array( '0', 0 ),
+ array( '0k', 0 ),
+ array( '0M', 0 ),
+ array( '0G', 0 ),
+ array( '-0', 0 ),
+ array( '-0k', 0 ),
+ array( '-0M', 0 ),
+ array( '-0G', 0 ),
+ );
+ }
+
+ /**
+ * @param String $old: Text as it was in the database
+ * @param String $mine: Text submitted while user was editing
+ * @param String $yours: Text submitted by the user
+ * @param Boolean $expectedMergeResult Whether the merge should be a success
+ * @param String $expectedText: Text after merge has been completed
+ *
+ * @dataProvider provideMerge()
+ * @group medium
+ * @covers ::wfMerge
+ */
+ public function testMerge( $old, $mine, $yours, $expectedMergeResult, $expectedText ) {
+ $this->checkHasDiff3();
+
+ $mergedText = null;
+ $isMerged = wfMerge( $old, $mine, $yours, $mergedText );
+
+ $msg = 'Merge should be a ';
+ $msg .= $expectedMergeResult ? 'success' : 'failure';
+ $this->assertEquals( $expectedMergeResult, $isMerged, $msg );
+
+ if ( $isMerged ) {
+ // Verify the merged text
+ $this->assertEquals( $expectedText, $mergedText,
+ 'is merged text as expected?' );
+ }
+ }
+
+ public static function provideMerge() {
+ $EXPECT_MERGE_SUCCESS = true;
+ $EXPECT_MERGE_FAILURE = false;
+
+ return array(
+ // #0: clean merge
+ array(
+ // old:
+ "one one one\n" . // trimmed
+ "\n" .
+ "two two two",
+
+ // mine:
+ "one one one ONE ONE\n" .
+ "\n" .
+ "two two two\n", // with tailing whitespace
+
+ // yours:
+ "one one one\n" .
+ "\n" .
+ "two two TWO TWO", // trimmed
+
+ // ok:
+ $EXPECT_MERGE_SUCCESS,
+
+ // result:
+ "one one one ONE ONE\n" .
+ "\n" .
+ "two two TWO TWO\n", // note: will always end in a newline
+ ),
+
+ // #1: conflict, fail
+ array(
+ // old:
+ "one one one", // trimmed
+
+ // mine:
+ "one one one ONE ONE\n" .
+ "\n" .
+ "bla bla\n" .
+ "\n", // with tailing whitespace
+
+ // yours:
+ "one one one\n" .
+ "\n" .
+ "two two", // trimmed
+
+ $EXPECT_MERGE_FAILURE,
+
+ // result:
+ null,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideMakeUrlIndexes()
+ * @covers ::wfMakeUrlIndexes
+ */
+ public function testMakeUrlIndexes( $url, $expected ) {
+ $index = wfMakeUrlIndexes( $url );
+ $this->assertEquals( $expected, $index, "wfMakeUrlIndexes(\"$url\")" );
+ }
+
+ public static function provideMakeUrlIndexes() {
+ return array(
+ array(
+ // just a regular :)
+ 'https://bugzilla.wikimedia.org/show_bug.cgi?id=28627',
+ array( 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627' )
+ ),
+ array(
+ // mailtos are handled special
+ // is this really right though? that final . probably belongs earlier?
+ 'mailto:wiki@wikimedia.org',
+ array( 'mailto:org.wikimedia@wiki.' )
+ ),
+
+ // file URL cases per bug 28627...
+ array(
+ // three slashes: local filesystem path Unix-style
+ 'file:///whatever/you/like.txt',
+ array( 'file://./whatever/you/like.txt' )
+ ),
+ array(
+ // three slashes: local filesystem path Windows-style
+ 'file:///c:/whatever/you/like.txt',
+ array( 'file://./c:/whatever/you/like.txt' )
+ ),
+ array(
+ // two slashes: UNC filesystem path Windows-style
+ 'file://intranet/whatever/you/like.txt',
+ array( 'file://intranet./whatever/you/like.txt' )
+ ),
+ // Multiple-slash cases that can sorta work on Mozilla
+ // if you hack it just right are kinda pathological,
+ // and unreliable cross-platform or on IE which means they're
+ // unlikely to appear on intranets.
+ //
+ // Those will survive the algorithm but with results that
+ // are less consistent.
+
+ // protocol-relative URL cases per bug 29854...
+ array(
+ '//bugzilla.wikimedia.org/show_bug.cgi?id=28627',
+ array(
+ 'http://org.wikimedia.bugzilla./show_bug.cgi?id=28627',
+ 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627'
+ )
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideWfMatchesDomainList
+ * @covers ::wfMatchesDomainList
+ */
+ public function testWfMatchesDomainList( $url, $domains, $expected, $description ) {
+ $actual = wfMatchesDomainList( $url, $domains );
+ $this->assertEquals( $expected, $actual, $description );
+ }
+
+ public static function provideWfMatchesDomainList() {
+ $a = array();
+ $protocols = array( 'HTTP' => 'http:', 'HTTPS' => 'https:', 'protocol-relative' => '' );
+ foreach ( $protocols as $pDesc => $p ) {
+ $a = array_merge( $a, array(
+ array( "$p//www.example.com", array(), false, "No matches for empty domains array, $pDesc URL" ),
+ array( "$p//www.example.com", array( 'www.example.com' ), true, "Exact match in domains array, $pDesc URL" ),
+ array( "$p//www.example.com", array( 'example.com' ), true, "Match without subdomain in domains array, $pDesc URL" ),
+ array( "$p//www.example2.com", array( 'www.example.com', 'www.example2.com', 'www.example3.com' ), true, "Exact match with other domains in array, $pDesc URL" ),
+ array( "$p//www.example2.com", array( 'example.com', 'example2.com', 'example3,com' ), true, "Match without subdomain with other domains in array, $pDesc URL" ),
+ array( "$p//www.example4.com", array( 'example.com', 'example2.com', 'example3,com' ), false, "Domain not in array, $pDesc URL" ),
+ array( "$p//nds-nl.wikipedia.org", array( 'nl.wikipedia.org' ), false, "Non-matching substring of domain, $pDesc URL" ),
+ ) );
+ }
+
+ return $a;
+ }
+
+ /**
+ * @covers ::wfMkdirParents
+ */
+ public function testWfMkdirParents() {
+ // Should not return true if file exists instead of directory
+ $fname = $this->getNewTempFile();
+ wfSuppressWarnings();
+ $ok = wfMkdirParents( $fname );
+ wfRestoreWarnings();
+ $this->assertFalse( $ok );
+ }
+
+ /**
+ * @dataProvider provideWfShellMaintenanceCmdList
+ * @covers ::wfShellMaintenanceCmd
+ */
+ public function testWfShellMaintenanceCmd( $script, $parameters, $options, $expected, $description ) {
+ if ( wfIsWindows() ) {
+ // Approximation that's good enough for our purposes just now
+ $expected = str_replace( "'", '"', $expected );
+ }
+ $actual = wfShellMaintenanceCmd( $script, $parameters, $options );
+ $this->assertEquals( $expected, $actual, $description );
+ }
+
+ public static function provideWfShellMaintenanceCmdList() {
+ global $wgPhpCli;
+
+ return array(
+ array( 'eval.php', array( '--help', '--test' ), array(),
+ "'$wgPhpCli' 'eval.php' '--help' '--test'",
+ "Called eval.php --help --test" ),
+ array( 'eval.php', array( '--help', '--test space' ), array( 'php' => 'php5' ),
+ "'php5' 'eval.php' '--help' '--test space'",
+ "Called eval.php --help --test with php option" ),
+ array( 'eval.php', array( '--help', '--test', 'X' ), array( 'wrapper' => 'MWScript.php' ),
+ "'$wgPhpCli' 'MWScript.php' 'eval.php' '--help' '--test' 'X'",
+ "Called eval.php --help --test with wrapper option" ),
+ array( 'eval.php', array( '--help', '--test', 'y' ), array( 'php' => 'php5', 'wrapper' => 'MWScript.php' ),
+ "'php5' 'MWScript.php' 'eval.php' '--help' '--test' 'y'",
+ "Called eval.php --help --test with wrapper and php option" ),
+ );
+ }
+ /* @TODO many more! */
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php
new file mode 100644
index 00000000..cf891e7b
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @group Database
+ */
+class GlobalWithDBTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideWfIsBadImageList
+ * @covers ::wfIsBadImage
+ */
+ public function testWfIsBadImage( $name, $title, $blacklist, $expected, $desc ) {
+ $this->assertEquals( $expected, wfIsBadImage( $name, $title, $blacklist ), $desc );
+ }
+
+ public static function provideWfIsBadImageList() {
+ $blacklist = '* [[File:Bad.jpg]] except [[Nasty page]]';
+
+ return array(
+ array( 'Bad.jpg', false, $blacklist, true,
+ 'Called on a bad image' ),
+ array( 'Bad.jpg', Title::makeTitle( NS_MAIN, 'A page' ), $blacklist, true,
+ 'Called on a bad image' ),
+ array( 'NotBad.jpg', false, $blacklist, false,
+ 'Called on a non-bad image' ),
+ array( 'Bad.jpg', Title::makeTitle( NS_MAIN, 'Nasty page' ), $blacklist, false,
+ 'Called on a bad image but is on a whitelisted page' ),
+ array( 'File:Bad.jpg', false, $blacklist, false,
+ 'Called on a bad image with File:' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/README b/tests/phpunit/includes/GlobalFunctions/README
new file mode 100644
index 00000000..0042bdac
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/README
@@ -0,0 +1,2 @@
+This directory hold tests for includes/GlobalFunctions.php file
+which is a pile of functions.
diff --git a/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php
new file mode 100644
index 00000000..9bb74873
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * @covers ::wfAssembleUrl
+ */
+class WfAssembleUrlTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideURLParts
+ */
+ public function testWfAssembleUrl( $parts, $output ) {
+ $partsDump = print_r( $parts, true );
+ $this->assertEquals(
+ $output,
+ wfAssembleUrl( $parts ),
+ "Testing $partsDump assembles to $output"
+ );
+ }
+
+ /**
+ * Provider of URL parts for testing wfAssembleUrl()
+ *
+ * @return array
+ */
+ public static function provideURLParts() {
+ $schemes = array(
+ '' => array(),
+ '//' => array(
+ 'delimiter' => '//',
+ ),
+ 'http://' => array(
+ 'scheme' => 'http',
+ 'delimiter' => '://',
+ ),
+ );
+
+ $hosts = array(
+ '' => array(),
+ 'example.com' => array(
+ 'host' => 'example.com',
+ ),
+ 'example.com:123' => array(
+ 'host' => 'example.com',
+ 'port' => 123,
+ ),
+ 'id@example.com' => array(
+ 'user' => 'id',
+ 'host' => 'example.com',
+ ),
+ 'id@example.com:123' => array(
+ 'user' => 'id',
+ 'host' => 'example.com',
+ 'port' => 123,
+ ),
+ 'id:key@example.com' => array(
+ 'user' => 'id',
+ 'pass' => 'key',
+ 'host' => 'example.com',
+ ),
+ 'id:key@example.com:123' => array(
+ 'user' => 'id',
+ 'pass' => 'key',
+ 'host' => 'example.com',
+ 'port' => 123,
+ ),
+ );
+
+ $cases = array();
+ foreach ( $schemes as $scheme => $schemeParts ) {
+ foreach ( $hosts as $host => $hostParts ) {
+ foreach ( array( '', '/path' ) as $path ) {
+ foreach ( array( '', 'query' ) as $query ) {
+ foreach ( array( '', 'fragment' ) as $fragment ) {
+ $parts = array_merge(
+ $schemeParts,
+ $hostParts
+ );
+ $url = $scheme .
+ $host .
+ $path;
+
+ if ( $path ) {
+ $parts['path'] = $path;
+ }
+ if ( $query ) {
+ $parts['query'] = $query;
+ $url .= '?' . $query;
+ }
+ if ( $fragment ) {
+ $parts['fragment'] = $fragment;
+ $url .= '#' . $fragment;
+ }
+
+ $cases[] = array(
+ $parts,
+ $url,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ $complexURL = 'http://id:key@example.org:321' .
+ '/over/there?name=ferret&foo=bar#nose';
+ $cases[] = array(
+ wfParseUrl( $complexURL ),
+ $complexURL,
+ );
+
+ return $cases;
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php b/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php
new file mode 100644
index 00000000..a01c0d49
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * @covers ::wfBCP47
+ */
+class WfBCP47Test extends MediaWikiTestCase {
+ /**
+ * test @see wfBCP47().
+ * Please note the BCP explicitly state that language codes are case
+ * insensitive, there are some exceptions to the rule :)
+ * This test is used to verify our formatting against all lower and
+ * all upper cases language code.
+ *
+ * @see http://tools.ietf.org/html/bcp47
+ * @dataProvider provideLanguageCodes()
+ */
+ public function testBCP47( $code, $expected ) {
+ $code = strtolower( $code );
+ $this->assertEquals( $expected, wfBCP47( $code ),
+ "Applying BCP47 standard to lower case '$code'"
+ );
+
+ $code = strtoupper( $code );
+ $this->assertEquals( $expected, wfBCP47( $code ),
+ "Applying BCP47 standard to upper case '$code'"
+ );
+ }
+
+ /**
+ * Array format is ($code, $expected)
+ */
+ public static function provideLanguageCodes() {
+ return array(
+ // Extracted from BCP47 (list not exhaustive)
+ # 2.1.1
+ array( 'en-ca-x-ca', 'en-CA-x-ca' ),
+ array( 'sgn-be-fr', 'sgn-BE-FR' ),
+ array( 'az-latn-x-latn', 'az-Latn-x-latn' ),
+ # 2.2
+ array( 'sr-Latn-RS', 'sr-Latn-RS' ),
+ array( 'az-arab-ir', 'az-Arab-IR' ),
+
+ # 2.2.5
+ array( 'sl-nedis', 'sl-nedis' ),
+ array( 'de-ch-1996', 'de-CH-1996' ),
+
+ # 2.2.6
+ array(
+ 'en-latn-gb-boont-r-extended-sequence-x-private',
+ 'en-Latn-GB-boont-r-extended-sequence-x-private'
+ ),
+
+ // Examples from BCP47 Appendix A
+ # Simple language subtag:
+ array( 'DE', 'de' ),
+ array( 'fR', 'fr' ),
+ array( 'ja', 'ja' ),
+
+ # Language subtag plus script subtag:
+ array( 'zh-hans', 'zh-Hans' ),
+ array( 'sr-cyrl', 'sr-Cyrl' ),
+ array( 'sr-latn', 'sr-Latn' ),
+
+ # Extended language subtags and their primary language subtag
+ # counterparts:
+ array( 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ),
+ array( 'cmn-hans-cn', 'cmn-Hans-CN' ),
+ array( 'zh-yue-hk', 'zh-yue-HK' ),
+ array( 'yue-hk', 'yue-HK' ),
+
+ # Language-Script-Region:
+ array( 'zh-hans-cn', 'zh-Hans-CN' ),
+ array( 'sr-latn-RS', 'sr-Latn-RS' ),
+
+ # Language-Variant:
+ array( 'sl-rozaj', 'sl-rozaj' ),
+ array( 'sl-rozaj-biske', 'sl-rozaj-biske' ),
+ array( 'sl-nedis', 'sl-nedis' ),
+
+ # Language-Region-Variant:
+ array( 'de-ch-1901', 'de-CH-1901' ),
+ array( 'sl-it-nedis', 'sl-IT-nedis' ),
+
+ # Language-Script-Region-Variant:
+ array( 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ),
+
+ # Language-Region:
+ array( 'de-de', 'de-DE' ),
+ array( 'en-us', 'en-US' ),
+ array( 'es-419', 'es-419' ),
+
+ # Private use subtags:
+ array( 'de-ch-x-phonebk', 'de-CH-x-phonebk' ),
+ array( 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ),
+ /**
+ * Previous test does not reflect the BCP which states:
+ * az-Arab-x-AZE-derbend
+ * AZE being private, it should be lower case, hence the test above
+ * should probably be:
+ #array( 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ),
+ */
+
+ # Private use registry values:
+ array( 'x-whatever', 'x-whatever' ),
+ array( 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ),
+ array( 'de-qaaa', 'de-Qaaa' ),
+ array( 'sr-latn-qm', 'sr-Latn-QM' ),
+ array( 'sr-qaaa-rs', 'sr-Qaaa-RS' ),
+
+ # Tags that use extensions
+ array( 'en-us-u-islamcal', 'en-US-u-islamcal' ),
+ array( 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ),
+ array( 'en-a-myext-b-another', 'en-a-myext-b-another' ),
+
+ # Invalid:
+ // de-419-DE
+ // a-DE
+ // ar-a-aaa-b-bbb-a-ccc
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php
new file mode 100644
index 00000000..7da804e6
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ * @covers ::wfBaseConvert
+ */
+class WfBaseConvertTest extends MediaWikiTestCase {
+ public static function provideSingleDigitConversions() {
+ return array(
+ // 2 3 5 8 10 16 36
+ array( '0', '0', '0', '0', '0', '0', '0' ),
+ array( '1', '1', '1', '1', '1', '1', '1' ),
+ array( '10', '2', '2', '2', '2', '2', '2' ),
+ array( '11', '10', '3', '3', '3', '3', '3' ),
+ array( '100', '11', '4', '4', '4', '4', '4' ),
+ array( '101', '12', '10', '5', '5', '5', '5' ),
+ array( '110', '20', '11', '6', '6', '6', '6' ),
+ array( '111', '21', '12', '7', '7', '7', '7' ),
+ array( '1000', '22', '13', '10', '8', '8', '8' ),
+ array( '1001', '100', '14', '11', '9', '9', '9' ),
+ array( '1010', '101', '20', '12', '10', 'a', 'a' ),
+ array( '1011', '102', '21', '13', '11', 'b', 'b' ),
+ array( '1100', '110', '22', '14', '12', 'c', 'c' ),
+ array( '1101', '111', '23', '15', '13', 'd', 'd' ),
+ array( '1110', '112', '24', '16', '14', 'e', 'e' ),
+ array( '1111', '120', '30', '17', '15', 'f', 'f' ),
+ array( '10000', '121', '31', '20', '16', '10', 'g' ),
+ array( '10001', '122', '32', '21', '17', '11', 'h' ),
+ array( '10010', '200', '33', '22', '18', '12', 'i' ),
+ array( '10011', '201', '34', '23', '19', '13', 'j' ),
+ array( '10100', '202', '40', '24', '20', '14', 'k' ),
+ array( '10101', '210', '41', '25', '21', '15', 'l' ),
+ array( '10110', '211', '42', '26', '22', '16', 'm' ),
+ array( '10111', '212', '43', '27', '23', '17', 'n' ),
+ array( '11000', '220', '44', '30', '24', '18', 'o' ),
+ array( '11001', '221', '100', '31', '25', '19', 'p' ),
+ array( '11010', '222', '101', '32', '26', '1a', 'q' ),
+ array( '11011', '1000', '102', '33', '27', '1b', 'r' ),
+ array( '11100', '1001', '103', '34', '28', '1c', 's' ),
+ array( '11101', '1002', '104', '35', '29', '1d', 't' ),
+ array( '11110', '1010', '110', '36', '30', '1e', 'u' ),
+ array( '11111', '1011', '111', '37', '31', '1f', 'v' ),
+ array( '100000', '1012', '112', '40', '32', '20', 'w' ),
+ array( '100001', '1020', '113', '41', '33', '21', 'x' ),
+ array( '100010', '1021', '114', '42', '34', '22', 'y' ),
+ array( '100011', '1022', '120', '43', '35', '23', 'z' )
+ );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase2( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base2, wfBaseConvert( $base3, '3', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base5, '5', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base8, '8', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base10, '10', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base16, '16', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base36, '36', '2' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase3( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base3, wfBaseConvert( $base2, '2', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base5, '5', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base8, '8', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base10, '10', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base16, '16', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base36, '36', '3' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase5( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base5, wfBaseConvert( $base2, '2', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base3, '3', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base8, '8', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base10, '10', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base16, '16', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base36, '36', '5' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase8( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base8, wfBaseConvert( $base2, '2', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base3, '3', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base5, '5', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base10, '10', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base16, '16', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base36, '36', '8' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase10( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base10, wfBaseConvert( $base2, '2', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base3, '3', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base5, '5', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base8, '8', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base16, '16', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base36, '36', '10' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase16( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base16, wfBaseConvert( $base2, '2', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base3, '3', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base5, '5', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base8, '8', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base10, '10', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base36, '36', '16' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase36( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base36, wfBaseConvert( $base2, '2', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base3, '3', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base5, '5', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base8, '8', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base10, '10', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base16, '16', '36' ) );
+ }
+
+ public function testLargeNumber() {
+ $this->assertSame( '1100110001111010000000101110100', wfBaseConvert( 'sd89ys', 36, 2 ) );
+ $this->assertSame( '11102112120221201101', wfBaseConvert( 'sd89ys', 36, 3 ) );
+ $this->assertSame( '12003102232400', wfBaseConvert( 'sd89ys', 36, 5 ) );
+ $this->assertSame( '14617200564', wfBaseConvert( 'sd89ys', 36, 8 ) );
+ $this->assertSame( '1715274100', wfBaseConvert( 'sd89ys', 36, 10 ) );
+ $this->assertSame( '663d0174', wfBaseConvert( 'sd89ys', 36, 16 ) );
+ }
+
+ public static function provideNumbers() {
+ $x = array();
+ $chars = '0123456789abcdefghijklmnopqrstuvwxyz';
+ for ( $i = 0; $i < 50; $i++ ) {
+ $base = mt_rand( 2, 36 );
+ $len = mt_rand( 10, 100 );
+
+ $str = '';
+ for ( $j = 0; $j < $len; $j++ ) {
+ $str .= $chars[mt_rand( 0, $base - 1 )];
+ }
+
+ $x[] = array( $base, $str );
+ }
+
+ return $x;
+ }
+
+ /**
+ * @dataProvider provideNumbers
+ */
+ public function testIdentity( $base, $number ) {
+ $this->assertSame( $number, wfBaseConvert( $number, $base, $base, strlen( $number ) ) );
+ }
+
+ public function testInvalid() {
+ $this->assertFalse( wfBaseConvert( '101', 1, 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 1 ) );
+ $this->assertFalse( wfBaseConvert( '101', 37, 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 37 ) );
+ $this->assertFalse( wfBaseConvert( 'abcde', 10, 11 ) );
+ $this->assertFalse( wfBaseConvert( '12930', 2, 10 ) );
+ $this->assertFalse( wfBaseConvert( '101', 'abc', 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 'abc' ) );
+ }
+
+ public function testPadding() {
+ $number = "10101010101";
+ $this->assertSame( strlen( $number ) + 5, strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) + 5 ) ) );
+ $this->assertSame( strlen( $number ), strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) - 5 ) ) );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php
new file mode 100644
index 00000000..8c548040
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @covers ::wfBaseName
+ */
+class WfBaseNameTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider providePaths
+ */
+ public function testBaseName( $fullpath, $basename ) {
+ $this->assertEquals( $basename, wfBaseName( $fullpath ),
+ "wfBaseName('$fullpath') => '$basename'" );
+ }
+
+ public static function providePaths() {
+ return array(
+ array( '', '' ),
+ array( '/', '' ),
+ array( '\\', '' ),
+ array( '//', '' ),
+ array( '\\\\', '' ),
+ array( 'a', 'a' ),
+ array( 'aaaa', 'aaaa' ),
+ array( '/a', 'a' ),
+ array( '\\a', 'a' ),
+ array( '/aaaa', 'aaaa' ),
+ array( '\\aaaa', 'aaaa' ),
+ array( '/aaaa/', 'aaaa' ),
+ array( '\\aaaa\\', 'aaaa' ),
+ array( '\\aaaa\\', 'aaaa' ),
+ array( '/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' ),
+ array( 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ),
+ array( 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php
new file mode 100644
index 00000000..41230a1e
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * @covers ::wfExpandUrl
+ */
+class WfExpandUrlTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideExpandableUrls
+ */
+ public function testWfExpandUrl( $fullUrl, $shortUrl, $defaultProto, $server, $canServer, $httpsMode, $message ) {
+ // Fake $wgServer and $wgCanonicalServer
+ $this->setMwGlobals( array(
+ 'wgServer' => $server,
+ 'wgCanonicalServer' => $canServer,
+ ) );
+
+ // Fake $_SERVER['HTTPS'] if needed
+ if ( $httpsMode ) {
+ $_SERVER['HTTPS'] = 'on';
+ } else {
+ unset( $_SERVER['HTTPS'] );
+ }
+
+ $this->assertEquals( $fullUrl, wfExpandUrl( $shortUrl, $defaultProto ), $message );
+ }
+
+ /**
+ * Provider of URL examples for testing wfExpandUrl()
+ *
+ * @return array
+ */
+ public static function provideExpandableUrls() {
+ $modes = array( 'http', 'https' );
+ $servers = array(
+ 'http' => 'http://example.com',
+ 'https' => 'https://example.com',
+ 'protocol-relative' => '//example.com'
+ );
+ $defaultProtos = array(
+ 'http' => PROTO_HTTP,
+ 'https' => PROTO_HTTPS,
+ 'protocol-relative' => PROTO_RELATIVE,
+ 'current' => PROTO_CURRENT,
+ 'canonical' => PROTO_CANONICAL
+ );
+
+ $retval = array();
+ foreach ( $modes as $mode ) {
+ $httpsMode = $mode == 'https';
+ foreach ( $servers as $serverDesc => $server ) {
+ foreach ( $modes as $canServerMode ) {
+ $canServer = "$canServerMode://example2.com";
+ foreach ( $defaultProtos as $protoDesc => $defaultProto ) {
+ $retval[] = array(
+ 'http://example.com', 'http://example.com',
+ $defaultProto, $server, $canServer, $httpsMode,
+ "Testing fully qualified http URLs (no need to expand) ' .
+ '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )"
+ );
+ $retval[] = array(
+ 'https://example.com', 'https://example.com',
+ $defaultProto, $server, $canServer, $httpsMode,
+ "Testing fully qualified https URLs (no need to expand) ' .
+ '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )"
+ );
+ # Would be nice to support this, see fixme on wfExpandUrl()
+ $retval[] = array(
+ "wiki/FooBar", 'wiki/FooBar',
+ $defaultProto, $server, $canServer, $httpsMode,
+ "Test non-expandable relative URLs ' .
+ '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )"
+ );
+
+ // Determine expected protocol
+ if ( $protoDesc == 'protocol-relative' ) {
+ $p = '';
+ } elseif ( $protoDesc == 'current' ) {
+ $p = "$mode:";
+ } elseif ( $protoDesc == 'canonical' ) {
+ $p = "$canServerMode:";
+ } else {
+ $p = $protoDesc . ':';
+ }
+ // Determine expected server name
+ if ( $protoDesc == 'canonical' ) {
+ $srv = $canServer;
+ } elseif ( $serverDesc == 'protocol-relative' ) {
+ $srv = $p . $server;
+ } else {
+ $srv = $server;
+ }
+
+ $retval[] = array(
+ "$p//wikipedia.org", '//wikipedia.org',
+ $defaultProto, $server, $canServer, $httpsMode,
+ "Test protocol-relative URL ' .
+ '(defaultProto: $protoDesc, wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )"
+ );
+ $retval[] = array(
+ "$srv/wiki/FooBar", '/wiki/FooBar',
+ $defaultProto, $server, $canServer, $httpsMode,
+ "Testing expanding URL beginning with / ' .
+ '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )"
+ );
+ }
+ }
+ }
+ }
+
+ return $retval;
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php
new file mode 100644
index 00000000..62296245
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @covers ::wfGetCaller
+ */
+class WfGetCallerTest extends MediaWikiTestCase {
+
+ public function testZero() {
+ $this->assertEquals( __METHOD__, wfGetCaller( 1 ) );
+ }
+
+ function callerOne() {
+ return wfGetCaller();
+ }
+
+ public function testOne() {
+ $this->assertEquals( 'WfGetCallerTest::testOne', self::callerOne() );
+ }
+
+ function intermediateFunction( $level = 2, $n = 0 ) {
+ if ( $n > 0 ) {
+ return self::intermediateFunction( $level, $n - 1 );
+ }
+
+ return wfGetCaller( $level );
+ }
+
+ public function testTwo() {
+ $this->assertEquals( 'WfGetCallerTest::testTwo', self::intermediateFunction() );
+ }
+
+ public function testN() {
+ $this->assertEquals( 'WfGetCallerTest::testN', self::intermediateFunction( 2, 0 ) );
+ $this->assertEquals( 'WfGetCallerTest::intermediateFunction', self::intermediateFunction( 1, 0 ) );
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ $this->assertEquals( 'WfGetCallerTest::intermediateFunction', self::intermediateFunction( $i + 1, $i ) );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php
new file mode 100644
index 00000000..5032dc11
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Copyright © 2013 Alexandre Emsenhuber
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @covers ::wfParseUrl
+ */
+class WfParseUrlTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgUrlProtocols', array(
+ '//', 'http://', 'file://', 'mailto:',
+ ) );
+ }
+
+ /**
+ * @dataProvider provideURLs
+ */
+ public function testWfParseUrl( $url, $parts ) {
+ $partsDump = var_export( $parts, true );
+ $this->assertEquals(
+ $parts,
+ wfParseUrl( $url ),
+ "Testing $url parses to $partsDump"
+ );
+ }
+
+ /**
+ * Provider of URLs for testing wfParseUrl()
+ *
+ * @return array
+ */
+ public static function provideURLs() {
+ return array(
+ array(
+ '//example.org',
+ array(
+ 'scheme' => '',
+ 'delimiter' => '//',
+ 'host' => 'example.org',
+ )
+ ),
+ array(
+ 'http://example.org',
+ array(
+ 'scheme' => 'http',
+ 'delimiter' => '://',
+ 'host' => 'example.org',
+ )
+ ),
+ array(
+ 'http://id:key@example.org:123/path?foo=bar#baz',
+ array(
+ 'scheme' => 'http',
+ 'delimiter' => '://',
+ 'user' => 'id',
+ 'pass' => 'key',
+ 'host' => 'example.org',
+ 'port' => 123,
+ 'path' => '/path',
+ 'query' => 'foo=bar',
+ 'fragment' => 'baz',
+ )
+ ),
+ array(
+ 'file://example.org/etc/php.ini',
+ array(
+ 'scheme' => 'file',
+ 'delimiter' => '://',
+ 'host' => 'example.org',
+ 'path' => '/etc/php.ini',
+ )
+ ),
+ array(
+ 'file:///etc/php.ini',
+ array(
+ 'scheme' => 'file',
+ 'delimiter' => '://',
+ 'host' => '',
+ 'path' => '/etc/php.ini',
+ )
+ ),
+ array(
+ 'file:///c:/',
+ array(
+ 'scheme' => 'file',
+ 'delimiter' => '://',
+ 'host' => '',
+ 'path' => '/c:/',
+ )
+ ),
+ array(
+ 'mailto:id@example.org',
+ array(
+ 'scheme' => 'mailto',
+ 'delimiter' => ':',
+ 'host' => 'id@example.org',
+ 'path' => '',
+ )
+ ),
+ array(
+ 'mailto:id@example.org?subject=Foo',
+ array(
+ 'scheme' => 'mailto',
+ 'delimiter' => ':',
+ 'host' => 'id@example.org',
+ 'path' => '',
+ 'query' => 'subject=Foo',
+ )
+ ),
+ array(
+ 'mailto:?subject=Foo',
+ array(
+ 'scheme' => 'mailto',
+ 'delimiter' => ':',
+ 'host' => '',
+ 'path' => '',
+ 'query' => 'subject=Foo',
+ )
+ ),
+ array(
+ 'invalid://test/',
+ false
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
new file mode 100644
index 00000000..238a2c9c
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ *@covers ::wfRemoveDotSegments
+ */
+class WfRemoveDotSegmentsTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider providePaths
+ */
+ public function testWfRemoveDotSegments( $inputPath, $outputPath ) {
+ $this->assertEquals(
+ $outputPath,
+ wfRemoveDotSegments( $inputPath ),
+ "Testing $inputPath expands to $outputPath"
+ );
+ }
+
+ /**
+ * Provider of URL paths for testing wfRemoveDotSegments()
+ *
+ * @return array
+ */
+ public static function providePaths() {
+ return array(
+ array( '/a/b/c/./../../g', '/a/g' ),
+ array( 'mid/content=5/../6', 'mid/6' ),
+ array( '/a//../b', '/a/b' ),
+ array( '/.../a', '/.../a' ),
+ array( '.../a', '.../a' ),
+ array( '', '' ),
+ array( '/', '/' ),
+ array( '//', '//' ),
+ array( '.', '' ),
+ array( '..', '' ),
+ array( '...', '...' ),
+ array( '/.', '/' ),
+ array( '/..', '/' ),
+ array( './', '' ),
+ array( '../', '' ),
+ array( './a', 'a' ),
+ array( '../a', 'a' ),
+ array( '../../a', 'a' ),
+ array( '.././a', 'a' ),
+ array( './../a', 'a' ),
+ array( '././a', 'a' ),
+ array( '../../', '' ),
+ array( '.././', '' ),
+ array( './../', '' ),
+ array( '././', '' ),
+ array( '../..', '' ),
+ array( '../.', '' ),
+ array( './..', '' ),
+ array( './.', '' ),
+ array( '/../../a', '/a' ),
+ array( '/.././a', '/a' ),
+ array( '/./../a', '/a' ),
+ array( '/././a', '/a' ),
+ array( '/../../', '/' ),
+ array( '/.././', '/' ),
+ array( '/./../', '/' ),
+ array( '/././', '/' ),
+ array( '/../..', '/' ),
+ array( '/../.', '/' ),
+ array( '/./..', '/' ),
+ array( '/./.', '/' ),
+ array( 'b/../../a', '/a' ),
+ array( 'b/.././a', '/a' ),
+ array( 'b/./../a', '/a' ),
+ array( 'b/././a', 'b/a' ),
+ array( 'b/../../', '/' ),
+ array( 'b/.././', '/' ),
+ array( 'b/./../', '/' ),
+ array( 'b/././', 'b/' ),
+ array( 'b/../..', '/' ),
+ array( 'b/../.', '/' ),
+ array( 'b/./..', '/' ),
+ array( 'b/./.', 'b/' ),
+ array( '/b/../../a', '/a' ),
+ array( '/b/.././a', '/a' ),
+ array( '/b/./../a', '/a' ),
+ array( '/b/././a', '/b/a' ),
+ array( '/b/../../', '/' ),
+ array( '/b/.././', '/' ),
+ array( '/b/./../', '/' ),
+ array( '/b/././', '/b/' ),
+ array( '/b/../..', '/' ),
+ array( '/b/../.', '/' ),
+ array( '/b/./..', '/' ),
+ array( '/b/./.', '/b/' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
new file mode 100644
index 00000000..aadec87f
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @covers ::wfShorthandToInteger
+ */
+class WfShorthandToIntegerTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideABunchOfShorthands
+ */
+ public function testWfShorthandToInteger( $input, $output, $description ) {
+ $this->assertEquals(
+ wfShorthandToInteger( $input ),
+ $output,
+ $description
+ );
+ }
+
+ public static function provideABunchOfShorthands() {
+ return array(
+ array( '', -1, 'Empty string' ),
+ array( ' ', -1, 'String of spaces' ),
+ array( '1G', 1024 * 1024 * 1024, 'One gig uppercased' ),
+ array( '1g', 1024 * 1024 * 1024, 'One gig lowercased' ),
+ array( '1M', 1024 * 1024, 'One meg uppercased' ),
+ array( '1m', 1024 * 1024, 'One meg lowercased' ),
+ array( '1K', 1024, 'One kb uppercased' ),
+ array( '1k', 1024, 'One kb lowercased' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php
new file mode 100644
index 00000000..5998f186
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php
@@ -0,0 +1,134 @@
+<?php
+/*
+ * @covers ::wfTimestamp
+ */
+class WfTimestampTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideNormalTimestamps
+ */
+ public function testNormalTimestamps( $input, $format, $output, $desc ) {
+ $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
+ }
+
+ public static function provideNormalTimestamps() {
+ $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
+
+ return array(
+ // TS_UNIX
+ array( $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ),
+ array( -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ),
+ array( $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ),
+ array( $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ),
+
+ array( $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ),
+
+ // TS_MW
+ array( '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ),
+ array( '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ),
+ array( '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ),
+ array( '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ),
+
+ // TS_DB
+ array( '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ),
+ array( '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ),
+ array( '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ),
+ array( '2001-01-15 12:34:56', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_DB to TS_ISO_8601_BASIC' ),
+
+ # rfc2822 section 3.3
+ array( '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ),
+ array( 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ),
+ array( ' Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 with leading space to TS_MW' ),
+ array( '15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', '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
+ array( 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ),
+
+ # WSP = SP / HTAB ; rfc2234
+ array( "Mon, 15 Jan\x092001 12:34:56 GMT", TS_MW, '20010115123456', 'TS_RFC2822 with HTAB to TS_MW' ),
+ array( "Mon, 15 Jan\x09 \x09 2001 12:34:56 GMT", TS_MW, '20010115123456', 'TS_RFC2822 with HTAB and SP to TS_MW' ),
+ array( 'Sun, 6 Nov 94 08:49:37 GMT', TS_MW, '19941106084937', '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
+ * @dataProvider provideOldTimestamps
+ */
+ public function testOldTimestamps( $input, $format, $output, $desc ) {
+ $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
+ }
+
+ public static function provideOldTimestamps() {
+ return array(
+ array( '19011213204554', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:54 GMT', 'Earliest time according to php documentation' ),
+ array( '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ),
+ array( '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ),
+ array( '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ),
+ array( '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ),
+ array( '19011213204551', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1' ),
+ array( '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ),
+ array( '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ),
+ array( '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ),
+ array( '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ),
+ array( '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ),
+ array( '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ),
+ array( '0117-08-09 12:34:56', TS_RFC2822, 'Tue, 09 Aug 0117 12:34:56 GMT', 'Death of Roman Emperor [[Trajan]]' ),
+
+ /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
+ array( '-58979923200', TS_RFC2822, 'Sun, 01 Jan 0101 00:00:00 GMT', '1/1/101' ),
+ array( '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', '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.
+ */
+ array( '-62142076800', TS_RFC2822, 'Wed, 18 Oct 0000 00:00:00 GMT', 'ISO 8601:2004 [[year 0]], also called [[1 BC]]' ),
+ );
+ }
+
+ /**
+ * The Resource Loader uses wfTimestamp() to convert timestamps
+ * from If-Modified-Since header. Thus it must be able to parse all
+ * rfc2616 date formats
+ * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
+ * @dataProvider provideHttpDates
+ */
+ public function testHttpDate( $input, $output, $desc ) {
+ $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc );
+ }
+
+ public static function provideHttpDates() {
+ return array(
+ array( 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ),
+ array( 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ),
+ array( 'Sun Nov 6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ),
+ // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
+ array( 'Mon, 22 Nov 2010 14:12:42 GMT; length=52626', '20101122141242', 'Netscape extension to HTTP/1.0' ),
+ );
+ }
+
+ /**
+ * There are a number of assumptions in our codebase where wfTimestamp()
+ * should give the current date but it is not given a 0 there. See r71751 CR
+ */
+ public function testTimestampParameter() {
+ $now = wfTimestamp( TS_UNIX );
+ // We check that wfTimestamp doesn't return false (error) and use a LessThan assert
+ // for the cases where the test is run in a second boundary.
+
+ $zero = wfTimestamp( TS_UNIX, 0 );
+ $this->assertNotEquals( false, $zero );
+ $this->assertLessThan( 5, $zero - $now );
+
+ $empty = wfTimestamp( TS_UNIX, '' );
+ $this->assertNotEquals( false, $empty );
+ $this->assertLessThan( 5, $empty - $now );
+
+ $null = wfTimestamp( TS_UNIX, null );
+ $this->assertNotEquals( false, $null );
+ $this->assertLessThan( 5, $null - $now );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
new file mode 100644
index 00000000..ce6c82c5
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * The function only need a string parameter and might react to IIS7.0
+ * @covers ::wfUrlencode
+ */
+class WfUrlencodeTest extends MediaWikiTestCase {
+ #### TESTS ##############################################################
+
+ /**
+ * @dataProvider provideURLS
+ */
+ public function testEncodingUrlWith( $input, $expected ) {
+ $this->verifyEncodingFor( 'Apache', $input, $expected );
+ }
+
+ /**
+ * @dataProvider provideURLS
+ */
+ public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) {
+ $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected );
+ }
+
+ #### HELPERS #############################################################
+
+ /**
+ * Internal helper that actually run the test.
+ * Called by the public methods testEncodingUrlWith...()
+ *
+ */
+ private function verifyEncodingFor( $server, $input, $expectations ) {
+ $expected = $this->extractExpect( $server, $expectations );
+
+ // save up global
+ $old = isset( $_SERVER['SERVER_SOFTWARE'] )
+ ? $_SERVER['SERVER_SOFTWARE']
+ : null;
+ $_SERVER['SERVER_SOFTWARE'] = $server;
+ wfUrlencode( null );
+
+ // do the requested test
+ $this->assertEquals(
+ $expected,
+ wfUrlencode( $input ),
+ "Encoding '$input' for server '$server' should be '$expected'"
+ );
+
+ // restore global
+ if ( $old === null ) {
+ unset( $_SERVER['SERVER_SOFTWARE'] );
+ } else {
+ $_SERVER['SERVER_SOFTWARE'] = $old;
+ }
+ wfUrlencode( null );
+ }
+
+ /**
+ * Interprets the provider array. Return expected value depending
+ * the HTTP server name.
+ */
+ private function extractExpect( $server, $expectations ) {
+ if ( is_string( $expectations ) ) {
+ return $expectations;
+ } elseif ( is_array( $expectations ) ) {
+ if ( !array_key_exists( $server, $expectations ) ) {
+ throw new MWException( __METHOD__ . " expectation does not have any value for server name $server. Check the provider array.\n" );
+ } else {
+ return $expectations[$server];
+ }
+ } else {
+ throw new MWException( __METHOD__ . " given invalid expectation for '$server'. Should be a string or an array( <http server name> => <string> ).\n" );
+ }
+ }
+
+ #### PROVIDERS ###########################################################
+
+ /**
+ * Format is either:
+ * array( 'input', 'expected' );
+ * Or:
+ * array( 'input',
+ * array( 'Apache', 'expected' ),
+ * array( 'Microsoft-IIS/7', 'expected' ),
+ * ),
+ * If you want to add other HTTP server name, you will have to add a new
+ * testing method much like the testEncodingUrlWith() method above.
+ */
+ public static function provideURLS() {
+ return array(
+ ### RFC 1738 chars
+ // + is not safe
+ array( '+', '%2B' ),
+ // & and = not safe in queries
+ array( '&', '%26' ),
+ array( '=', '%3D' ),
+
+ array( ':', array(
+ 'Apache' => ':',
+ 'Microsoft-IIS/7' => '%3A',
+ ) ),
+
+ // remaining chars do not need encoding
+ array(
+ ';@$-_.!*',
+ ';@$-_.!*',
+ ),
+
+ ### Other tests
+ // slash remain unchanged. %2F seems to break things
+ array( '/', '/' ),
+
+ // Other 'funnies' chars
+ array( '[]', '%5B%5D' ),
+ array( '<>', '%3C%3E' ),
+
+ // Apostrophe is encoded
+ array( '\'', '%27' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/HTMLCheckMatrixTest.php b/tests/phpunit/includes/HTMLCheckMatrixTest.php
new file mode 100644
index 00000000..5bbafd37
--- /dev/null
+++ b/tests/phpunit/includes/HTMLCheckMatrixTest.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * Unit tests for the HTMLCheckMatrix form field
+ */
+class HtmlCheckMatrixTest extends MediaWikiTestCase {
+ static private $defaultOptions = array(
+ 'rows' => array( 'r1', 'r2' ),
+ 'columns' => array( 'c1', 'c2' ),
+ 'fieldname' => 'test',
+ );
+
+ public function testPlainInstantiation() {
+ try {
+ $form = new HTMLCheckMatrix( array() );
+ } catch ( MWException $e ) {
+ $this->assertInstanceOf( 'HTMLFormFieldRequiredOptionsException', $e );
+ return;
+ }
+
+ $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' );
+ }
+
+ public function testInstantiationWithMinimumRequiredParameters() {
+ $form = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertTrue( true ); // form instantiation must throw exception on failure
+ }
+
+ public function testValidateCallsUserDefinedValidationCallback() {
+ $called = false;
+ $field = new HTMLCheckMatrix( self::$defaultOptions + array(
+ 'validation-callback' => function() use ( &$called ) {
+ $called = true;
+ return false;
+ },
+ ) );
+ $this->assertEquals( false, $this->validate( $field, array() ) );
+ $this->assertTrue( $called );
+ }
+
+ public function testValidateRequiresArrayInput() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertEquals( false, $this->validate( $field, null ) );
+ $this->assertEquals( false, $this->validate( $field, true ) );
+ $this->assertEquals( false, $this->validate( $field, 'abc' ) );
+ $this->assertEquals( false, $this->validate( $field, new stdClass ) );
+ $this->assertEquals( true, $this->validate( $field, array() ) );
+ }
+
+ public function testValidateAllowsOnlyKnownTags() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertInternalType( 'string', $this->validate( $field, array( 'foo' ) ) );
+ }
+
+ public function testValidateAcceptsPartialTagList() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertTrue( $this->validate( $field, array() ) );
+ $this->assertTrue( $this->validate( $field, array( 'c1-r1' ) ) );
+ $this->assertTrue( $this->validate( $field, array( 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ) ) );
+ }
+
+ /**
+ * This form object actually has no visibility into what happens later on, but essentially
+ * if the data submitted by the user passes validate the following is run:
+ * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) {
+ * $user->setOption( $k, $v );
+ * }
+ */
+ public function testValuesForcedOnRemainOn() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions + array(
+ 'force-options-on' => array( 'c2-r1' ),
+ ) );
+ $expected = array(
+ 'c1-r1' => false,
+ 'c1-r2' => false,
+ 'c2-r1' => true,
+ 'c2-r2' => false,
+ );
+ $this->assertEquals( $expected, $field->filterDataForSubmit( array() ) );
+ }
+
+ public function testValuesForcedOffRemainOff() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions + array(
+ 'force-options-off' => array( 'c1-r2', 'c2-r2' ),
+ ) );
+ $expected = array(
+ 'c1-r1' => true,
+ 'c1-r2' => false,
+ 'c2-r1' => true,
+ 'c2-r2' => false,
+ );
+ // array_keys on the result simulates submitting all fields checked
+ $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) );
+ }
+
+ protected function validate( HTMLFormField $field, $submitted ) {
+ return $field->validate(
+ $submitted,
+ array( self::$defaultOptions['fieldname'] => $submitted )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/HashRingTest.php b/tests/phpunit/includes/HashRingTest.php
new file mode 100644
index 00000000..65f13696
--- /dev/null
+++ b/tests/phpunit/includes/HashRingTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @group HashRing
+ */
+class HashRingTest extends MediaWikiTestCase {
+ public function testHashRing() {
+ $ring = new HashRing( array( 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3 ) );
+
+ $locations = array();
+ for ( $i = 0; $i < 20; $i++ ) {
+ $locations[ "hello$i"] = $ring->getLocation( "hello$i" );
+ }
+ $expectedLocations = array(
+ "hello0" => "s5",
+ "hello1" => "s6",
+ "hello2" => "s2",
+ "hello3" => "s5",
+ "hello4" => "s6",
+ "hello5" => "s4",
+ "hello6" => "s5",
+ "hello7" => "s4",
+ "hello8" => "s5",
+ "hello9" => "s5",
+ "hello10" => "s3",
+ "hello11" => "s6",
+ "hello12" => "s1",
+ "hello13" => "s3",
+ "hello14" => "s3",
+ "hello15" => "s5",
+ "hello16" => "s4",
+ "hello17" => "s6",
+ "hello18" => "s6",
+ "hello19" => "s3"
+ );
+
+ $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
+
+ $locations = array();
+ for ( $i = 0; $i < 5; $i++ ) {
+ $locations[ "hello$i"] = $ring->getLocations( "hello$i", 2 );
+ }
+
+ $expectedLocations = array(
+ "hello0" => array( "s5", "s6" ),
+ "hello1" => array( "s6", "s4" ),
+ "hello2" => array( "s2", "s1" ),
+ "hello3" => array( "s5", "s6" ),
+ "hello4" => array( "s6", "s4" ),
+ );
+ $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
+ }
+}
diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php
new file mode 100644
index 00000000..81dd4870
--- /dev/null
+++ b/tests/phpunit/includes/HooksTest.php
@@ -0,0 +1,158 @@
+<?php
+
+class HooksTest extends MediaWikiTestCase {
+
+ function setUp() {
+ global $wgHooks;
+ parent::setUp();
+ Hooks::clear( 'MediaWikiHooksTest001' );
+ unset( $wgHooks['MediaWikiHooksTest001'] );
+ }
+
+ public static function provideHooks() {
+ $i = new NothingClass();
+
+ return array(
+ array( 'Object and method', array( $i, 'someNonStatic' ), 'changed-nonstatic', 'changed-nonstatic' ),
+ array( 'Object and no method', array( $i ), 'changed-onevent', 'original' ),
+ array( 'Object and method with data', array( $i, 'someNonStaticWithData', 'data' ), 'data', 'original' ),
+ array( 'Object and static method', array( $i, 'someStatic' ), 'changed-static', 'original' ),
+ array( 'Class::method static call', array( 'NothingClass::someStatic' ), 'changed-static', 'original' ),
+ array( 'Global function', array( 'NothingFunction' ), 'changed-func', 'original' ),
+ array( 'Global function with data', array( 'NothingFunctionData', 'data' ), 'data', 'original' ),
+ array( 'Closure', array( function ( &$foo, $bar ) {
+ $foo = 'changed-closure';
+
+ return true;
+ } ), 'changed-closure', 'original' ),
+ array( 'Closure with data', array( function ( $data, &$foo, $bar ) {
+ $foo = $data;
+
+ return true;
+ }, 'data' ), 'data', 'original' )
+ );
+ }
+
+ /**
+ * @dataProvider provideHooks
+ */
+ public function testOldStyleHooks( $msg, array $hook, $expectedFoo, $expectedBar ) {
+ global $wgHooks;
+ $foo = $bar = 'original';
+
+ $wgHooks['MediaWikiHooksTest001'][] = $hook;
+ wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertSame( $expectedFoo, $foo, $msg );
+ $this->assertSame( $expectedBar, $bar, $msg );
+ }
+
+ /**
+ * @dataProvider provideHooks
+ */
+ public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) {
+ $foo = $bar = 'original';
+
+ Hooks::register( 'MediaWikiHooksTest001', $hook );
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertSame( $expectedFoo, $foo, $msg );
+ $this->assertSame( $expectedBar, $bar, $msg );
+ }
+
+ public function testNewStyleHookInteraction() {
+ global $wgHooks;
+
+ $a = new NothingClass();
+ $b = new NothingClass();
+
+ $wgHooks['MediaWikiHooksTest001'][] = $a;
+ $this->assertTrue( Hooks::isRegistered( 'MediaWikiHooksTest001' ), 'Hook registered via $wgHooks should be noticed by Hooks::isRegistered' );
+
+ Hooks::register( 'MediaWikiHooksTest001', $b );
+ $this->assertEquals( 2, count( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ), 'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register' );
+
+ $foo = 'quux';
+ $bar = 'qaax';
+
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+ $this->assertEquals( 1, $a->calls, 'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register' );
+ $this->assertEquals( 1, $b->calls, 'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register' );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testUncallableFunction() {
+ Hooks::register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' );
+ Hooks::run( 'MediaWikiHooksTest001', array() );
+ }
+
+ public function testFalseReturn() {
+ Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+ return false;
+ } );
+ Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+ $foo = 'test';
+
+ return true;
+ } );
+ $foo = 'original';
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo ) );
+ $this->assertSame( 'original', $foo, 'Hooks continued processing after a false return.' );
+ }
+
+ /**
+ * @expectedException FatalError
+ */
+ public function testFatalError() {
+ Hooks::register( 'MediaWikiHooksTest001', function () {
+ return 'test';
+ } );
+ Hooks::run( 'MediaWikiHooksTest001', array() );
+ }
+}
+
+function NothingFunction( &$foo, &$bar ) {
+ $foo = 'changed-func';
+
+ return true;
+}
+
+function NothingFunctionData( $data, &$foo, &$bar ) {
+ $foo = $data;
+
+ return true;
+}
+
+class NothingClass {
+ public $calls = 0;
+
+ public static function someStatic( &$foo, &$bar ) {
+ $foo = 'changed-static';
+
+ return true;
+ }
+
+ public function someNonStatic( &$foo, &$bar ) {
+ $this->calls++;
+ $foo = 'changed-nonstatic';
+ $bar = 'changed-nonstatic';
+
+ return true;
+ }
+
+ public function onMediaWikiHooksTest001( &$foo, &$bar ) {
+ $this->calls++;
+ $foo = 'changed-onevent';
+
+ return true;
+ }
+
+ public function someNonStaticWithData( $data, &$foo, &$bar ) {
+ $this->calls++;
+ $foo = $data;
+
+ return true;
+ }
+}
diff --git a/tests/phpunit/includes/HtmlFormatterTest.php b/tests/phpunit/includes/HtmlFormatterTest.php
new file mode 100644
index 00000000..a37df74f
--- /dev/null
+++ b/tests/phpunit/includes/HtmlFormatterTest.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @group HtmlFormatter
+ */
+class HtmlFormatterTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider getHtmlData
+ */
+ public function testTransform( $input, $expected, $callback = false ) {
+ $input = self::normalize( $input );
+ $formatter = new HtmlFormatter( HtmlFormatter::wrapHTML( $input ) );
+ if ( $callback ) {
+ $callback( $formatter );
+ }
+ $formatter->filterContent();
+ $html = $formatter->getText();
+ $this->assertEquals( self::normalize( $expected ), self::normalize( $html ) );
+ }
+
+ private static function normalize( $s ) {
+ return str_replace( "\n", '',
+ str_replace( "\r", '', $s ) // "yay" to Windows!
+ );
+ }
+
+ public function getHtmlData() {
+ $removeImages = function( HtmlFormatter $f ) {
+ $f->setRemoveMedia();
+ };
+ $removeTags = function( HtmlFormatter $f ) {
+ $f->remove( array( 'table', '.foo', '#bar', 'div.baz' ) );
+ };
+ $flattenSomeStuff = function( HtmlFormatter $f ) {
+ $f->flatten( array( 's', 'div' ) );
+ };
+ $flattenEverything = function( HtmlFormatter $f ) {
+ $f->flattenAllTags();
+ };
+ return array(
+ // remove images if asked
+ array(
+ '<img src="/foo/bar.jpg" alt="Blah"/>',
+ '',
+ $removeImages,
+ ),
+ // basic tag removal
+ array(
+ '<table><tr><td>foo</td></tr></table><div class="foo">foo</div><div class="foo quux">foo</div><span id="bar">bar</span>
+<strong class="foo" id="bar">foobar</strong><div class="notfoo">test</div><div class="baz"/>
+<span class="baz">baz</span>',
+
+ '<div class="notfoo">test</div>
+<span class="baz">baz</span>',
+ $removeTags,
+ ),
+ // don't flatten tags that start like chosen ones
+ array(
+ '<div><s>foo</s> <span>bar</span></div>',
+ 'foo <span>bar</span>',
+ $flattenSomeStuff,
+ ),
+ // total flattening
+ array(
+ '<div style="foo">bar<sup>2</sup></div>',
+ 'bar2',
+ $flattenEverything,
+ ),
+ // UTF-8 preservation and security
+ array(
+ '<span title="&quot; \' &amp;">&lt;Тест!&gt;</span> &amp;&lt;&#38;&#0038;&#x26;&#x026;',
+ '<span title="&quot; \' &amp;">&lt;Тест!&gt;</span> &amp;&lt;&amp;&amp;&amp;&amp;',
+ ),
+ // https://bugzilla.wikimedia.org/show_bug.cgi?id=53086
+ array(
+ 'Foo<sup id="cite_ref-1" class="reference"><a href="#cite_note-1">[1]</a></sup> <a href="/wiki/Bar" title="Bar" class="mw-redirect">Bar</a>',
+ 'Foo<sup id="cite_ref-1" class="reference"><a href="#cite_note-1">[1]</a></sup> <a href="/wiki/Bar" title="Bar" class="mw-redirect">Bar</a>',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php
new file mode 100644
index 00000000..1c62d032
--- /dev/null
+++ b/tests/phpunit/includes/HtmlTest.php
@@ -0,0 +1,631 @@
+<?php
+/** tests for includes/Html.php */
+
+class HtmlTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $langCode = 'en';
+ $langObj = Language::factory( $langCode );
+
+ // Hardcode namespaces during test runs,
+ // so that html output based on existing namespaces
+ // can be properly evaluated.
+ $langObj->setNamespaces( array(
+ -2 => 'Media',
+ -1 => 'Special',
+ 0 => '',
+ 1 => 'Talk',
+ 2 => 'User',
+ 3 => 'User_talk',
+ 4 => 'MyWiki',
+ 5 => 'MyWiki_Talk',
+ 6 => 'File',
+ 7 => 'File_talk',
+ 8 => 'MediaWiki',
+ 9 => 'MediaWiki_talk',
+ 10 => 'Template',
+ 11 => 'Template_talk',
+ 14 => 'Category',
+ 15 => 'Category_talk',
+ 100 => 'Custom',
+ 101 => 'Custom_talk',
+ ) );
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => $langCode,
+ 'wgContLang' => $langObj,
+ 'wgLang' => $langObj,
+ 'wgWellFormedXml' => false,
+ ) );
+ }
+
+ public function testElementBasics() {
+ $this->assertEquals(
+ '<img>',
+ Html::element( 'img', null, '' ),
+ 'No close tag for short-tag elements'
+ );
+
+ $this->assertEquals(
+ '<element></element>',
+ Html::element( 'element', null, null ),
+ 'Close tag for empty element (null, null)'
+ );
+
+ $this->assertEquals(
+ '<element></element>',
+ Html::element( 'element', array(), '' ),
+ 'Close tag for empty element (array, string)'
+ );
+
+ $this->setMwGlobals( 'wgWellFormedXml', true );
+
+ $this->assertEquals(
+ '<img />',
+ Html::element( 'img', null, '' ),
+ 'Self-closing tag for short-tag elements (wgWellFormedXml = true)'
+ );
+ }
+
+ public function dataXmlMimeType() {
+ return array(
+ // ( $mimetype, $isXmlMimeType )
+ # HTML is not an XML MimeType
+ array( 'text/html', false ),
+ # XML is an XML MimeType
+ array( 'text/xml', true ),
+ array( 'application/xml', true ),
+ # XHTML is an XML MimeType
+ array( 'application/xhtml+xml', true ),
+ # Make sure other +xml MimeTypes are supported
+ # SVG is another random MimeType even though we don't use it
+ array( 'image/svg+xml', true ),
+ # Complete random other MimeTypes are not XML
+ array( 'text/plain', false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataXmlMimeType
+ */
+ public function testXmlMimeType( $mimetype, $isXmlMimeType ) {
+ $this->assertEquals( $isXmlMimeType, Html::isXmlMimeType( $mimetype ) );
+ }
+
+ public function testExpandAttributesSkipsNullAndFalse() {
+
+ ### EMPTY ########
+ $this->assertEmpty(
+ Html::expandAttributes( array( 'foo' => null ) ),
+ 'skip keys with null value'
+ );
+ $this->assertEmpty(
+ Html::expandAttributes( array( 'foo' => false ) ),
+ 'skip keys with false value'
+ );
+ $this->assertNotEmpty(
+ Html::expandAttributes( array( 'foo' => '' ) ),
+ 'keep keys with an empty string'
+ );
+ }
+
+ public function testExpandAttributesForBooleans() {
+ $this->assertEquals(
+ '',
+ Html::expandAttributes( array( 'selected' => false ) ),
+ 'Boolean attributes do not generates output when value is false'
+ );
+ $this->assertEquals(
+ '',
+ Html::expandAttributes( array( 'selected' => null ) ),
+ 'Boolean attributes do not generates output when value is null'
+ );
+
+ $this->assertEquals(
+ ' selected',
+ Html::expandAttributes( array( 'selected' => true ) ),
+ 'Boolean attributes have no value when value is true'
+ );
+ $this->assertEquals(
+ ' selected',
+ Html::expandAttributes( array( 'selected' ) ),
+ 'Boolean attributes have no value when value is true (passed as numerical array)'
+ );
+
+ $this->setMwGlobals( 'wgWellFormedXml', true );
+
+ $this->assertEquals(
+ ' selected=""',
+ Html::expandAttributes( array( 'selected' => true ) ),
+ 'Boolean attributes have empty string value when value is true (wgWellFormedXml)'
+ );
+ }
+
+ /**
+ * Test for Html::expandAttributes()
+ * Please note it output a string prefixed with a space!
+ */
+ public function testExpandAttributesVariousExpansions() {
+ ### NOT EMPTY ####
+ $this->assertEquals(
+ ' empty_string=""',
+ Html::expandAttributes( array( 'empty_string' => '' ) ),
+ 'Empty string is always quoted'
+ );
+ $this->assertEquals(
+ ' key=value',
+ Html::expandAttributes( array( 'key' => 'value' ) ),
+ 'Simple string value needs no quotes'
+ );
+ $this->assertEquals(
+ ' one=1',
+ Html::expandAttributes( array( 'one' => 1 ) ),
+ 'Number 1 value needs no quotes'
+ );
+ $this->assertEquals(
+ ' zero=0',
+ Html::expandAttributes( array( 'zero' => 0 ) ),
+ 'Number 0 value needs no quotes'
+ );
+
+ $this->setMwGlobals( 'wgWellFormedXml', true );
+
+ $this->assertEquals(
+ ' empty_string=""',
+ Html::expandAttributes( array( 'empty_string' => '' ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Empty string'
+ );
+ $this->assertEquals(
+ ' key="value"',
+ Html::expandAttributes( array( 'key' => 'value' ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Simple string'
+ );
+ $this->assertEquals(
+ ' one="1"',
+ Html::expandAttributes( array( 'one' => 1 ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Number 1'
+ );
+ $this->assertEquals(
+ ' zero="0"',
+ Html::expandAttributes( array( 'zero' => 0 ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Number 0'
+ );
+ }
+
+ /**
+ * Html::expandAttributes has special features for HTML
+ * attributes that use space separated lists and also
+ * allows arrays to be used as values.
+ */
+ public function testExpandAttributesListValueAttributes() {
+ ### STRING VALUES
+ $this->assertEquals(
+ ' class="redundant spaces here"',
+ Html::expandAttributes( array( 'class' => ' redundant spaces here ' ) ),
+ 'Normalization should strip redundant spaces'
+ );
+ $this->assertEquals(
+ ' class="foo bar"',
+ Html::expandAttributes( array( 'class' => 'foo bar foo bar bar' ) ),
+ 'Normalization should remove duplicates in string-lists'
+ );
+ ### "EMPTY" ARRAY VALUES
+ $this->assertEquals(
+ ' class=""',
+ Html::expandAttributes( array( 'class' => array() ) ),
+ 'Value with an empty array'
+ );
+ $this->assertEquals(
+ ' class=""',
+ Html::expandAttributes( array( 'class' => array( null, '', ' ', ' ' ) ) ),
+ 'Array with null, empty string and spaces'
+ );
+ ### NON-EMPTY ARRAY VALUES
+ $this->assertEquals(
+ ' class="foo bar"',
+ Html::expandAttributes( array( 'class' => array(
+ 'foo',
+ 'bar',
+ 'foo',
+ 'bar',
+ 'bar',
+ ) ) ),
+ 'Normalization should remove duplicates in the array'
+ );
+ $this->assertEquals(
+ ' class="foo bar"',
+ Html::expandAttributes( array( 'class' => array(
+ 'foo bar',
+ 'bar foo',
+ 'foo',
+ 'bar bar',
+ ) ) ),
+ 'Normalization should remove duplicates in string-lists in the array'
+ );
+ }
+
+ /**
+ * Test feature added by r96188, let pass attributes values as
+ * a PHP array. Restricted to class,rel, accesskey.
+ */
+ public function testExpandAttributesSpaceSeparatedAttributesWithBoolean() {
+ $this->assertEquals(
+ ' class="booltrue one"',
+ Html::expandAttributes( array( 'class' => array(
+ 'booltrue' => true,
+ 'one' => 1,
+
+ # Method use isset() internally, make sure we do discard
+ # attributes values which have been assigned well known values
+ 'emptystring' => '',
+ 'boolfalse' => false,
+ 'zero' => 0,
+ 'null' => null,
+ ) ) )
+ );
+ }
+
+ /**
+ * How do we handle duplicate keys in HTML attributes expansion?
+ * We could pass a "class" the values: 'GREEN' and array( 'GREEN' => false )
+ * The later will take precedence.
+ *
+ * Feature added by r96188
+ */
+ public function testValueIsAuthoritativeInSpaceSeparatedAttributesArrays() {
+ $this->assertEquals(
+ ' class=""',
+ Html::expandAttributes( array( 'class' => array(
+ 'GREEN',
+ 'GREEN' => false,
+ 'GREEN',
+ ) ) )
+ );
+ }
+
+ public function testNamespaceSelector() {
+ $this->assertEquals(
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option value=0>(Main)</option>' . "\n" .
+ '<option value=1>Talk</option>' . "\n" .
+ '<option value=2>User</option>' . "\n" .
+ '<option value=3>User talk</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(),
+ 'Basic namespace selector without custom options'
+ );
+
+ $this->assertEquals(
+ '<label for=mw-test-namespace>Select a namespace:</label>&#160;' .
+ '<select id=mw-test-namespace name=wpNamespace>' . "\n" .
+ '<option value=all>all</option>' . "\n" .
+ '<option value=0>(Main)</option>' . "\n" .
+ '<option value=1>Talk</option>' . "\n" .
+ '<option value=2 selected>User</option>' . "\n" .
+ '<option value=3>User talk</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(
+ array( 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ),
+ array( 'name' => 'wpNamespace', 'id' => 'mw-test-namespace' )
+ ),
+ 'Basic namespace selector with custom values'
+ );
+
+ $this->assertEquals(
+ '<label for=namespace>Select a namespace:</label>&#160;' .
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option value=0>(Main)</option>' . "\n" .
+ '<option value=1>Talk</option>' . "\n" .
+ '<option value=2>User</option>' . "\n" .
+ '<option value=3>User talk</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(
+ array( 'label' => 'Select a namespace:' )
+ ),
+ 'Basic namespace selector with a custom label but no id attribtue for the <select>'
+ );
+ }
+
+ public function testCanFilterOutNamespaces() {
+ $this->assertEquals(
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option value=2>User</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(
+ array( 'exclude' => array( 0, 1, 3, 100, 101 ) )
+ ),
+ 'Namespace selector namespace filtering.'
+ );
+ }
+
+ public function testCanDisableANamespaces() {
+ $this->assertEquals(
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option disabled value=0>(Main)</option>' . "\n" .
+ '<option disabled value=1>Talk</option>' . "\n" .
+ '<option disabled value=2>User</option>' . "\n" .
+ '<option disabled value=3>User talk</option>' . "\n" .
+ '<option disabled value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector( array(
+ 'disable' => array( 0, 1, 2, 3, 4 )
+ ) ),
+ 'Namespace selector namespace disabling'
+ );
+ }
+
+ /**
+ * @dataProvider provideHtml5InputTypes
+ */
+ public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
+ $this->assertEquals(
+ '<input type=' . $HTML5InputType . '>',
+ Html::element( 'input', array( 'type' => $HTML5InputType ) ),
+ 'In HTML5, HTML::element() should accept type="' . $HTML5InputType . '"'
+ );
+ }
+
+ /**
+ * List of input element types values introduced by HTML5
+ * Full list at http://www.w3.org/TR/html-markup/input.html
+ */
+ public static function provideHtml5InputTypes() {
+ $types = array(
+ 'datetime',
+ 'datetime-local',
+ 'date',
+ 'month',
+ 'time',
+ 'week',
+ 'number',
+ 'range',
+ 'email',
+ 'url',
+ 'search',
+ 'tel',
+ 'color',
+ );
+ $cases = array();
+ foreach ( $types as $type ) {
+ $cases[] = array( $type );
+ }
+
+ return $cases;
+ }
+
+ /**
+ * Test out Html::element drops or enforces default value
+ * @covers Html::dropDefaults
+ * @dataProvider provideElementsWithAttributesHavingDefaultValues
+ */
+ public function testDropDefaults( $expected, $element, $attribs, $message = '' ) {
+ $this->assertEquals( $expected, Html::element( $element, $attribs ), $message );
+ }
+
+ public static function provideElementsWithAttributesHavingDefaultValues() {
+ # Use cases in a concise format:
+ # <expected>, <element name>, <array of attributes> [, <message>]
+ # Will be mapped to Html::element()
+ $cases = array();
+
+ ### Generic cases, match $attribDefault static array
+ $cases[] = array( '<area>',
+ 'area', array( 'shape' => 'rect' )
+ );
+
+ $cases[] = array( '<button type=submit></button>',
+ 'button', array( 'formaction' => 'GET' )
+ );
+ $cases[] = array( '<button type=submit></button>',
+ 'button', array( 'formenctype' => 'application/x-www-form-urlencoded' )
+ );
+
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'height' => '150' )
+ );
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'width' => '300' )
+ );
+ # Also check with numeric values
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'height' => 150 )
+ );
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'width' => 300 )
+ );
+
+ $cases[] = array( '<command>',
+ 'command', array( 'type' => 'command' )
+ );
+
+ $cases[] = array( '<form></form>',
+ 'form', array( 'action' => 'GET' )
+ );
+ $cases[] = array( '<form></form>',
+ 'form', array( 'autocomplete' => 'on' )
+ );
+ $cases[] = array( '<form></form>',
+ 'form', array( 'enctype' => 'application/x-www-form-urlencoded' )
+ );
+
+ $cases[] = array( '<input>',
+ 'input', array( 'formaction' => 'GET' )
+ );
+ $cases[] = array( '<input>',
+ 'input', array( 'type' => 'text' )
+ );
+
+ $cases[] = array( '<keygen>',
+ 'keygen', array( 'keytype' => 'rsa' )
+ );
+
+ $cases[] = array( '<link>',
+ 'link', array( 'media' => 'all' )
+ );
+
+ $cases[] = array( '<menu></menu>',
+ 'menu', array( 'type' => 'list' )
+ );
+
+ $cases[] = array( '<script></script>',
+ 'script', array( 'type' => 'text/javascript' )
+ );
+
+ $cases[] = array( '<style></style>',
+ 'style', array( 'media' => 'all' )
+ );
+ $cases[] = array( '<style></style>',
+ 'style', array( 'type' => 'text/css' )
+ );
+
+ $cases[] = array( '<textarea></textarea>',
+ 'textarea', array( 'wrap' => 'soft' )
+ );
+
+ ### SPECIFIC CASES
+
+ # <link type="text/css">
+ $cases[] = array( '<link>',
+ 'link', array( 'type' => 'text/css' )
+ );
+
+ # <input> specific handling
+ $cases[] = array( '<input type=checkbox>',
+ 'input', array( 'type' => 'checkbox', 'value' => 'on' ),
+ 'Default value "on" is stripped of checkboxes',
+ );
+ $cases[] = array( '<input type=radio>',
+ 'input', array( 'type' => 'radio', 'value' => 'on' ),
+ 'Default value "on" is stripped of radio buttons',
+ );
+ $cases[] = array( '<input type=submit value=Submit>',
+ 'input', array( 'type' => 'submit', 'value' => 'Submit' ),
+ 'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
+ );
+ $cases[] = array( '<input type=color>',
+ 'input', array( 'type' => 'color', 'value' => '' ),
+ );
+ $cases[] = array( '<input type=range>',
+ 'input', array( 'type' => 'range', 'value' => '' ),
+ );
+
+ # <button> specific handling
+ # see remarks on http://msdn.microsoft.com/en-us/library/ie/ms535211%28v=vs.85%29.aspx
+ $cases[] = array( '<button type=submit></button>',
+ 'button', array( 'type' => 'submit' ),
+ 'According to standard the default type is "submit". Depending on compatibility mode IE might use "button", instead.',
+ );
+
+ # <select> specifc handling
+ $cases[] = array( '<select multiple></select>',
+ 'select', array( 'size' => '4', 'multiple' => true ),
+ );
+ # .. with numeric value
+ $cases[] = array( '<select multiple></select>',
+ 'select', array( 'size' => 4, 'multiple' => true ),
+ );
+ $cases[] = array( '<select></select>',
+ 'select', array( 'size' => '1', 'multiple' => false ),
+ );
+ # .. with numeric value
+ $cases[] = array( '<select></select>',
+ 'select', array( 'size' => 1, 'multiple' => false ),
+ );
+
+ # Passing an array as value
+ $cases[] = array( '<a class="css-class-one css-class-two"></a>',
+ 'a', array( 'class' => array( 'css-class-one', 'css-class-two' ) ),
+ "dropDefaults accepts values given as an array"
+ );
+
+ # FIXME: doDropDefault should remove defaults given in an array
+ # Expected should be '<a></a>'
+ $cases[] = array( '<a class=""></a>',
+ 'a', array( 'class' => array( '', '' ) ),
+ "dropDefaults accepts values given as an array"
+ );
+
+ # Craft the Html elements
+ $ret = array();
+ foreach ( $cases as $case ) {
+ $ret[] = array(
+ $case[0],
+ $case[1], $case[2],
+ isset( $case[3] ) ? $case[3] : ''
+ );
+ }
+
+ return $ret;
+ }
+
+ public function testFormValidationBlacklist() {
+ $this->assertEmpty(
+ Html::expandAttributes( array( 'min' => 1, 'max' => 100, 'pattern' => 'abc', 'required' => true, 'step' => 2 ) ),
+ 'Blacklist form validation attributes.'
+ );
+ $this->assertEquals(
+ ' step=any',
+ Html::expandAttributes( array( 'min' => 1, 'max' => 100, 'pattern' => 'abc', 'required' => true, 'step' => 'any' ) ),
+ 'Allow special case "step=any".'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/HttpTest.php b/tests/phpunit/includes/HttpTest.php
new file mode 100644
index 00000000..11d8ed60
--- /dev/null
+++ b/tests/phpunit/includes/HttpTest.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @group Broken
+ */
+class HttpTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider cookieDomains
+ * @covers Cookie::validateCookieDomain
+ */
+ public function testValidateCookieDomain( $expected, $domain, $origin = null ) {
+ if ( $origin ) {
+ $ok = Cookie::validateCookieDomain( $domain, $origin );
+ $msg = "$domain against origin $origin";
+ } else {
+ $ok = Cookie::validateCookieDomain( $domain );
+ $msg = "$domain";
+ }
+ $this->assertEquals( $expected, $ok, $msg );
+ }
+
+ public static function cookieDomains() {
+ return array(
+ array( false, "org" ),
+ array( false, ".org" ),
+ array( true, "wikipedia.org" ),
+ array( true, ".wikipedia.org" ),
+ array( false, "co.uk" ),
+ array( false, ".co.uk" ),
+ array( false, "gov.uk" ),
+ array( false, ".gov.uk" ),
+ array( true, "supermarket.uk" ),
+ array( false, "uk" ),
+ array( false, ".uk" ),
+ array( false, "127.0.0." ),
+ array( false, "127." ),
+ array( false, "127.0.0.1." ),
+ array( true, "127.0.0.1" ),
+ array( false, "333.0.0.1" ),
+ array( true, "example.com" ),
+ array( false, "example.com." ),
+ array( true, ".example.com" ),
+
+ array( true, ".example.com", "www.example.com" ),
+ array( false, "example.com", "www.example.com" ),
+ array( true, "127.0.0.1", "127.0.0.1" ),
+ array( false, "127.0.0.1", "localhost" ),
+ );
+ }
+
+ /**
+ * Test Http::isValidURI()
+ * @bug 27854 : Http::isValidURI is too lax
+ * @dataProvider provideURI
+ * @covers Http::isValidURI
+ */
+ public function testIsValidUri( $expect, $URI, $message = '' ) {
+ $this->assertEquals(
+ $expect,
+ (bool)Http::isValidURI( $URI ),
+ $message
+ );
+ }
+
+ /**
+ * Feeds URI to test a long regular expression in Http::isValidURI
+ */
+ public static function provideURI() {
+ /** Format: 'boolean expectation', 'URI to test', 'Optional message' */
+ return array(
+ array( false, '¿non sens before!! http://a', 'Allow anything before URI' ),
+
+ # (http|https) - only two schemes allowed
+ array( true, 'http://www.example.org/' ),
+ array( true, 'https://www.example.org/' ),
+ array( true, 'http://www.example.org', 'URI without directory' ),
+ array( true, 'http://a', 'Short name' ),
+ array( true, 'http://étoile', 'Allow UTF-8 in hostname' ), # 'étoile' is french for 'star'
+ array( false, '\\host\directory', 'CIFS share' ),
+ array( false, 'gopher://host/dir', 'Reject gopher scheme' ),
+ array( false, 'telnet://host', 'Reject telnet scheme' ),
+
+ # :\/\/ - double slashes
+ array( false, 'http//example.org', 'Reject missing colon in protocol' ),
+ array( false, 'http:/example.org', 'Reject missing slash in protocol' ),
+ array( false, 'http:example.org', 'Must have two slashes' ),
+ # Following fail since hostname can be made of anything
+ array( false, 'http:///example.org', 'Must have exactly two slashes, not three' ),
+
+ # (\w+:{0,1}\w*@)? - optional user:pass
+ array( true, 'http://user@host', 'Username provided' ),
+ array( true, 'http://user:@host', 'Username provided, no password' ),
+ array( true, 'http://user:pass@host', 'Username and password provided' ),
+
+ # (\S+) - host part is made of anything not whitespaces
+ array( false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ),
+ array( false, 'http://exam:ple.org/', 'hostname can not use colons!' ),
+
+ # (:[0-9]+)? - port number
+ array( true, 'http://example.org:80/' ),
+ array( true, 'https://example.org:80/' ),
+ array( true, 'http://example.org:443/' ),
+ array( true, 'https://example.org:443/' ),
+
+ # Part after the hostname is / or / with something else
+ array( true, 'http://example/#' ),
+ array( true, 'http://example/!' ),
+ array( true, 'http://example/:' ),
+ array( true, 'http://example/.' ),
+ array( true, 'http://example/?' ),
+ array( true, 'http://example/+' ),
+ array( true, 'http://example/=' ),
+ array( true, 'http://example/&' ),
+ array( true, 'http://example/%' ),
+ array( true, 'http://example/@' ),
+ array( true, 'http://example/-' ),
+ array( true, 'http://example//' ),
+ array( true, 'http://example/&' ),
+
+ # Fragment
+ array( true, 'http://exam#ple.org', ), # This one is valid, really!
+ array( true, 'http://example.org:80#anchor' ),
+ array( true, 'http://example.org/?id#anchor' ),
+ array( true, 'http://example.org/?#anchor' ),
+
+ array( false, 'http://a ¿non !!sens after', 'Allow anything after URI' ),
+ );
+ }
+
+ /**
+ * Warning:
+ *
+ * These tests are for code that makes use of an artifact of how CURL
+ * handles header reporting on redirect pages, and will need to be
+ * rewritten when bug 29232 is taken care of (high-level handling of
+ * HTTP redirects).
+ */
+ public function testRelativeRedirections() {
+ $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext' );
+
+ # Forge a Location header
+ $h->setRespHeaders( 'location', array(
+ 'http://newsite/file.ext',
+ '/newfile.ext',
+ )
+ );
+ # Verify we correctly fix the Location
+ $this->assertEquals(
+ 'http://newsite/newfile.ext',
+ $h->getFinalUrl(),
+ "Relative file path Location: interpreted as full URL"
+ );
+
+ $h->setRespHeaders( 'location', array(
+ 'https://oldsite/file.ext'
+ )
+ );
+ $this->assertEquals(
+ 'https://oldsite/file.ext',
+ $h->getFinalUrl(),
+ "Location to the HTTPS version of the site"
+ );
+
+ $h->setRespHeaders( 'location', array(
+ '/anotherfile.ext',
+ 'http://anotherfile/hoster.ext',
+ 'https://anotherfile/hoster.ext'
+ )
+ );
+ $this->assertEquals(
+ 'https://anotherfile/hoster.ext',
+ $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" )
+ );
+ }
+}
+
+/**
+ * Class to let us overwrite MWHttpRequest respHeaders variable
+ */
+class MWHttpRequestTester extends MWHttpRequest {
+
+ // function derived from the MWHttpRequest factory function but
+ // returns appropriate tester class here
+ public static function factory( $url, $options = null ) {
+ if ( !Http::$httpEngine ) {
+ Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
+ } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
+ throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
+ 'Http::$httpEngine is set to "curl"' );
+ }
+
+ switch ( Http::$httpEngine ) {
+ case 'curl':
+ return new CurlHttpRequestTester( $url, $options );
+ case 'php':
+ if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
+ throw new MWException( __METHOD__ . ': allow_url_fopen needs to be enabled for pure PHP' .
+ ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' );
+ }
+
+ return new PhpHttpRequestTester( $url, $options );
+ default:
+ }
+ }
+}
+
+class CurlHttpRequestTester extends CurlHttpRequest {
+ function setRespHeaders( $name, $value ) {
+ $this->respHeaders[$name] = $value;
+ }
+}
+
+class PhpHttpRequestTester extends PhpHttpRequest {
+ function setRespHeaders( $name, $value ) {
+ $this->respHeaders[$name] = $value;
+ }
+}
diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php
new file mode 100644
index 00000000..c074eea6
--- /dev/null
+++ b/tests/phpunit/includes/IPTest.php
@@ -0,0 +1,595 @@
+<?php
+/**
+ * Tests for IP validity functions.
+ *
+ * Ported from /t/inc/IP.t by avar.
+ *
+ * @group IP
+ * @todo Test methods in this call should be split into a method and a
+ * dataprovider.
+ */
+
+class IPTest extends MediaWikiTestCase {
+ /**
+ * not sure it should be tested with boolean false. hashar 20100924
+ * @covers IP::isIPAddress
+ */
+ 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 :: occurrence' );
+ $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::' ), 'IPv6 with a double :: occurrence, last at end' );
+ $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1' ), 'IPv6 with a double :: occurrence, 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" );
+ }
+ }
+
+ /**
+ * @covers IP::isIPv6
+ */
+ 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->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 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' ) );
+ }
+
+ /**
+ * @covers IP::isIPv4
+ */
+ 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' ) );
+ }
+
+ /**
+ * @covers IP::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, 0xF ) 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" );
+ }
+ }
+ // test with some abbreviations
+ $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+ $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+ $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' );
+ $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+ $this->assertTrue( IP::isValid( 'fc:100::' ) );
+ $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) );
+ $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) );
+
+ $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+ $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
+ $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+
+ $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
+ $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' );
+ }
+
+ /**
+ * @covers IP::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" );
+ }
+ }
+
+ /**
+ * @covers IP::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" );
+ }
+ }
+
+ /**
+ * @covers IP::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" );
+ }
+ }
+
+ /**
+ * Improve IP::sanitizeIP() code coverage
+ * @todo Most probably incomplete
+ */
+ public function testSanitizeIP() {
+ $this->assertNull( IP::sanitizeIP( '' ) );
+ $this->assertNull( IP::sanitizeIP( ' ' ) );
+ }
+
+ /**
+ * @covers IP::toUnsigned
+ * @dataProvider provideToUnsigned
+ */
+ public function testToUnsigned( $expected, $input ) {
+ $result = IP::toUnsigned( $input );
+ $this->assertTrue( $result === false || is_string( $result ) || is_int( $result ) );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * Provider for IP::testToUnsigned()
+ */
+ public static function provideToUnsigned() {
+ return array(
+ array( 1, '0.0.0.1' ),
+ array( 16909060, '1.2.3.4' ),
+ array( 2130706433, '127.0.0.1' ),
+ array( '2147483648', '128.0.0.0' ),
+ array( '3735931646', '222.173.202.254' ),
+ array( pow( 2, 32 ) - 1, '255.255.255.255' ),
+ array( false, 'IN.VA.LI.D' ),
+ array( 1, '::1' ),
+ array( '42540766452641154071740215577757643572', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ),
+ array( '42540766452641154071740215577757643572', '2001:db8:85a3::8a2e:0370:7334' ),
+ array( false, 'IN:VA::LI:D' ),
+ array( false, ':::1' )
+ );
+ }
+
+ /**
+ * @covers IP::toHex
+ * @dataProvider provideToHex
+ */
+ public function testToHex( $expected, $input ) {
+ $result = IP::toHex( $input );
+ $this->assertTrue( $result === false || is_string( $result ) );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * Provider for IP::testToHex()
+ */
+ public static function provideToHex() {
+ return array(
+ array( '00000001', '0.0.0.1' ),
+ array( '01020304', '1.2.3.4' ),
+ array( '7F000001', '127.0.0.1' ),
+ array( '80000000', '128.0.0.0' ),
+ array( 'DEADCAFE', '222.173.202.254' ),
+ array( 'FFFFFFFF', '255.255.255.255' ),
+ array( false, 'IN.VA.LI.D' ),
+ array( 'v6-00000000000000000000000000000001', '::1' ),
+ array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ),
+ array( 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ),
+ array( false, 'IN:VA::LI:D' ),
+ array( false, ':::1' )
+ );
+ }
+
+ /**
+ * @covers IP::isPublic
+ */
+ public function testPrivateIPs() {
+ $private = array( 'fc00::3', 'fc00::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" );
+ }
+ $public = array( '2001:5c0:1000:a::133', 'fc::3', '00FC::' );
+ foreach ( $public as $p ) {
+ $this->assertTrue( IP::isPublic( $p ), "$p is 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" );
+ }
+
+ /**
+ * @covers IP::hexToQuad
+ */
+ 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' ) );
+ }
+
+ /**
+ * @covers IP::hexToOctet
+ */
+ 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.
+ * @covers IP::parseCIDR
+ */
+ public 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' ) );
+
+ // @todo 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' );
+ }
+
+ /**
+ * @covers IP::canonicalize
+ */
+ public function testIPCanonicalizeOnValidIp() {
+ $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ),
+ 'Canonicalization of a valid IP returns it unchanged' );
+ }
+
+ /**
+ * @covers IP::canonicalize
+ */
+ public function testIPCanonicalizeMappedAddress() {
+ $this->assertEquals(
+ '192.0.2.152',
+ IP::canonicalize( '::ffff:192.0.2.152' )
+ );
+ $this->assertEquals(
+ '192.0.2.152',
+ IP::canonicalize( '::192.0.2.152' )
+ );
+ }
+
+ /**
+ * Issues there are most probably from IP::toHex() or IP::parseRange()
+ * @covers IP::isInRange
+ * @dataProvider provideIPsAndRanges
+ */
+ public function testIPIsInRange( $expected, $addr, $range, $message = '' ) {
+ $this->assertEquals(
+ $expected,
+ IP::isInRange( $addr, $range ),
+ $message
+ );
+ }
+
+ /** Provider for testIPIsInRange() */
+ public static function provideIPsAndRanges() {
+ # Format: (expected boolean, address, range, optional message)
+ return array(
+ # IPv4
+ array( true, '192.0.2.0', '192.0.2.0/24', 'Network address' ),
+ array( true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ),
+ array( true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ),
+
+ array( false, '0.0.0.0', '192.0.2.0/24' ),
+ array( false, '255.255.255', '192.0.2.0/24' ),
+
+ # IPv6
+ array( false, '::1', '2001:DB8::/32' ),
+ array( false, '::', '2001:DB8::/32' ),
+ array( false, 'FE80::1', '2001:DB8::/32' ),
+
+ array( true, '2001:DB8::', '2001:DB8::/32' ),
+ array( true, '2001:0DB8::', '2001:DB8::/32' ),
+ array( true, '2001:DB8::1', '2001:DB8::/32' ),
+ array( true, '2001:0DB8::1', '2001:DB8::/32' ),
+ array( true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
+ '2001:DB8::/32' ),
+
+ array( false, '2001:0DB8:F::', '2001:DB8::/96' ),
+ );
+ }
+
+ /**
+ * Test for IP::splitHostAndPort().
+ * @dataProvider provideSplitHostAndPort
+ */
+ public function testSplitHostAndPort( $expected, $input, $description ) {
+ $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
+ }
+
+ /**
+ * Provider for IP::splitHostAndPort()
+ */
+ public static function provideSplitHostAndPort() {
+ return array(
+ array( false, '[', 'Unclosed square bracket' ),
+ array( false, '[::', 'Unclosed square bracket 2' ),
+ array( array( '::', false ), '::', 'Bare IPv6 0' ),
+ array( array( '::1', false ), '::1', 'Bare IPv6 1' ),
+ array( array( '::', false ), '[::]', 'Bracketed IPv6 0' ),
+ array( array( '::1', false ), '[::1]', 'Bracketed IPv6 1' ),
+ array( array( '::1', 80 ), '[::1]:80', 'Bracketed IPv6 with port' ),
+ array( false, '::x', 'Double colon but no IPv6' ),
+ array( array( 'x', 80 ), 'x:80', 'Hostname and port' ),
+ array( false, 'x:x', 'Hostname and invalid port' ),
+ array( array( 'x', false ), 'x', 'Plain hostname' )
+ );
+ }
+
+ /**
+ * Test for IP::combineHostAndPort()
+ * @dataProvider provideCombineHostAndPort
+ */
+ public function testCombineHostAndPort( $expected, $input, $description ) {
+ list( $host, $port, $defaultPort ) = $input;
+ $this->assertEquals(
+ $expected,
+ IP::combineHostAndPort( $host, $port, $defaultPort ),
+ $description );
+ }
+
+ /**
+ * Provider for IP::combineHostAndPort()
+ */
+ public static function provideCombineHostAndPort() {
+ return array(
+ array( '[::1]', array( '::1', 2, 2 ), 'IPv6 default port' ),
+ array( '[::1]:2', array( '::1', 2, 3 ), 'IPv6 non-default port' ),
+ array( 'x', array( 'x', 2, 2 ), 'Normal default port' ),
+ array( 'x:2', array( 'x', 2, 3 ), 'Normal non-default port' ),
+ );
+ }
+
+ /**
+ * Test for IP::sanitizeRange()
+ * @dataProvider provideIPCIDRs
+ */
+ public function testSanitizeRange( $input, $expected, $description ) {
+ $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
+ }
+
+ /**
+ * Provider for IP::testSanitizeRange()
+ */
+ public static function provideIPCIDRs() {
+ return array(
+ array( '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ),
+ array( '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ),
+ array( '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ),
+ array( '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ),
+ array( '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ),
+ array( '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ),
+ array( '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ),
+ array( '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ),
+ );
+ }
+
+ /**
+ * Test for IP::prettifyIP()
+ * @dataProvider provideIPsToPrettify
+ */
+ public function testPrettifyIP( $ip, $prettified ) {
+ $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" );
+ }
+
+ /**
+ * Provider for IP::testPrettifyIP()
+ */
+ public static function provideIPsToPrettify() {
+ return array(
+ array( '0:0:0:0:0:0:0:0', '::' ),
+ array( '0:0:0::0:0:0', '::' ),
+ array( '0:0:0:1:0:0:0:0', '0:0:0:1::' ),
+ array( '0:0::f', '::f' ),
+ array( '0::0:0:0:33:fef:b', '::33:fef:b' ),
+ array( '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ),
+ array( '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ),
+ array( 'abbc:2004::0:0:0:0', 'abbc:2004::' ),
+ array( 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ),
+ array( '0:0:0:0:0:0:0:0/16', '::/16' ),
+ array( '0:0:0::0:0:0/64', '::/64' ),
+ array( '0:0::f/52', '::f/52' ),
+ array( '::0:0:33:fef:b/52', '::33:fef:b/52' ),
+ array( '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ),
+ array( '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ),
+ array( 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ),
+ array( 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/LanguageConverterTest.php b/tests/phpunit/includes/LanguageConverterTest.php
new file mode 100644
index 00000000..7c2134b9
--- /dev/null
+++ b/tests/phpunit/includes/LanguageConverterTest.php
@@ -0,0 +1,148 @@
+<?php
+
+class LanguageConverterTest extends MediaWikiLangTestCase {
+ protected $lang = null;
+ protected $lc = null;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'tg' ),
+ 'wgLanguageCode' => 'tg',
+ 'wgDefaultLanguageVariant' => false,
+ 'wgMemc' => new EmptyBagOStuff,
+ 'wgRequest' => new FauxRequest( array() ),
+ 'wgUser' => new User,
+ ) );
+
+ $this->lang = new LanguageToTest();
+ $this->lc = new TestConverter(
+ $this->lang, 'tg',
+ array( 'tg', 'tg-latn' )
+ );
+ }
+
+ protected function tearDown() {
+ unset( $this->lc );
+ unset( $this->lang );
+
+ parent::tearDown();
+ }
+
+ public function testGetPreferredVariantDefaults() {
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantHeaders() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantHeaderWeight() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg;q=1' );
+
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantHeaderWeight2() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantHeaderMulti() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'en, tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantUserOption() {
+ global $wgUser;
+
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantUserOptionForForeignLanguage() {
+ global $wgContLang, $wgUser;
+
+ $wgContLang = Language::factory( 'en' );
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant-tg', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ public function testGetPreferredVariantHeaderUserVsUrl() {
+ global $wgContLang, $wgRequest, $wgUser;
+
+ $wgContLang = Language::factory( 'tg-latn' );
+ $wgRequest->setVal( 'variant', 'tg' );
+ $wgUser = User::newFromId( "admin" );
+ $wgUser->setId( 1 );
+ $wgUser->mFrom = 'defaults';
+ $wgUser->mOptionsLoaded = true;
+ // The user's data is ignored because the variant is set in the URL.
+ $wgUser->setOption( 'variant', 'tg-latn' );
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+
+ public function testGetPreferredVariantDefaultLanguageVariant() {
+ global $wgDefaultLanguageVariant;
+
+ $wgDefaultLanguageVariant = 'tg-latn';
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ public 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 00000000..478a2ffc
--- /dev/null
+++ b/tests/phpunit/includes/LicensesTest.php
@@ -0,0 +1,22 @@
+<?php
+
+class LicensesTest extends MediaWikiTestCase {
+
+ public function testLicenses() {
+ $str = "
+* Free licenses:
+** GFDL|Debian disagrees
+";
+
+ $lc = new Licenses( array(
+ 'fieldname' => 'FooField',
+ 'type' => 'select',
+ 'section' => 'description',
+ 'id' => 'wpLicense',
+ 'label' => 'A label text', # Note can't test label-message because $wgOut is not defined
+ 'name' => 'AnotherName',
+ 'licenses' => $str,
+ ) );
+ $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) );
+ }
+}
diff --git a/tests/phpunit/includes/LinkerTest.php b/tests/phpunit/includes/LinkerTest.php
new file mode 100644
index 00000000..b605f08f
--- /dev/null
+++ b/tests/phpunit/includes/LinkerTest.php
@@ -0,0 +1,71 @@
+<?php
+
+class LinkerTest extends MediaWikiLangTestCase {
+
+ /**
+ * @dataProvider provideCasesForUserLink
+ * @covers Linker::userLink
+ */
+ public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) {
+ $this->setMwGlobals( array(
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgWellFormedXml' => true,
+ ) );
+
+ $this->assertEquals( $expected,
+ Linker::userLink( $userId, $userName, $altUserName, $msg )
+ );
+ }
+
+ public static function provideCasesForUserLink() {
+ # Format:
+ # - expected
+ # - userid
+ # - username
+ # - optional altUserName
+ # - optional message
+ return array(
+
+ ### ANONYMOUS USER ########################################
+ array(
+ '<a href="/wiki/Special:Contributions/JohnDoe" title="Special:Contributions/JohnDoe" class="mw-userlink">JohnDoe</a>',
+ 0, 'JohnDoe', false,
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/::1" title="Special:Contributions/::1" class="mw-userlink">::1</a>',
+ 0, '::1', false,
+ 'Anonymous with pretty IPv6'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/0:0:0:0:0:0:0:1" title="Special:Contributions/0:0:0:0:0:0:0:1" class="mw-userlink">::1</a>',
+ 0, '0:0:0:0:0:0:0:1', false,
+ 'Anonymous with almost pretty IPv6'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001" title="Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001" class="mw-userlink">::1</a>',
+ 0, '0000:0000:0000:0000:0000:0000:0000:0001', false,
+ 'Anonymous with full IPv6'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/::1" title="Special:Contributions/::1" class="mw-userlink">AlternativeUsername</a>',
+ 0, '::1', 'AlternativeUsername',
+ 'Anonymous with pretty IPv6 and an alternative username'
+ ),
+
+ # IPV4
+ array(
+ '<a href="/wiki/Special:Contributions/127.0.0.1" title="Special:Contributions/127.0.0.1" class="mw-userlink">127.0.0.1</a>',
+ 0, '127.0.0.1', false,
+ 'Anonymous with IPv4'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/127.0.0.1" title="Special:Contributions/127.0.0.1" class="mw-userlink">AlternativeUsername</a>',
+ 0, '127.0.0.1', 'AlternativeUsername',
+ 'Anonymous with IPv4 and an alternative username'
+ ),
+
+ ### Regular user ##########################################
+ # TODO!
+ );
+ }
+}
diff --git a/tests/phpunit/includes/LinksUpdateTest.php b/tests/phpunit/includes/LinksUpdateTest.php
new file mode 100644
index 00000000..5ade250e
--- /dev/null
+++ b/tests/phpunit/includes/LinksUpdateTest.php
@@ -0,0 +1,175 @@
+<?php
+
+/**
+ *
+ * @group Database
+ * ^--- make sure temporary tables are used.
+ */
+class LinksUpdateTest extends MediaWikiTestCase {
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge( $this->tablesUsed,
+ array(
+ 'interwiki',
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks'
+ )
+ );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace(
+ 'interwiki',
+ array( 'iw_prefix' ),
+ array(
+ 'iw_prefix' => 'linksupdatetest',
+ 'iw_url' => 'http://testing.com/wiki/$1',
+ 'iw_api' => 'http://testing.com/w/api.php',
+ 'iw_local' => 0,
+ 'iw_trans' => 0,
+ 'iw_wikiid' => 'linksupdatetest',
+ )
+ );
+ }
+
+ protected function makeTitleAndParserOutput( $name, $id ) {
+ $t = Title::newFromText( $name );
+ $t->mArticleID = $id; # XXX: this is fugly
+
+ $po = new ParserOutput();
+ $po->setTitleText( $t->getPrefixedText() );
+
+ return array( $t, $po );
+ }
+
+ public function testUpdate_pagelinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addLink( Title::newFromText( "Foo" ) );
+ $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored
+ $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
+ $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
+
+ $update = $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array(
+ array( NS_MAIN, 'Foo' ),
+ ) );
+ $this->assertArrayEquals( array(
+ Title::makeTitle( NS_MAIN, 'Foo' ), // newFromText doesn't yield the same internal state....
+ ), $update->getAddedLinks() );
+
+ $po = new ParserOutput();
+ $po->setTitleText( $t->getPrefixedText() );
+
+ $po->addLink( Title::newFromText( "Bar" ) );
+ $po->addLink( Title::newFromText( "Talk:Bar" ) );
+
+ $update = $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array(
+ array( NS_MAIN, 'Bar' ),
+ array( NS_TALK, 'Bar' ),
+ ) );
+ $this->assertArrayEquals( array(
+ Title::makeTitle( NS_MAIN, 'Bar' ),
+ Title::makeTitle( NS_TALK, 'Bar' ),
+ ), $update->getAddedLinks() );
+ $this->assertArrayEquals( array(
+ Title::makeTitle( NS_MAIN, 'Foo' ),
+ ), $update->getRemovedLinks() );
+ }
+
+ public function testUpdate_externallinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addExternalLink( "http://testing.com/wiki/Foo" );
+
+ $this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array(
+ array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ),
+ ) );
+ }
+
+ public function testUpdate_categorylinks() {
+ $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
+
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addCategory( "Foo", "FOO" );
+
+ $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array(
+ array( 'Foo', "FOO\nTESTING" ),
+ ) );
+ }
+
+ public function testUpdate_iwlinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' );
+ $po->addInterwikiLink( $target );
+
+ $this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array(
+ array( 'linksupdatetest', 'Foo' ),
+ ) );
+ }
+
+ public function testUpdate_templatelinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 );
+
+ $this->assertLinksUpdate( $t, $po, 'templatelinks', 'tl_namespace, tl_title', 'tl_from = 111', array(
+ array( NS_TEMPLATE, 'Foo' ),
+ ) );
+ }
+
+ public function testUpdate_imagelinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addImage( "Foo.png" );
+
+ $this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array(
+ array( 'Foo.png' ),
+ ) );
+ }
+
+ public function testUpdate_langlinks() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addLanguageLink( Title::newFromText( "en:Foo" )->getFullText() );
+
+ $this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array(
+ array( 'En', 'Foo' ),
+ ) );
+ }
+
+ public function testUpdate_page_props() {
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->setProperty( "foo", "bar" );
+
+ $this->assertLinksUpdate( $t, $po, 'page_props', 'pp_propname, pp_value', 'pp_page = 111', array(
+ array( 'foo', 'bar' ),
+ ) );
+ }
+
+ // @todo test recursive, too!
+
+ protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) {
+ $update = new LinksUpdate( $title, $parserOutput );
+
+ //NOTE: make sure LinksUpdate does not generate warnings when called inside a transaction.
+ $update->beginTransaction();
+ $update->doUpdate();
+ $update->commitTransaction();
+
+ $this->assertSelect( $table, $fields, $condition, $expectedRows );
+ return $update;
+ }
+}
diff --git a/tests/phpunit/includes/LocalFileTest.php b/tests/phpunit/includes/LocalFileTest.php
new file mode 100644
index 00000000..2501c783
--- /dev/null
+++ b/tests/phpunit/includes/LocalFileTest.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * These tests should work regardless of $wgCapitalLinks
+ * @group Database
+ */
+
+class LocalFileTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgCapitalLinks', true );
+
+ $info = array(
+ 'name' => 'test',
+ 'directory' => '/testdir',
+ 'url' => '/testurl',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-backend',
+ 'lockManager' => 'fsLockManager',
+ 'containerPaths' => array(
+ 'cont1' => "/testdir/local-backend/tempimages/cont1",
+ 'cont2' => "/testdir/local-backend/tempimages/cont2"
+ )
+ ) )
+ );
+ $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!' );
+ }
+
+ public function testGetHashPath() {
+ $this->assertEquals( '', $this->file_hl0->getHashPath() );
+ $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() );
+ $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() );
+ }
+
+ public 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() );
+ }
+
+ public 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() );
+ }
+
+ public function testGetArchivePath() {
+ $this->assertEquals( 'mwstore://local-backend/test-public/archive', $this->file_hl0->getArchivePath() );
+ $this->assertEquals( 'mwstore://local-backend/test-public/archive/a/a2', $this->file_hl2->getArchivePath() );
+ $this->assertEquals( 'mwstore://local-backend/test-public/archive/!', $this->file_hl0->getArchivePath( '!' ) );
+ $this->assertEquals( 'mwstore://local-backend/test-public/archive/a/a2/!', $this->file_hl2->getArchivePath( '!' ) );
+ }
+
+ public function testGetThumbPath() {
+ $this->assertEquals( 'mwstore://local-backend/test-thumb/Test!', $this->file_hl0->getThumbPath() );
+ $this->assertEquals( 'mwstore://local-backend/test-thumb/a/a2/Test!', $this->file_hl2->getThumbPath() );
+ $this->assertEquals( 'mwstore://local-backend/test-thumb/Test!/x', $this->file_hl0->getThumbPath( 'x' ) );
+ $this->assertEquals( 'mwstore://local-backend/test-thumb/a/a2/Test!/x', $this->file_hl2->getThumbPath( 'x' ) );
+ }
+
+ public 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( '!' ) );
+ }
+
+ public 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' ) );
+ }
+
+ public 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( '!' ) );
+ }
+
+ public 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( '!' ) );
+ }
+
+ public function testGetUrl() {
+ $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() );
+ $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() );
+ }
+
+ public 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/LocalisationCacheTest.php b/tests/phpunit/includes/LocalisationCacheTest.php
new file mode 100644
index 00000000..b34847aa
--- /dev/null
+++ b/tests/phpunit/includes/LocalisationCacheTest.php
@@ -0,0 +1,31 @@
+<?php
+
+class LocalisationCacheTest extends MediaWikiTestCase {
+ public function testPuralRulesFallback() {
+ $cache = Language::getLocalisationCache();
+
+ $this->assertEquals(
+ $cache->getItem( 'ar', 'pluralRules' ),
+ $cache->getItem( 'arz', 'pluralRules' ),
+ 'arz plural rules (undefined) fallback to ar (defined)'
+ );
+
+ $this->assertEquals(
+ $cache->getItem( 'ar', 'compiledPluralRules' ),
+ $cache->getItem( 'arz', 'compiledPluralRules' ),
+ 'arz compiled plural rules (undefined) fallback to ar (defined)'
+ );
+
+ $this->assertNotEquals(
+ $cache->getItem( 'ksh', 'pluralRules' ),
+ $cache->getItem( 'de', 'pluralRules' ),
+ 'ksh plural rules (defined) dont fallback to de (defined)'
+ );
+
+ $this->assertNotEquals(
+ $cache->getItem( 'ksh', 'compiledPluralRules' ),
+ $cache->getItem( 'de', 'compiledPluralRules' ),
+ 'ksh compiled plural rules (defined) dont fallback to de (defined)'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/MWExceptionHandlerTest.php b/tests/phpunit/includes/MWExceptionHandlerTest.php
new file mode 100644
index 00000000..987dfa83
--- /dev/null
+++ b/tests/phpunit/includes/MWExceptionHandlerTest.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Tests for includes/Exception.php.
+ *
+ * @author Antoine Musso
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ * @file
+ */
+
+class MWExceptionHandlerTest extends MediaWikiTestCase {
+
+ /**
+ * @covers MWExceptionHandler::getRedactedTrace
+ */
+ function testGetRedactedTrace() {
+ try {
+ $array = array( 'a', 'b' );
+ $object = new StdClass();
+ self::helperThrowAnException( $array, $object );
+ } catch (Exception $e) {
+ }
+
+ # Make sure our strack trace contains an array and an object passed to
+ # some function in the stacktrace. Else, we can not assert the trace
+ # redaction achieved its job.
+ $trace = $e->getTrace();
+ $hasObject = false;
+ $hasArray = false;
+ foreach ( $trace as $frame ) {
+ if ( ! isset( $frame['args'] ) ) {
+ continue;
+ }
+ foreach ( $frame['args'] as $arg ) {
+ $hasObject = $hasObject || is_object( $arg );
+ $hasArray = $hasArray || is_array( $arg );
+ }
+
+ if( $hasObject && $hasArray ) {
+ break;
+ }
+ }
+ $this->assertTrue( $hasObject,
+ "The stacktrace must have a function having an object has parameter" );
+ $this->assertTrue( $hasArray,
+ "The stacktrace must have a function having an array has parameter" );
+
+ # Now we redact the trace.. and make sure no function arguments are
+ # arrays or objects.
+ $redacted = MWExceptionHandler::getRedactedTrace( $e );
+
+ foreach ( $redacted as $frame ) {
+ if ( ! isset( $frame['args'] ) ) {
+ continue;
+ }
+ foreach ( $frame['args'] as $arg ) {
+ $this->assertNotInternalType( 'array', $arg);
+ $this->assertNotInternalType( 'object', $arg);
+ }
+ }
+ }
+
+ /**
+ * Helper function for testExpandArgumentsInCall
+ *
+ * Pass it an object and an array :-)
+ *
+ * @throws Exception
+ */
+ protected static function helperThrowAnException( $a, $b ) {
+ throw new Exception();
+ }
+}
diff --git a/tests/phpunit/includes/MWFunctionTest.php b/tests/phpunit/includes/MWFunctionTest.php
new file mode 100644
index 00000000..d86f2c9b
--- /dev/null
+++ b/tests/phpunit/includes/MWFunctionTest.php
@@ -0,0 +1,30 @@
+<?php
+
+class MWFunctionTest extends MediaWikiTestCase {
+ public function testNewObjFunction() {
+ $arg1 = 'Foo';
+ $arg2 = 'Bar';
+ $arg3 = array( 'Baz' );
+ $arg4 = new ExampleObject;
+
+ $args = array( $arg1, $arg2, $arg3, $arg4 );
+
+ $newObject = new MWBlankClass( $arg1, $arg2, $arg3, $arg4 );
+ $this->assertEquals(
+ MWFunction::newObj( 'MWBlankClass', $args )->args,
+ $newObject->args
+ );
+ }
+}
+
+class MWBlankClass {
+
+ public $args = array();
+
+ function __construct( $arg1, $arg2, $arg3, $arg4 ) {
+ $this->args = array( $arg1, $arg2, $arg3, $arg4 );
+ }
+}
+
+class ExampleObject {
+}
diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php
new file mode 100644
index 00000000..10e9db61
--- /dev/null
+++ b/tests/phpunit/includes/MWNamespaceTest.php
@@ -0,0 +1,571 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2011, Antoine Musso
+ * @file
+ */
+
+/**
+ * Test class for MWNamespace.
+ * Generated by PHPUnit on 2011-02-20 at 21:01:55.
+ *
+ */
+class MWNamespaceTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContentNamespaces' => array( NS_MAIN ),
+ 'wgNamespacesWithSubpages' => array(
+ NS_TALK => true,
+ NS_USER => true,
+ NS_USER_TALK => true,
+ ),
+ 'wgCapitalLinks' => true,
+ 'wgCapitalLinkOverrides' => array(),
+ 'wgNonincludableNamespaces' => array(),
+ ) );
+ }
+
+#### START OF TESTS #########################################################
+
+ /**
+ * @todo Write more texts, handle $wgAllowImageMoving setting
+ */
+ public function testIsMovable() {
+ $this->assertFalse( MWNamespace::isMovable( NS_CATEGORY ) );
+ # @todo FIXME: Write more tests!!
+ }
+
+ /**
+ * Please make sure to change testIsTalk() if you change the assertions below
+ */
+ public function testIsSubject() {
+ // Special namespaces
+ $this->assertIsSubject( NS_MEDIA );
+ $this->assertIsSubject( NS_SPECIAL );
+
+ // Subject pages
+ $this->assertIsSubject( NS_MAIN );
+ $this->assertIsSubject( NS_USER );
+ $this->assertIsSubject( 100 ); # user defined
+
+ // Talk pages
+ $this->assertIsNotSubject( NS_TALK );
+ $this->assertIsNotSubject( NS_USER_TALK );
+ $this->assertIsNotSubject( 101 ); # user defined
+ }
+
+ /**
+ * Reverse of testIsSubject().
+ * Please update testIsSubject() if you change assertions below
+ */
+ public function testIsTalk() {
+ // Special namespaces
+ $this->assertIsNotTalk( NS_MEDIA );
+ $this->assertIsNotTalk( NS_SPECIAL );
+
+ // Subject pages
+ $this->assertIsNotTalk( NS_MAIN );
+ $this->assertIsNotTalk( NS_USER );
+ $this->assertIsNotTalk( 100 ); # user defined
+
+ // Talk pages
+ $this->assertIsTalk( NS_TALK );
+ $this->assertIsTalk( NS_USER_TALK );
+ $this->assertIsTalk( 101 ); # user defined
+ }
+
+ /**
+ */
+ public function testGetSubject() {
+ // Special namespaces are their own subjects
+ $this->assertEquals( NS_MEDIA, MWNamespace::getSubject( NS_MEDIA ) );
+ $this->assertEquals( NS_SPECIAL, MWNamespace::getSubject( NS_SPECIAL ) );
+
+ $this->assertEquals( NS_MAIN, MWNamespace::getSubject( NS_TALK ) );
+ $this->assertEquals( NS_USER, MWNamespace::getSubject( NS_USER_TALK ) );
+ }
+
+ /**
+ * Regular getTalk() calls
+ * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in
+ * the function testGetTalkExceptions()
+ */
+ public function testGetTalk() {
+ $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_MAIN ) );
+ $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_TALK ) );
+ $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER ) );
+ $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER_TALK ) );
+ }
+
+ /**
+ * Exceptions with getTalk()
+ * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them.
+ * @expectedException MWException
+ */
+ public function testGetTalkExceptionsForNsMedia() {
+ $this->assertNull( MWNamespace::getTalk( NS_MEDIA ) );
+ }
+
+ /**
+ * Exceptions with getTalk()
+ * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them.
+ * @expectedException MWException
+ */
+ public function testGetTalkExceptionsForNsSpecial() {
+ $this->assertNull( MWNamespace::getTalk( NS_SPECIAL ) );
+ }
+
+ /**
+ * Regular getAssociated() calls
+ * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in
+ * the function testGetAssociatedExceptions()
+ */
+ public function testGetAssociated() {
+ $this->assertEquals( NS_TALK, MWNamespace::getAssociated( NS_MAIN ) );
+ $this->assertEquals( NS_MAIN, MWNamespace::getAssociated( NS_TALK ) );
+ }
+
+ ### Exceptions with getAssociated()
+ ### NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises
+ ### an exception for them.
+ /**
+ * @expectedException MWException
+ */
+ public function testGetAssociatedExceptionsForNsMedia() {
+ $this->assertNull( MWNamespace::getAssociated( NS_MEDIA ) );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testGetAssociatedExceptionsForNsSpecial() {
+ $this->assertNull( MWNamespace::getAssociated( NS_SPECIAL ) );
+ }
+
+ /**
+ * @todo Implement testExists().
+ */
+ /*
+ public function testExists() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+ */
+
+ /**
+ * Test MWNamespace::equals
+ * Note if we add a namespace registration system with keys like 'MAIN'
+ * we should add tests here for equivilance on things like 'MAIN' == 0
+ * and 'MAIN' == NS_MAIN.
+ */
+ public function testEquals() {
+ $this->assertTrue( MWNamespace::equals( NS_MAIN, NS_MAIN ) );
+ $this->assertTrue( MWNamespace::equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN'
+ $this->assertTrue( MWNamespace::equals( NS_USER, NS_USER ) );
+ $this->assertTrue( MWNamespace::equals( NS_USER, 2 ) );
+ $this->assertTrue( MWNamespace::equals( NS_USER_TALK, NS_USER_TALK ) );
+ $this->assertTrue( MWNamespace::equals( NS_SPECIAL, NS_SPECIAL ) );
+ $this->assertFalse( MWNamespace::equals( NS_MAIN, NS_TALK ) );
+ $this->assertFalse( MWNamespace::equals( NS_USER, NS_USER_TALK ) );
+ $this->assertFalse( MWNamespace::equals( NS_PROJECT, NS_TEMPLATE ) );
+ }
+
+ /**
+ * Test MWNamespace::subjectEquals
+ */
+ public function testSubjectEquals() {
+ $this->assertSameSubject( NS_MAIN, NS_MAIN );
+ $this->assertSameSubject( NS_MAIN, 0 ); // In case we make NS_MAIN 'MAIN'
+ $this->assertSameSubject( NS_USER, NS_USER );
+ $this->assertSameSubject( NS_USER, 2 );
+ $this->assertSameSubject( NS_USER_TALK, NS_USER_TALK );
+ $this->assertSameSubject( NS_SPECIAL, NS_SPECIAL );
+ $this->assertSameSubject( NS_MAIN, NS_TALK );
+ $this->assertSameSubject( NS_USER, NS_USER_TALK );
+
+ $this->assertDifferentSubject( NS_PROJECT, NS_TEMPLATE );
+ $this->assertDifferentSubject( NS_SPECIAL, NS_MAIN );
+ }
+
+ public function testSpecialAndMediaAreDifferentSubjects() {
+ $this->assertDifferentSubject(
+ NS_MEDIA, NS_SPECIAL,
+ "NS_MEDIA and NS_SPECIAL are different subject namespaces"
+ );
+ $this->assertDifferentSubject(
+ NS_SPECIAL, NS_MEDIA,
+ "NS_SPECIAL and NS_MEDIA are different subject namespaces"
+ );
+ }
+
+ /**
+ * @todo Implement testGetCanonicalNamespaces().
+ */
+ /*
+ public function testGetCanonicalNamespaces() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+ */
+ /**
+ * @todo Implement testGetCanonicalName().
+ */
+ /*
+ public function testGetCanonicalName() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+ */
+ /**
+ * @todo Implement testGetCanonicalIndex().
+ */
+ /*
+ public function testGetCanonicalIndex() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+ */
+
+ /**
+ * @todo Implement testGetValidNamespaces().
+ */
+ /*
+ public function testGetValidNamespaces() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+ */
+
+ /**
+ */
+ public function testCanTalk() {
+ $this->assertCanNotTalk( NS_MEDIA );
+ $this->assertCanNotTalk( NS_SPECIAL );
+
+ $this->assertCanTalk( NS_MAIN );
+ $this->assertCanTalk( NS_TALK );
+ $this->assertCanTalk( NS_USER );
+ $this->assertCanTalk( NS_USER_TALK );
+
+ // User defined namespaces
+ $this->assertCanTalk( 100 );
+ $this->assertCanTalk( 101 );
+ }
+
+ /**
+ */
+ public function testIsContent() {
+ // NS_MAIN is a content namespace per DefaultSettings.php
+ // and per function definition.
+
+ $this->assertIsContent( NS_MAIN );
+
+ // Other namespaces which are not expected to be content
+
+ $this->assertIsNotContent( NS_MEDIA );
+ $this->assertIsNotContent( NS_SPECIAL );
+ $this->assertIsNotContent( NS_TALK );
+ $this->assertIsNotContent( NS_USER );
+ $this->assertIsNotContent( NS_CATEGORY );
+ $this->assertIsNotContent( 100 );
+ }
+
+ /**
+ * Similar to testIsContent() but alters the $wgContentNamespaces
+ * global variable.
+ */
+ public function testIsContentAdvanced() {
+ global $wgContentNamespaces;
+
+ // Test that user defined namespace #252 is not content
+ $this->assertIsNotContent( 252 );
+
+ // Bless namespace # 252 as a content namespace
+ $wgContentNamespaces[] = 252;
+
+ $this->assertIsContent( 252 );
+
+ // Makes sure NS_MAIN was not impacted
+ $this->assertIsContent( NS_MAIN );
+ }
+
+ public function testIsWatchable() {
+ // Specials namespaces are not watchable
+ $this->assertIsNotWatchable( NS_MEDIA );
+ $this->assertIsNotWatchable( NS_SPECIAL );
+
+ // Core defined namespaces are watchables
+ $this->assertIsWatchable( NS_MAIN );
+ $this->assertIsWatchable( NS_TALK );
+
+ // Additional, user defined namespaces are watchables
+ $this->assertIsWatchable( 100 );
+ $this->assertIsWatchable( 101 );
+ }
+
+ public function testHasSubpages() {
+ global $wgNamespacesWithSubpages;
+
+ // Special namespaces:
+ $this->assertHasNotSubpages( NS_MEDIA );
+ $this->assertHasNotSubpages( NS_SPECIAL );
+
+ // Namespaces without subpages
+ $this->assertHasNotSubpages( NS_MAIN );
+
+ $wgNamespacesWithSubpages[NS_MAIN] = true;
+ $this->assertHasSubpages( NS_MAIN );
+
+ $wgNamespacesWithSubpages[NS_MAIN] = false;
+ $this->assertHasNotSubpages( NS_MAIN );
+
+ // Some namespaces with subpages
+ $this->assertHasSubpages( NS_TALK );
+ $this->assertHasSubpages( NS_USER );
+ $this->assertHasSubpages( NS_USER_TALK );
+ }
+
+ /**
+ */
+ public function testGetContentNamespaces() {
+ global $wgContentNamespaces;
+
+ $this->assertEquals(
+ array( NS_MAIN ),
+ MWNamespace::getContentNamespaces(),
+ '$wgContentNamespaces is an array with only NS_MAIN by default'
+ );
+
+ # test !is_array( $wgcontentNamespaces )
+ $wgContentNamespaces = '';
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ $wgContentNamespaces = false;
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ $wgContentNamespaces = null;
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ $wgContentNamespaces = 5;
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ # test $wgContentNamespaces === array()
+ $wgContentNamespaces = array();
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ # test !in_array( NS_MAIN, $wgContentNamespaces )
+ $wgContentNamespaces = array( NS_USER, NS_CATEGORY );
+ $this->assertEquals(
+ array( NS_MAIN, NS_USER, NS_CATEGORY ),
+ MWNamespace::getContentNamespaces(),
+ 'NS_MAIN is forced in $wgContentNamespaces even if unwanted'
+ );
+
+ # test other cases, return $wgcontentNamespaces as is
+ $wgContentNamespaces = array( NS_MAIN );
+ $this->assertEquals(
+ array( NS_MAIN ),
+ MWNamespace::getContentNamespaces()
+ );
+
+ $wgContentNamespaces = array( NS_MAIN, NS_USER, NS_CATEGORY );
+ $this->assertEquals(
+ array( NS_MAIN, NS_USER, NS_CATEGORY ),
+ MWNamespace::getContentNamespaces()
+ );
+ }
+
+ /**
+ */
+ public function testGetSubjectNamespaces() {
+ $subjectsNS = MWNamespace::getSubjectNamespaces();
+ $this->assertContains( NS_MAIN, $subjectsNS,
+ "Talk namespaces should have NS_MAIN" );
+ $this->assertNotContains( NS_TALK, $subjectsNS,
+ "Talk namespaces should have NS_TALK" );
+
+ $this->assertNotContains( NS_MEDIA, $subjectsNS,
+ "Talk namespaces should not have NS_MEDIA" );
+ $this->assertNotContains( NS_SPECIAL, $subjectsNS,
+ "Talk namespaces should not have NS_SPECIAL" );
+ }
+
+ /**
+ */
+ public function testGetTalkNamespaces() {
+ $talkNS = MWNamespace::getTalkNamespaces();
+ $this->assertContains( NS_TALK, $talkNS,
+ "Subject namespaces should have NS_TALK" );
+ $this->assertNotContains( NS_MAIN, $talkNS,
+ "Subject namespaces should not have NS_MAIN" );
+
+ $this->assertNotContains( NS_MEDIA, $talkNS,
+ "Subject namespaces should not have NS_MEDIA" );
+ $this->assertNotContains( NS_SPECIAL, $talkNS,
+ "Subject namespaces should not have NS_SPECIAL" );
+ }
+
+ /**
+ * Some namespaces are always capitalized per code definition
+ * in MWNamespace::$alwaysCapitalizedNamespaces
+ */
+ public function testIsCapitalizedHardcodedAssertions() {
+ // NS_MEDIA and NS_FILE are treated the same
+ $this->assertEquals(
+ MWNamespace::isCapitalized( NS_MEDIA ),
+ MWNamespace::isCapitalized( NS_FILE ),
+ 'NS_MEDIA and NS_FILE have same capitalization rendering'
+ );
+
+ // Boths are capitalized by default
+ $this->assertIsCapitalized( NS_MEDIA );
+ $this->assertIsCapitalized( NS_FILE );
+
+ // Always capitalized namespaces
+ // @see MWNamespace::$alwaysCapitalizedNamespaces
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+ }
+
+ /**
+ * Follows up for testIsCapitalizedHardcodedAssertions() but alter the
+ * global $wgCapitalLink setting to have extended coverage.
+ *
+ * MWNamespace::isCapitalized() rely on two global settings:
+ * $wgCapitalLinkOverrides = array(); by default
+ * $wgCapitalLinks = true; by default
+ * This function test $wgCapitalLinks
+ *
+ * Global setting correctness is tested against the NS_PROJECT and
+ * NS_PROJECT_TALK namespaces since they are not hardcoded nor specials
+ */
+ public function testIsCapitalizedWithWgCapitalLinks() {
+ global $wgCapitalLinks;
+
+ $this->assertIsCapitalized( NS_PROJECT );
+ $this->assertIsCapitalized( NS_PROJECT_TALK );
+
+ $wgCapitalLinks = false;
+
+ // hardcoded namespaces (see above function) are still capitalized:
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+
+ // setting is correctly applied
+ $this->assertIsNotCapitalized( NS_PROJECT );
+ $this->assertIsNotCapitalized( NS_PROJECT_TALK );
+ }
+
+ /**
+ * Counter part for MWNamespace::testIsCapitalizedWithWgCapitalLinks() now
+ * testing the $wgCapitalLinkOverrides global.
+ *
+ * @todo split groups of assertions in autonomous testing functions
+ */
+ public function testIsCapitalizedWithWgCapitalLinkOverrides() {
+ global $wgCapitalLinkOverrides;
+
+ // Test default settings
+ $this->assertIsCapitalized( NS_PROJECT );
+ $this->assertIsCapitalized( NS_PROJECT_TALK );
+
+ // hardcoded namespaces (see above function) are capitalized:
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+
+ // Hardcoded namespaces remains capitalized
+ $wgCapitalLinkOverrides[NS_SPECIAL] = false;
+ $wgCapitalLinkOverrides[NS_USER] = false;
+ $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false;
+
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+
+ $wgCapitalLinkOverrides[NS_PROJECT] = false;
+ $this->assertIsNotCapitalized( NS_PROJECT );
+
+ $wgCapitalLinkOverrides[NS_PROJECT] = true;
+ $this->assertIsCapitalized( NS_PROJECT );
+
+ unset( $wgCapitalLinkOverrides[NS_PROJECT] );
+ $this->assertIsCapitalized( NS_PROJECT );
+ }
+
+ public function testHasGenderDistinction() {
+ // Namespaces with gender distinctions
+ $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER ) );
+ $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER_TALK ) );
+
+ // Other ones, "genderless"
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_SPECIAL ) );
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MAIN ) );
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_TALK ) );
+ }
+
+ public function testIsNonincludable() {
+ global $wgNonincludableNamespaces;
+
+ $wgNonincludableNamespaces = array( NS_USER );
+
+ $this->assertTrue( MWNamespace::isNonincludable( NS_USER ) );
+ $this->assertFalse( MWNamespace::isNonincludable( NS_TEMPLATE ) );
+ }
+
+ ####### HELPERS ###########################################################
+ function __call( $method, $args ) {
+ // Call the real method if it exists
+ if ( method_exists( $this, $method ) ) {
+ return $this->$method( $args );
+ }
+
+ if ( preg_match( '/^assert(Has|Is|Can)(Not|)(Subject|Talk|Watchable|Content|Subpages|Capitalized)$/', $method, $m ) ) {
+ # Interprets arguments:
+ $ns = $args[0];
+ $msg = isset( $args[1] ) ? $args[1] : " dummy message";
+
+ # Forge the namespace constant name:
+ if ( $ns === 0 ) {
+ $ns_name = "NS_MAIN";
+ } else {
+ $ns_name = "NS_" . strtoupper( MWNamespace::getCanonicalName( $ns ) );
+ }
+ # ... and the MWNamespace method name
+ $nsMethod = strtolower( $m[1] ) . $m[3];
+
+ $expect = ( $m[2] === '' );
+ $expect_name = $expect ? 'TRUE' : 'FALSE';
+
+ return $this->assertEquals( $expect,
+ MWNamespace::$nsMethod( $ns, $msg ),
+ "MWNamespace::$nsMethod( $ns_name ) should returns $expect_name"
+ );
+ }
+
+ throw new Exception( __METHOD__ . " could not find a method named $method\n" );
+ }
+
+ function assertSameSubject( $ns1, $ns2, $msg = '' ) {
+ $this->assertTrue( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) );
+ }
+
+ function assertDifferentSubject( $ns1, $ns2, $msg = '' ) {
+ $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) );
+ }
+}
diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php
new file mode 100644
index 00000000..1e18f975
--- /dev/null
+++ b/tests/phpunit/includes/MessageTest.php
@@ -0,0 +1,130 @@
+<?php
+
+class MessageTest extends MediaWikiLangTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgForceUIMsgAsContentMsg' => array(),
+ ) );
+ }
+
+ public 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() );
+ }
+
+ public function testKey() {
+ $this->assertInstanceOf( 'Message', wfMessage( 'mainpage' ) );
+ $this->assertInstanceOf( 'Message', wfMessage( 'i-dont-exist-evar' ) );
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() );
+ $this->assertEquals( '&lt;i-dont-exist-evar&gt;', wfMessage( 'i-dont-exist-evar' )->text() );
+ $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->plain() );
+ $this->assertEquals( '&lt;i-dont-exist-evar&gt;', wfMessage( 'i-dont-exist-evar' )->escaped() );
+ }
+
+ public 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() );
+ }
+
+ public function testMessageParams() {
+ $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() );
+ }
+
+ public function testMessageParamSubstitution() {
+ $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses', 'Заглавная страница' )->plain() );
+ $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses', 'Заглавная страница $1' )->plain() );
+ $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница' )->plain() );
+ $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница $1' )->plain() );
+ }
+
+ public function testDeliciouslyManyParams() {
+ $msg = new RawMessage( '$1$2$3$4$5$6$7$8$9$10$11$12' );
+ // One less than above has placeholders
+ $params = array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k' );
+ $this->assertEquals( 'abcdefghijka2', $msg->params( $params )->plain(), 'Params > 9 are replaced correctly' );
+ }
+
+ /**
+ * FIXME: This should not need database, but Language#formatExpiry does (bug 55912)
+ * @group Database
+ */
+ public function testMessageParamTypes() {
+ $lang = Language::factory( 'en' );
+
+ $msg = new RawMessage( '$1' );
+ $this->assertEquals(
+ $lang->formatNum( 123456.789 ),
+ $msg->inLanguage( $lang )->numParams( 123456.789 )->plain(),
+ 'numParams is handled correctly'
+ );
+
+ $msg = new RawMessage( '$1' );
+ $this->assertEquals(
+ $lang->formatDuration( 1234 ),
+ $msg->inLanguage( $lang )->durationParams( 1234 )->plain(),
+ 'durationParams is handled correctly'
+ );
+
+ $msg = new RawMessage( '$1' );
+ $this->assertEquals(
+ $lang->formatExpiry( wfTimestampNow() ),
+ $msg->inLanguage( $lang )->expiryParams( wfTimestampNow() )->plain(),
+ 'expiryParams is handled correctly'
+ );
+
+ $msg = new RawMessage( '$1' );
+ $this->assertEquals(
+ $lang->formatTimePeriod( 1234 ),
+ $msg->inLanguage( $lang )->timeperiodParams( 1234 )->plain(),
+ 'timeperiodParams is handled correctly'
+ );
+
+ $msg = new RawMessage( '$1' );
+ $this->assertEquals(
+ $lang->formatSize( 123456 ),
+ $msg->inLanguage( $lang )->sizeParams( 123456 )->plain(),
+ 'sizeParams is handled correctly'
+ );
+
+ $msg = new RawMessage( '$1' );
+ $this->assertEquals(
+ $lang->formatBitrate( 123456 ),
+ $msg->inLanguage( $lang )->bitrateParams( 123456 )->plain(),
+ 'bitrateParams is handled correctly'
+ );
+ }
+
+ public function testInContentLanguageDisabled() {
+ $this->setMwGlobals( 'wgLang', Language::factory( 'fr' ) );
+
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg disabled' );
+ }
+
+ public function testInContentLanguageEnabled() {
+ $this->setMwGlobals( array(
+ 'wgLang' => Language::factory( 'fr' ),
+ 'wgForceUIMsgAsContentMsg' => array( 'mainpage' ),
+ ) );
+
+ $this->assertEquals( 'Accueil', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg enabled' );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testInLanguageThrows() {
+ wfMessage( 'foo' )->inLanguage( 123 );
+ }
+}
diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php
new file mode 100644
index 00000000..56bb0fce
--- /dev/null
+++ b/tests/phpunit/includes/OutputPageTest.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ *
+ * @author Matthew Flaschen
+ *
+ * @group Output
+ *
+ */
+class OutputPageTest extends MediaWikiTestCase {
+ const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
+ const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
+
+ /**
+ * Tests a particular case of transformCssMedia, using the given input, globals,
+ * expected return, and message
+ *
+ * Asserts that $expectedReturn is returned.
+ *
+ * options['printableQuery'] - value of query string for printable, or omitted for none
+ * options['handheldQuery'] - value of query string for handheld, or omitted for none
+ * options['media'] - passed into the method under the same name
+ * options['expectedReturn'] - expected return value
+ * options['message'] - PHPUnit message for assertion
+ *
+ * @param array $args key-value array of arguments as shown above
+ */
+ protected function assertTransformCssMediaCase( $args ) {
+ $queryData = array();
+ if ( isset( $args['printableQuery'] ) ) {
+ $queryData['printable'] = $args['printableQuery'];
+ }
+
+ if ( isset( $args['handheldQuery'] ) ) {
+ $queryData['handheld'] = $args['handheldQuery'];
+ }
+
+ $fauxRequest = new FauxRequest( $queryData, false );
+ $this->setMWGlobals( array(
+ 'wgRequest' => $fauxRequest,
+ ) );
+
+ $actualReturn = OutputPage::transformCssMedia( $args['media'] );
+ $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
+ }
+
+ /**
+ * Tests print requests
+ */
+ public function testPrintRequests() {
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => 'screen',
+ 'expectedReturn' => null,
+ 'message' => 'On printable request, screen returns null'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => self::SCREEN_MEDIA_QUERY,
+ 'expectedReturn' => null,
+ 'message' => 'On printable request, screen media query returns null'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => self::SCREEN_ONLY_MEDIA_QUERY,
+ 'expectedReturn' => null,
+ 'message' => 'On printable request, screen media query with only returns null'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => 'print',
+ 'expectedReturn' => '',
+ 'message' => 'On printable request, media print returns empty string'
+ ) );
+ }
+
+ /**
+ * Tests screen requests, without either query parameter set
+ */
+ public function testScreenRequests() {
+ $this->assertTransformCssMediaCase( array(
+ 'media' => 'screen',
+ 'expectedReturn' => 'screen',
+ 'message' => 'On screen request, screen media type is preserved'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => 'handheld',
+ 'expectedReturn' => 'handheld',
+ 'message' => 'On screen request, handheld media type is preserved'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => self::SCREEN_MEDIA_QUERY,
+ 'expectedReturn' => self::SCREEN_MEDIA_QUERY,
+ 'message' => 'On screen request, screen media query is preserved.'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => self::SCREEN_ONLY_MEDIA_QUERY,
+ 'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
+ 'message' => 'On screen request, screen media query with only is preserved.'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => 'print',
+ 'expectedReturn' => 'print',
+ 'message' => 'On screen request, print media type is preserved'
+ ) );
+ }
+
+ /**
+ * Tests handheld behavior
+ */
+ public function testHandheld() {
+ $this->assertTransformCssMediaCase( array(
+ 'handheldQuery' => '1',
+ 'media' => 'handheld',
+ 'expectedReturn' => '',
+ 'message' => 'On request with handheld querystring and media is handheld, returns empty string'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'handheldQuery' => '1',
+ 'media' => 'screen',
+ 'expectedReturn' => null,
+ 'message' => 'On request with handheld querystring and media is screen, returns null'
+ ) );
+ }
+}
diff --git a/tests/phpunit/includes/PathRouterTest.php b/tests/phpunit/includes/PathRouterTest.php
new file mode 100644
index 00000000..adfb215a
--- /dev/null
+++ b/tests/phpunit/includes/PathRouterTest.php
@@ -0,0 +1,263 @@
+<?php
+/**
+ * Tests for the PathRouter parsing.
+ *
+ * @todo Add covers tags.
+ */
+
+class PathRouterTest extends MediaWikiTestCase {
+
+ /**
+ * @var PathRouter
+ */
+ protected $basicRouter;
+
+ protected function setUp() {
+ parent::setUp();
+ $router = new PathRouter;
+ $router->add( "/wiki/$1" );
+ $this->basicRouter = $router;
+ }
+
+ /**
+ * Test basic path parsing
+ */
+ public function testBasic() {
+ $matches = $this->basicRouter->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+ }
+
+ /**
+ * Test loose path auto-$1
+ */
+ public function testLoose() {
+ $router = new PathRouter;
+ $router->add( "/" ); # Should be the same as "/$1"
+ $matches = $router->parse( "/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+
+ $router = new PathRouter;
+ $router->add( "/wiki" ); # Should be the same as /wiki/$1
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+
+ $router = new PathRouter;
+ $router->add( "/wiki/" ); # Should be the same as /wiki/$1
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+ }
+
+ /**
+ * Test to ensure that path is based on specifity, not order
+ */
+ public function testOrder() {
+ $router = new PathRouter;
+ $router->add( "/$1" );
+ $router->add( "/a/$1" );
+ $router->add( "/b/$1" );
+ $matches = $router->parse( "/a/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+
+ $router = new PathRouter;
+ $router->add( "/b/$1" );
+ $router->add( "/a/$1" );
+ $router->add( "/$1" );
+ $matches = $router->parse( "/a/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+ }
+
+ /**
+ * Test the handling of key based arrays with a url parameter
+ */
+ public function testKeyParameter() {
+ $router = new PathRouter;
+ $router->add( array( 'edit' => "/edit/$1" ), array( 'action' => '$key' ) );
+ $matches = $router->parse( "/edit/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'action' => 'edit' ) );
+ }
+
+ /**
+ * Test the handling of $2 inside paths
+ */
+ public function testAdditionalParameter() {
+ // Basic $2
+ $router = new PathRouter;
+ $router->add( '/$2/$1', array( 'test' => '$2' ) );
+ $matches = $router->parse( "/asdf/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'test' => 'asdf' ) );
+ }
+
+ /**
+ * Test additional restricted value parameter
+ */
+ public function testRestrictedValue() {
+ $router = new PathRouter;
+ $router->add( '/$2/$1',
+ array( 'test' => '$2' ),
+ array( '$2' => array( 'a', 'b' ) )
+ );
+ $router->add( '/$2/$1',
+ array( 'test2' => '$2' ),
+ array( '$2' => 'c' )
+ );
+ $router->add( '/$1' );
+
+ $matches = $router->parse( "/asdf/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "asdf/Foo" ) );
+
+ $matches = $router->parse( "/a/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'test' => 'a' ) );
+
+ $matches = $router->parse( "/c/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'test2' => 'c' ) );
+ }
+
+ public function callbackForTest( &$matches, $data ) {
+ $matches['x'] = $data['$1'];
+ $matches['foo'] = $data['foo'];
+ }
+
+ public function testCallback() {
+ $router = new PathRouter;
+ $router->add( "/$1",
+ array( 'a' => 'b', 'data:foo' => 'bar' ),
+ array( 'callback' => array( $this, 'callbackForTest' ) )
+ );
+ $matches = $router->parse( '/Foo' );
+ $this->assertEquals( $matches, array(
+ 'title' => "Foo",
+ 'x' => 'Foo',
+ 'a' => 'b',
+ 'foo' => 'bar'
+ ) );
+ }
+
+ /**
+ * Test to ensure that matches are not made if a parameter expects nonexistent input
+ */
+ public function testFail() {
+ $router = new PathRouter;
+ $router->add( "/wiki/$1", array( 'title' => "$1$2" ) );
+ $matches = $router->parse( "/wiki/A" );
+ $this->assertEquals( array(), $matches );
+ }
+
+ /**
+ * Test to ensure weight of paths is handled correctly
+ */
+ public function testWeight() {
+ $router = new PathRouter;
+ $router->addStrict( "/Bar", array( 'ping' => 'pong' ) );
+ $router->add( "/asdf-$1", array( 'title' => 'qwerty-$1' ) );
+ $router->add( "/$1" );
+ $router->add( "/qwerty-$1", array( 'title' => 'asdf-$1' ) );
+ $router->addStrict( "/Baz", array( 'marco' => 'polo' ) );
+ $router->add( "/a/$1" );
+ $router->add( "/asdf/$1" );
+ $router->add( "/$2/$1", array( 'unrestricted' => '$2' ) );
+ $router->add( array( 'qwerty' => "/qwerty/$1" ), array( 'qwerty' => '$key' ) );
+ $router->add( "/$2/$1", array( 'restricted-to-y' => '$2' ), array( '$2' => 'y' ) );
+
+ foreach (
+ array(
+ '/Foo' => array( 'title' => 'Foo' ),
+ '/Bar' => array( 'ping' => 'pong' ),
+ '/Baz' => array( 'marco' => 'polo' ),
+ '/asdf-foo' => array( 'title' => 'qwerty-foo' ),
+ '/qwerty-bar' => array( 'title' => 'asdf-bar' ),
+ '/a/Foo' => array( 'title' => 'Foo' ),
+ '/asdf/Foo' => array( 'title' => 'Foo' ),
+ '/qwerty/Foo' => array( 'title' => 'Foo', 'qwerty' => 'qwerty' ),
+ '/baz/Foo' => array( 'title' => 'Foo', 'unrestricted' => 'baz' ),
+ '/y/Foo' => array( 'title' => 'Foo', 'restricted-to-y' => 'y' ),
+ ) as $path => $result
+ ) {
+ $this->assertEquals( $router->parse( $path ), $result );
+ }
+ }
+
+ /**
+ * Make sure the router handles titles like Special:Recentchanges correctly
+ */
+ public function testSpecial() {
+ $matches = $this->basicRouter->parse( "/wiki/Special:Recentchanges" );
+ $this->assertEquals( $matches, array( 'title' => "Special:Recentchanges" ) );
+ }
+
+ /**
+ * Make sure the router decodes urlencoding properly
+ */
+ public function testUrlencoding() {
+ $matches = $this->basicRouter->parse( "/wiki/Title_With%20Space" );
+ $this->assertEquals( $matches, array( 'title' => "Title_With Space" ) );
+ }
+
+ public static function provideRegexpChars() {
+ return array(
+ array( "$" ),
+ array( "$1" ),
+ array( "\\" ),
+ array( "\\$1" ),
+ );
+ }
+
+ /**
+ * Make sure the router doesn't break on special characters like $ used in regexp replacements
+ * @dataProvider provideRegexpChars
+ */
+ public function testRegexpChars( $char ) {
+ $matches = $this->basicRouter->parse( "/wiki/$char" );
+ $this->assertEquals( $matches, array( 'title' => "$char" ) );
+ }
+
+ /**
+ * Make sure the router handles characters like +&() properly
+ */
+ public function testCharacters() {
+ $matches = $this->basicRouter->parse( "/wiki/Plus+And&Dollar\\Stuff();[]{}*" );
+ $this->assertEquals( $matches, array( 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ) );
+ }
+
+ /**
+ * Make sure the router handles unicode characters correctly
+ * @depends testSpecial
+ * @depends testUrlencoding
+ * @depends testCharacters
+ */
+ public function testUnicode() {
+ $matches = $this->basicRouter->parse( "/wiki/Spécial:Modifications_récentes" );
+ $this->assertEquals( $matches, array( 'title' => "Spécial:Modifications_récentes" ) );
+
+ $matches = $this->basicRouter->parse( "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes" );
+ $this->assertEquals( $matches, array( 'title' => "Spécial:Modifications_récentes" ) );
+ }
+
+ /**
+ * Ensure the router doesn't choke on long paths.
+ */
+ public function testLength() {
+ $matches = $this->basicRouter->parse( "/wiki/Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." );
+ $this->assertEquals( $matches, array( 'title' => "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." ) );
+ }
+
+
+ /**
+ * Ensure that the php passed site of parameter values are not urldecoded
+ */
+ public function testPatternUrlencoding() {
+ $router = new PathRouter;
+ $router->add( "/wiki/$1", array( 'title' => '%20:$1' ) );
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => '%20:Foo' ) );
+ }
+
+ /**
+ * Ensure that raw parameter values do not have any variable replacements or urldecoding
+ */
+ public function testRawParamValue() {
+ $router = new PathRouter;
+ $router->add( "/wiki/$1", array( 'title' => array( 'value' => 'bar%20$1' ) ) );
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => 'bar%20$1' ) );
+ }
+}
diff --git a/tests/phpunit/includes/PreferencesTest.php b/tests/phpunit/includes/PreferencesTest.php
new file mode 100644
index 00000000..3dec2da0
--- /dev/null
+++ b/tests/phpunit/includes/PreferencesTest.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @group Database
+ */
+class PreferencesTest extends MediaWikiTestCase {
+ /**
+ * @var User[]
+ */
+ private $prefUsers;
+ /**
+ * @var RequestContext
+ */
+ private $context;
+
+ public function __construct() {
+ parent::__construct();
+
+ $this->prefUsers['noemail'] = new User;
+
+ $this->prefUsers['notauth'] = new User;
+ $this->prefUsers['notauth']
+ ->setEmail( 'noauth@example.org' );
+
+ $this->prefUsers['auth'] = new User;
+ $this->prefUsers['auth']
+ ->setEmail( 'noauth@example.org' );
+ $this->prefUsers['auth']
+ ->setEmailAuthenticationTimestamp( 1330946623 );
+
+ $this->context = new RequestContext;
+ $this->context->setTitle( Title::newFromText( 'PreferencesTest' ) );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgEnableEmail' => true,
+ 'wgEmailAuthentication' => true,
+ ) );
+ }
+
+ /**
+ * Placeholder to verify bug 34302
+ * @covers Preferences::profilePreferences
+ */
+ public function testEmailFieldsWhenUserHasNoEmail() {
+ $prefs = $this->prefsFor( 'noemail' );
+ $this->assertArrayHasKey( 'cssclass',
+ $prefs['emailaddress']
+ );
+ $this->assertEquals( 'mw-email-none', $prefs['emailaddress']['cssclass'] );
+ }
+
+ /**
+ * Placeholder to verify bug 34302
+ * @covers Preferences::profilePreferences
+ */
+ public function testEmailFieldsWhenUserEmailNotAuthenticated() {
+ $prefs = $this->prefsFor( 'notauth' );
+ $this->assertArrayHasKey( 'cssclass',
+ $prefs['emailaddress']
+ );
+ $this->assertEquals( 'mw-email-not-authenticated', $prefs['emailaddress']['cssclass'] );
+ }
+
+ /**
+ * Placeholder to verify bug 34302
+ * @covers Preferences::profilePreferences
+ */
+ public function testEmailFieldsWhenUserEmailIsAuthenticated() {
+ $prefs = $this->prefsFor( 'auth' );
+ $this->assertArrayHasKey( 'cssclass',
+ $prefs['emailaddress']
+ );
+ $this->assertEquals( 'mw-email-authenticated', $prefs['emailaddress']['cssclass'] );
+ }
+
+ /** Helper */
+ protected function prefsFor( $user_key ) {
+ $preferences = array();
+ Preferences::profilePreferences(
+ $this->prefUsers[$user_key]
+ , $this->context
+ , $preferences
+ );
+
+ return $preferences;
+ }
+}
diff --git a/tests/phpunit/includes/RecentChangeTest.php b/tests/phpunit/includes/RecentChangeTest.php
new file mode 100644
index 00000000..cfa3e777
--- /dev/null
+++ b/tests/phpunit/includes/RecentChangeTest.php
@@ -0,0 +1,280 @@
+<?php
+/**
+ * @group Database
+ */
+class RecentChangeTest extends MediaWikiTestCase {
+ protected $title;
+ protected $target;
+ protected $user;
+ protected $user_comment;
+ protected $context;
+
+ public function __construct() {
+ parent::__construct();
+
+ $this->title = Title::newFromText( 'SomeTitle' );
+ $this->target = Title::newFromText( 'TestTarget' );
+ $this->user = User::newFromName( 'UserName' );
+
+ $this->user_comment = '<User comment about action>';
+ $this->context = RequestContext::newExtraneousContext( $this->title );
+ }
+
+ /**
+ * The testIrcMsgForAction* tests are supposed to cover the hacky
+ * LogFormatter::getIRCActionText / bug 34508
+ *
+ * Third parties bots listen to those messages. They are clever enough
+ * to fetch the i18n messages from the wiki and then analyze the IRC feed
+ * to reverse engineer the $1, $2 messages.
+ * One thing bots can not detect is when MediaWiki change the meaning of
+ * a message like what happened when we deployed 1.19. $1 became the user
+ * performing the action which broke basically all bots around.
+ *
+ * Should cover the following log actions (which are most commonly used by bots):
+ * - block/block
+ * - block/unblock
+ * - delete/delete
+ * - delete/restore
+ * - newusers/create
+ * - newusers/create2
+ * - newusers/autocreate
+ * - move/move
+ * - move/move_redir
+ * - protect/protect
+ * - protect/modifyprotect
+ * - protect/unprotect
+ * - upload/upload
+ *
+ * As well as the following Auto Edit Summaries:
+ * - blank
+ * - replace
+ * - rollback
+ * - undo
+ */
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeBlock() {
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # block/block
+ $this->assertIRCComment(
+ $this->context->msg( 'blocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'block', 'block',
+ array(),
+ $this->user_comment
+ );
+ # block/unblock
+ $this->assertIRCComment(
+ $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'block', 'unblock',
+ array(),
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeDelete() {
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # delete/delete
+ $this->assertIRCComment(
+ $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'delete', 'delete',
+ array(),
+ $this->user_comment
+ );
+
+ # delete/restore
+ $this->assertIRCComment(
+ $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'delete', 'restore',
+ array(),
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeNewusers() {
+ $this->assertIRCComment(
+ 'New user account',
+ 'newusers', 'newusers',
+ array()
+ );
+ $this->assertIRCComment(
+ 'New user account',
+ 'newusers', 'create',
+ array()
+ );
+ $this->assertIRCComment(
+ 'created new account SomeTitle',
+ 'newusers', 'create2',
+ array()
+ );
+ $this->assertIRCComment(
+ 'Account created automatically',
+ 'newusers', 'autocreate',
+ array()
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeMove() {
+ $move_params = array(
+ '4::target' => $this->target->getPrefixedText(),
+ '5::noredir' => 0,
+ );
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # move/move
+ $this->assertIRCComment(
+ $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' )->plain() . $sep . $this->user_comment,
+ 'move', 'move',
+ $move_params,
+ $this->user_comment
+ );
+
+ # move/move_redir
+ $this->assertIRCComment(
+ $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )->plain() . $sep . $this->user_comment,
+ 'move', 'move_redir',
+ $move_params,
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypePatrol() {
+ # patrol/patrol
+ $this->assertIRCComment(
+ $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(),
+ 'patrol', 'patrol',
+ array(
+ '4::curid' => '777',
+ '5::previd' => '666',
+ '6::auto' => 0,
+ )
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeProtect() {
+ $protectParams = array(
+ '[edit=sysop] (indefinite) ‎[move=sysop] (indefinite)'
+ );
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # protect/protect
+ $this->assertIRCComment(
+ $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] )->plain() . $sep . $this->user_comment,
+ 'protect', 'protect',
+ $protectParams,
+ $this->user_comment
+ );
+
+ # protect/unprotect
+ $this->assertIRCComment(
+ $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'protect', 'unprotect',
+ array(),
+ $this->user_comment
+ );
+
+ # protect/modify
+ $this->assertIRCComment(
+ $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] )->plain() . $sep . $this->user_comment,
+ 'protect', 'modify',
+ $protectParams,
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeUpload() {
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # upload/upload
+ $this->assertIRCComment(
+ $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'upload', 'upload',
+ array(),
+ $this->user_comment
+ );
+
+ # upload/overwrite
+ $this->assertIRCComment(
+ $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'upload', 'overwrite',
+ array(),
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @todo Emulate these edits somehow and extract
+ * raw edit summary from RecentChange object
+ * --
+ */
+ /*
+ public function testIrcMsgForBlankingAES() {
+ // $this->context->msg( 'autosumm-blank', .. );
+ }
+
+ public function testIrcMsgForReplaceAES() {
+ // $this->context->msg( 'autosumm-replace', .. );
+ }
+
+ public function testIrcMsgForRollbackAES() {
+ // $this->context->msg( 'revertpage', .. );
+ }
+
+ public function testIrcMsgForUndoAES() {
+ // $this->context->msg( 'undo-summary', .. );
+ }
+ */
+
+ /**
+ * @param $expected String Expected IRC text without colors codes
+ * @param $type String Log type (move, delete, suppress, patrol ...)
+ * @param $action String A log type action
+ * @param $params
+ * @param $comment String (optional) A comment for the log action
+ * @param $msg String (optional) A message for PHPUnit :-)
+ */
+ protected function assertIRCComment( $expected, $type, $action, $params, $comment = null, $msg = '' ) {
+
+ $logEntry = new ManualLogEntry( $type, $action );
+ $logEntry->setPerformer( $this->user );
+ $logEntry->setTarget( $this->title );
+ if ( $comment !== null ) {
+ $logEntry->setComment( $comment );
+ }
+ $logEntry->setParameters( $params );
+
+ $formatter = LogFormatter::newFromEntry( $logEntry );
+ $formatter->setContext( $this->context );
+
+ // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment
+ $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() );
+
+ $this->assertEquals(
+ $expected,
+ $ircRcComment,
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/RequestContextTest.php b/tests/phpunit/includes/RequestContextTest.php
new file mode 100644
index 00000000..1776b5d5
--- /dev/null
+++ b/tests/phpunit/includes/RequestContextTest.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @group Database
+ */
+class RequestContextTest extends MediaWikiTestCase {
+
+ /**
+ * Test the relationship between title and wikipage in RequestContext
+ * @covers RequestContext::getWikiPage
+ * @covers RequestContext::getTitle
+ */
+ public function testWikiPageTitle() {
+ $context = new RequestContext();
+
+ $curTitle = Title::newFromText( "A" );
+ $context->setTitle( $curTitle );
+ $this->assertTrue( $curTitle->equals( $context->getWikiPage()->getTitle() ),
+ "When a title is first set WikiPage should be created on-demand for that title." );
+
+ $curTitle = Title::newFromText( "B" );
+ $context->setWikiPage( WikiPage::factory( $curTitle ) );
+ $this->assertTrue( $curTitle->equals( $context->getTitle() ),
+ "Title must be updated when a new WikiPage is provided." );
+
+ $curTitle = Title::newFromText( "C" );
+ $context->setTitle( $curTitle );
+ $this->assertTrue( $curTitle->equals( $context->getWikiPage()->getTitle() ),
+ "When a title is updated the WikiPage should be purged and recreated on-demand with the new title." );
+ }
+
+ /**
+ * @covers RequestContext::importScopedSession
+ */
+ public function testImportScopedSession() {
+ $context = RequestContext::getMain();
+
+ $oInfo = $context->exportSession();
+ $this->assertEquals( '127.0.0.1', $oInfo['ip'], "Correct initial IP address." );
+ $this->assertEquals( 0, $oInfo['userId'], "Correct initial user ID." );
+
+ $user = User::newFromName( 'UnitTestContextUser' );
+ $user->addToDatabase();
+
+ $sinfo = array(
+ 'sessionId' => 'd612ee607c87e749ef14da4983a702cd',
+ 'userId' => $user->getId(),
+ 'ip' => '192.0.2.0',
+ 'headers' => array( 'USER-AGENT' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0' )
+ );
+ $sc = RequestContext::importScopedSession( $sinfo ); // load new context
+
+ $info = $context->exportSession();
+ $this->assertEquals( $sinfo['ip'], $info['ip'], "Correct IP address." );
+ $this->assertEquals( $sinfo['headers'], $info['headers'], "Correct headers." );
+ $this->assertEquals( $sinfo['sessionId'], $info['sessionId'], "Correct session ID." );
+ $this->assertEquals( $sinfo['userId'], $info['userId'], "Correct user ID." );
+ $this->assertEquals( $sinfo['ip'], $context->getRequest()->getIP(), "Correct context IP address." );
+ $this->assertEquals( $sinfo['headers'], $context->getRequest()->getAllHeaders(), "Correct context headers." );
+ $this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
+ $this->assertEquals( true, $context->getUser()->isLoggedIn(), "Correct context user." );
+ $this->assertEquals( $sinfo['userId'], $context->getUser()->getId(), "Correct context user ID." );
+ $this->assertEquals( 'UnitTestContextUser', $context->getUser()->getName(), "Correct context user name." );
+
+ unset( $sc ); // restore previous context
+
+ $info = $context->exportSession();
+ $this->assertEquals( $oInfo['ip'], $info['ip'], "Correct initial IP address." );
+ $this->assertEquals( $oInfo['headers'], $info['headers'], "Correct initial headers." );
+ $this->assertEquals( $oInfo['sessionId'], $info['sessionId'], "Correct initial session ID." );
+ $this->assertEquals( $oInfo['userId'], $info['userId'], "Correct initial user ID." );
+ }
+}
diff --git a/tests/phpunit/includes/ResourceLoaderTest.php b/tests/phpunit/includes/ResourceLoaderTest.php
new file mode 100644
index 00000000..ca8b2b6e
--- /dev/null
+++ b/tests/phpunit/includes/ResourceLoaderTest.php
@@ -0,0 +1,149 @@
+<?php
+
+class ResourceLoaderTest extends MediaWikiTestCase {
+
+ protected static $resourceLoaderRegisterModulesHook;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths; $wgResourceLoaderLESSVars;
+
+ $this->setMwGlobals( array(
+ 'wgResourceLoaderLESSFunctions' => array(
+ 'test-sum' => function ( $frame, $less ) {
+ $sum = 0;
+ foreach ( $frame[2] as $arg ) {
+ $sum += (int)$arg[1];
+ }
+ return $sum;
+ },
+ ),
+ 'wgResourceLoaderLESSImportPaths' => array(
+ dirname( __DIR__ ) . '/data/less/common',
+ ),
+ 'wgResourceLoaderLESSVars' => array(
+ 'foo' => '2px',
+ 'Foo' => '#eeeeee',
+ 'bar' => 5,
+ ),
+ ) );
+ }
+
+ /* Hook Methods */
+
+ /**
+ * ResourceLoaderRegisterModules hook
+ */
+ public static function resourceLoaderRegisterModules( &$resourceLoader ) {
+ self::$resourceLoaderRegisterModulesHook = true;
+
+ return true;
+ }
+
+ /* Provider Methods */
+ public static function provideValidModules() {
+ return array(
+ array( 'TEST.validModule1', new ResourceLoaderTestModule() ),
+ );
+ }
+
+ public static function provideResourceLoaderContext() {
+ $resourceLoader = new ResourceLoader();
+ $request = new FauxRequest();
+ return array(
+ array( new ResourceLoaderContext( $resourceLoader, $request ) ),
+ );
+ }
+
+ /* 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 ) );
+ }
+
+ /**
+ * @dataProvider provideResourceLoaderContext
+ * @covers ResourceLoaderFileModule::compileLessFile
+ */
+ public function testLessFileCompilation( $context ) {
+ $basePath = __DIR__ . '/../data/less/module';
+ $module = new ResourceLoaderFileModule( array(
+ 'localBasePath' => $basePath,
+ 'styles' => array( 'styles.less' ),
+ ) );
+ $styles = $module->getStyles( $context );
+ $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
+ }
+
+ /**
+ * @dataProvider providePackedModules
+ * @covers ResourceLoader::makePackedModulesString
+ */
+ public function testMakePackedModulesString( $desc, $modules, $packed ) {
+ $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
+ }
+
+ /**
+ * @dataProvider providePackedModules
+ * @covers ResourceLoaderContext::expandModuleNames
+ */
+ public function testexpandModuleNames( $desc, $modules, $packed ) {
+ $this->assertEquals( $modules, ResourceLoaderContext::expandModuleNames( $packed ), $desc );
+ }
+
+ public static function providePackedModules() {
+ return array(
+ array(
+ 'Example from makePackedModulesString doc comment',
+ array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ),
+ 'foo.bar,baz|bar.baz,quux',
+ ),
+ array(
+ 'Example from expandModuleNames doc comment',
+ array( 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ),
+ 'jquery.foo,bar|jquery.ui.baz,quux',
+ ),
+ array(
+ 'Regression fixed in r88706 with dotless names',
+ array( 'foo', 'bar', 'baz' ),
+ 'foo,bar,baz',
+ ),
+ array(
+ 'Prefixless modules after a prefixed module',
+ array( 'single.module', 'foobar', 'foobaz' ),
+ 'single.module|foobar,foobaz',
+ ),
+ );
+ }
+}
+
+/* Stubs */
+
+class ResourceLoaderTestModule extends ResourceLoaderModule {
+}
+
+/* Hooks */
+global $wgHooks;
+$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules';
diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php
new file mode 100644
index 00000000..e17c7b0f
--- /dev/null
+++ b/tests/phpunit/includes/RevisionStorageTest.php
@@ -0,0 +1,546 @@
+<?php
+
+/**
+ * Test class for Revision storage.
+ *
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ *
+ * @group medium
+ * ^--- important, causes tests not to fail with timeout
+ */
+class RevisionStorageTest extends MediaWikiTestCase {
+
+ /**
+ * @var WikiPage $the_page
+ */
+ var $the_page;
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge( $this->tablesUsed,
+ array( 'page',
+ 'revision',
+ 'text',
+
+ 'recentchanges',
+ 'logging',
+
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks' ) );
+ }
+
+ protected function setUp() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::setUp();
+
+ $wgExtraNamespaces[12312] = 'Dummy';
+ $wgExtraNamespaces[12313] = 'Dummy_talk';
+
+ $wgNamespaceContentModels[12312] = 'DUMMY';
+ $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting';
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ if ( !$this->the_page ) {
+ $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page", CONTENT_MODEL_WIKITEXT );
+ }
+ }
+
+ public function tearDown() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::tearDown();
+
+ unset( $wgExtraNamespaces[12312] );
+ unset( $wgExtraNamespaces[12313] );
+
+ unset( $wgNamespaceContentModels[12312] );
+ unset( $wgContentHandlers['DUMMY'] );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ protected function makeRevision( $props = null ) {
+ if ( $props === null ) {
+ $props = array();
+ }
+
+ if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
+ $props['text'] = 'Lorem Ipsum';
+ }
+
+ if ( !isset( $props['comment'] ) ) {
+ $props['comment'] = 'just a test';
+ }
+
+ if ( !isset( $props['page'] ) ) {
+ $props['page'] = $this->the_page->getId();
+ }
+
+ $rev = new Revision( $props );
+
+ $dbw = wfgetDB( DB_MASTER );
+ $rev->insertOn( $dbw );
+
+ return $rev;
+ }
+
+ protected function createPage( $page, $text, $model = null ) {
+ if ( is_string( $page ) ) {
+ if ( !preg_match( '/:/', $page ) &&
+ ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
+ ) {
+ $ns = $this->getDefaultWikitextNS();
+ $page = MWNamespace::getCanonicalName( $ns ) . ':' . $page;
+ }
+
+ $page = Title::newFromText( $page );
+ }
+
+ if ( $page instanceof Title ) {
+ $page = new WikiPage( $page );
+ }
+
+ if ( $page->exists() ) {
+ $page->doDeleteArticle( "done" );
+ }
+
+ $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+ $page->doEditContent( $content, "testing", EDIT_NEW );
+
+ return $page;
+ }
+
+ protected function assertRevEquals( Revision $orig, Revision $rev = null ) {
+ $this->assertNotNull( $rev, 'missing revision' );
+
+ $this->assertEquals( $orig->getId(), $rev->getId() );
+ $this->assertEquals( $orig->getPage(), $rev->getPage() );
+ $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
+ $this->assertEquals( $orig->getUser(), $rev->getUser() );
+ $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
+ $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
+ $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
+ }
+
+ /**
+ * @covers Revision::__construct
+ */
+ public function testConstructFromRow() {
+ $orig = $this->makeRevision();
+
+ $dbr = wfgetDB( DB_SLAVE );
+ $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+
+ $rev = new Revision( $row );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::newFromRow
+ */
+ public function testNewFromRow() {
+ $orig = $this->makeRevision();
+
+ $dbr = wfgetDB( DB_SLAVE );
+ $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+
+ $rev = Revision::newFromRow( $row );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+
+ /**
+ * @covers Revision::newFromArchiveRow
+ */
+ public function testNewFromArchiveRow() {
+ $page = $this->createPage( 'RevisionStorageTest_testNewFromArchiveRow', 'Lorem Ipsum', CONTENT_MODEL_WIKITEXT );
+ $orig = $page->getRevision();
+ $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
+
+ $dbr = wfgetDB( DB_SLAVE );
+ $res = $dbr->select( 'archive', '*', array( 'ar_rev_id' => $orig->getId() ) );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+
+ $rev = Revision::newFromArchiveRow( $row );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::newFromId
+ */
+ public function testNewFromId() {
+ $orig = $this->makeRevision();
+
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::fetchRevision
+ */
+ public function testFetchRevision() {
+ $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one', CONTENT_MODEL_WIKITEXT );
+ $id1 = $page->getRevision()->getId();
+
+ $page->doEditContent( new WikitextContent( 'two' ), 'second rev' );
+ $id2 = $page->getRevision()->getId();
+
+ $res = Revision::fetchRevision( $page->getTitle() );
+
+ #note: order is unspecified
+ $rows = array();
+ while ( ( $row = $res->fetchObject() ) ) {
+ $rows[$row->rev_id] = $row;
+ }
+
+ $row = $res->fetchObject();
+ $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
+ $this->assertArrayHasKey( $id2, $rows, 'missing revision with id ' . $id2 );
+ }
+
+ /**
+ * @covers Revision::selectFields
+ */
+ public function testSelectFields() {
+ global $wgContentHandlerUseDB;
+
+ $fields = Revision::selectFields();
+
+ $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' );
+ $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' );
+ $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields' );
+ $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' );
+
+ if ( $wgContentHandlerUseDB ) {
+ $this->assertTrue( in_array( 'rev_content_model', $fields ),
+ 'missing rev_content_model in list of fields' );
+ $this->assertTrue( in_array( 'rev_content_format', $fields ),
+ 'missing rev_content_format in list of fields' );
+ }
+ }
+
+ /**
+ * @covers Revision::getPage
+ */
+ public function testGetPage() {
+ $page = $this->the_page;
+
+ $orig = $this->makeRevision( array( 'page' => $page->getId() ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( $page->getId(), $rev->getPage() );
+ }
+
+ /**
+ * @covers Revision::getText
+ */
+ public function testGetText() {
+ $this->hideDeprecated( 'Revision::getText' );
+
+ $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello.', $rev->getText() );
+ }
+
+ /**
+ * @covers Revision::getContent
+ */
+ public function testGetContent_failure() {
+ $rev = new Revision( array(
+ 'page' => $this->the_page->getId(),
+ 'content_model' => $this->the_page->getContentModel(),
+ 'text_id' => 123456789, // not in the test DB
+ ) );
+
+ $this->assertNull( $rev->getContent(),
+ "getContent() should return null if the revision's text blob could not be loaded." );
+
+ //NOTE: check this twice, once for lazy initialization, and once with the cached value.
+ $this->assertNull( $rev->getContent(),
+ "getContent() should return null if the revision's text blob could not be loaded." );
+ }
+
+ /**
+ * @covers Revision::getContent
+ */
+ public function testGetContent() {
+ $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
+ }
+
+ /**
+ * @covers Revision::revText
+ */
+ public function testRevText() {
+ $this->hideDeprecated( 'Revision::revText' );
+ $orig = $this->makeRevision( array( 'text' => 'hello hello rev.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello rev.', $rev->revText() );
+ }
+
+ /**
+ * @covers Revision::getRawText
+ */
+ public function testGetRawText() {
+ $this->hideDeprecated( 'Revision::getRawText' );
+
+ $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
+ }
+
+ /**
+ * @covers Revision::getContentModel
+ */
+ public function testGetContentModel() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $orig = $this->makeRevision( array( 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+ }
+
+ /**
+ * @covers Revision::getContentFormat
+ */
+ public function testGetContentFormat() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $orig = $this->makeRevision( array(
+ 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT,
+ 'content_format' => CONTENT_FORMAT_JAVASCRIPT
+ ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
+ }
+
+ /**
+ * @covers Revision::isCurrent
+ */
+ public function testIsCurrent() {
+ $page = $this->createPage( 'RevisionStorageTest_testIsCurrent', 'Lorem Ipsum', CONTENT_MODEL_WIKITEXT );
+ $rev1 = $page->getRevision();
+
+ # @todo find out if this should be true
+ # $this->assertTrue( $rev1->isCurrent() );
+
+ $rev1x = Revision::newFromId( $rev1->getId() );
+ $this->assertTrue( $rev1x->isCurrent() );
+
+ $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ), 'second rev' );
+ $rev2 = $page->getRevision();
+
+ # @todo find out if this should be true
+ # $this->assertTrue( $rev2->isCurrent() );
+
+ $rev1x = Revision::newFromId( $rev1->getId() );
+ $this->assertFalse( $rev1x->isCurrent() );
+
+ $rev2x = Revision::newFromId( $rev2->getId() );
+ $this->assertTrue( $rev2x->isCurrent() );
+ }
+
+ /**
+ * @covers Revision::getPrevious
+ */
+ public function testGetPrevious() {
+ $page = $this->createPage( 'RevisionStorageTest_testGetPrevious', 'Lorem Ipsum testGetPrevious', CONTENT_MODEL_WIKITEXT );
+ $rev1 = $page->getRevision();
+
+ $this->assertNull( $rev1->getPrevious() );
+
+ $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ 'second rev testGetPrevious' );
+ $rev2 = $page->getRevision();
+
+ $this->assertNotNull( $rev2->getPrevious() );
+ $this->assertEquals( $rev1->getId(), $rev2->getPrevious()->getId() );
+ }
+
+ /**
+ * @covers Revision::getNext
+ */
+ public function testGetNext() {
+ $page = $this->createPage( 'RevisionStorageTest_testGetNext', 'Lorem Ipsum testGetNext', CONTENT_MODEL_WIKITEXT );
+ $rev1 = $page->getRevision();
+
+ $this->assertNull( $rev1->getNext() );
+
+ $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ 'second rev testGetNext' );
+ $rev2 = $page->getRevision();
+
+ $this->assertNotNull( $rev1->getNext() );
+ $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
+ }
+
+ /**
+ * @covers Revision::newNullRevision
+ */
+ public function testNewNullRevision() {
+ $page = $this->createPage( 'RevisionStorageTest_testNewNullRevision', 'some testing text', CONTENT_MODEL_WIKITEXT );
+ $orig = $page->getRevision();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false );
+
+ $this->assertNotEquals( $orig->getId(), $rev->getId(),
+ 'new null revision shold have a different id from the original revision' );
+ $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
+ 'new null revision shold have the same text id as the original revision' );
+ $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
+ }
+
+ public static function provideUserWasLastToEdit() {
+ return array(
+ array( #0
+ 3, true, # actually the last edit
+ ),
+ array( #1
+ 2, true, # not the current edit, but still by this user
+ ),
+ array( #2
+ 1, false, # edit by another user
+ ),
+ array( #3
+ 0, false, # first edit, by this user, but another user edited in the mean time
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUserWasLastToEdit
+ */
+ public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
+ $userA = User::newFromName( "RevisionStorageTest_userA" );
+ $userB = User::newFromName( "RevisionStorageTest_userB" );
+
+ if ( $userA->getId() === 0 ) {
+ $userA = User::createNew( $userA->getName() );
+ }
+
+ if ( $userB->getId() === 0 ) {
+ $userB = User::createNew( $userB->getName() );
+ }
+
+ $ns = $this->getDefaultWikitextNS();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $revisions = array();
+
+ // create revisions -----------------------------
+ $page = WikiPage::factory( Title::newFromText(
+ 'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
+
+ # zero
+ $revisions[0] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(), // we need the title to determine the page's default content model
+ 'timestamp' => '20120101000000',
+ 'user' => $userA->getId(),
+ 'text' => 'zero',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit zero'
+ ) );
+ $revisions[0]->insertOn( $dbw );
+
+ # one
+ $revisions[1] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(), // still need the title, because $page->getId() is 0 (there's no entry in the page table)
+ 'timestamp' => '20120101000100',
+ 'user' => $userA->getId(),
+ 'text' => 'one',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit one'
+ ) );
+ $revisions[1]->insertOn( $dbw );
+
+ # two
+ $revisions[2] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000200',
+ 'user' => $userB->getId(),
+ 'text' => 'two',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit two'
+ ) );
+ $revisions[2]->insertOn( $dbw );
+
+ # three
+ $revisions[3] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000300',
+ 'user' => $userA->getId(),
+ 'text' => 'three',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit three'
+ ) );
+ $revisions[3]->insertOn( $dbw );
+
+ # four
+ $revisions[4] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000200',
+ 'user' => $userA->getId(),
+ 'text' => 'zero',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit four'
+ ) );
+ $revisions[4]->insertOn( $dbw );
+
+ // test it ---------------------------------
+ $since = $revisions[$sinceIdx]->getTimestamp();
+
+ $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
+
+ $this->assertEquals( $expectedLast, $wasLast );
+ }
+}
diff --git a/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php
new file mode 100644
index 00000000..4e83e355
--- /dev/null
+++ b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ */
+class RevisionTest_ContentHandlerUseDB extends RevisionStorageTest {
+
+ protected function setUp() {
+ $this->setMwGlobals( 'wgContentHandlerUseDB', false );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $page_table = $dbw->tableName( 'page' );
+ $revision_table = $dbw->tableName( 'revision' );
+ $archive_table = $dbw->tableName( 'archive' );
+
+ if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) {
+ $dbw->query( "alter table $page_table drop column page_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_format" );
+ $dbw->query( "alter table $archive_table drop column ar_content_model" );
+ $dbw->query( "alter table $archive_table drop column ar_content_format" );
+ }
+
+ parent::setUp();
+ }
+
+ /**
+ * @covers Revision::selectFields
+ */
+ public function testSelectFields() {
+ $fields = Revision::selectFields();
+
+ $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' );
+ $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' );
+ $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields' );
+ $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' );
+
+ $this->assertFalse( in_array( 'rev_content_model', $fields ), 'missing rev_content_model in list of fields' );
+ $this->assertFalse( in_array( 'rev_content_format', $fields ), 'missing rev_content_format in list of fields' );
+ }
+
+ /**
+ * @covers Revision::getContentModel
+ */
+ public function testGetContentModel() {
+ try {
+ $this->makeRevision( array( 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+
+ $this->fail( "Creating JavaScript content on a wikitext page should fail with "
+ . "\$wgContentHandlerUseDB disabled" );
+ } catch ( MWException $ex ) {
+ $this->assertTrue( true ); // ok
+ }
+ }
+
+
+ /**
+ * @covers Revision::getContentFormat
+ */
+ public function testGetContentFormat() {
+ try {
+ // @todo change this to test failure on using a non-standard (but supported) format
+ // for a content model supported in the given location. As of 1.21, there are
+ // no alternative formats for any of the standard content models that could be
+ // used for this though.
+
+ $this->makeRevision( array( 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT,
+ 'content_format' => 'text/javascript' ) );
+
+ $this->fail( "Creating JavaScript content on a wikitext page should fail with "
+ . "\$wgContentHandlerUseDB disabled" );
+ } catch ( MWException $ex ) {
+ $this->assertTrue( true ); // ok
+ }
+ }
+}
diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php
new file mode 100644
index 00000000..b5819ff6
--- /dev/null
+++ b/tests/phpunit/includes/RevisionTest.php
@@ -0,0 +1,481 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class RevisionTest extends MediaWikiTestCase {
+ protected function setUp() {
+ global $wgContLang;
+
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'en' ),
+ 'wgLanguageCode' => 'en',
+ 'wgLegacyEncoding' => false,
+ 'wgCompressRevisions' => false,
+
+ 'wgContentHandlerTextFallback' => 'ignore',
+ ) );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgExtraNamespaces',
+ array(
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ )
+ );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgNamespaceContentModels',
+ array(
+ 12312 => 'testing',
+ )
+ );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ array(
+ 'testing' => 'DummyContentHandlerForTesting',
+ 'RevisionTestModifyableContent' => 'RevisionTestModifyableContentHandler',
+ )
+ );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ function tearDown() {
+ global $wgContLang;
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+
+ parent::tearDown();
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public 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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public function testGetRevisionTextGzip() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public 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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public 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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public function testGetRevisionTextUtf8NativeGzip() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public function testGetRevisionTextUtf8LegacyGzip() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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 ) );
+ }
+
+ /**
+ * @covers Revision::compressRevisionText
+ */
+ public 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" );
+ }
+
+ /**
+ * @covers Revision::compressRevisionText
+ */
+ public function testCompressRevisionTextUtf8Gzip() {
+ $this->checkPHPExtension( 'zlib' );
+ $this->setMwGlobals( '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" );
+ }
+
+ # =================================================================================================================
+
+ /**
+ * @param string $text
+ * @param string $title
+ * @param string $model
+ * @param null $format
+ *
+ * @return Revision
+ */
+ function newTestRevision( $text, $title = "Test", $model = CONTENT_MODEL_WIKITEXT, $format = null ) {
+ if ( is_string( $title ) ) {
+ $title = Title::newFromText( $title );
+ }
+
+ $content = ContentHandler::makeContent( $text, $title, $model, $format );
+
+ $rev = new Revision(
+ array(
+ 'id' => 42,
+ 'page' => 23,
+ 'title' => $title,
+
+ 'content' => $content,
+ 'length' => $content->getSize(),
+ 'comment' => "testing",
+ 'minor_edit' => false,
+
+ 'content_format' => $format,
+ )
+ );
+
+ return $rev;
+ }
+
+ function dataGetContentModel() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ),
+ array( 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContentModel
+ * @covers Revision::getContentModel
+ */
+ public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedModel, $rev->getContentModel() );
+ }
+
+ function dataGetContentFormat() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ),
+ array( 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ),
+ array( 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContentFormat
+ * @covers Revision::getContentFormat
+ */
+ public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
+ }
+
+ function dataGetContentHandler() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ),
+ array( 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContentHandler
+ * @covers Revision::getContentHandler
+ */
+ public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
+ }
+
+ function dataGetContent() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+ array( serialize( 'hello world' ), 'Hello', "testing", null, Revision::FOR_PUBLIC, serialize( 'hello world' ) ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, serialize( 'hello world' ) ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContent
+ * @covers Revision::getContent
+ */
+ public function testGetContent( $text, $title, $model, $format, $audience, $expectedSerialization ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+ $content = $rev->getContent( $audience );
+
+ $this->assertEquals( $expectedSerialization, is_null( $content ) ? null : $content->serialize( $format ) );
+ }
+
+ function dataGetText() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+ array( serialize( 'hello world' ), 'Hello', "testing", null, Revision::FOR_PUBLIC, null ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetText
+ * @covers Revision::getText
+ */
+ public function testGetText( $text, $title, $model, $format, $audience, $expectedText ) {
+ $this->hideDeprecated( 'Revision::getText' );
+
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedText, $rev->getText( $audience ) );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetText
+ * @covers Revision::getRawText
+ */
+ public function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) {
+ $this->hideDeprecated( 'Revision::getRawText' );
+
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedText, $rev->getRawText( $audience ) );
+ }
+
+
+ public function dataGetSize() {
+ return array(
+ array( "hello world.", CONTENT_MODEL_WIKITEXT, 12 ),
+ array( serialize( "hello world." ), "testing", 12 ),
+ );
+ }
+
+ /**
+ * @covers Revision::getSize
+ * @group Database
+ * @dataProvider dataGetSize
+ */
+ public function testGetSize( $text, $model, $expected_size ) {
+ $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
+ $this->assertEquals( $expected_size, $rev->getSize() );
+ }
+
+ public function dataGetSha1() {
+ return array(
+ array( "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ),
+ array( serialize( "hello world." ), "testing", Revision::base36Sha1( serialize( "hello world." ) ) ),
+ );
+ }
+
+ /**
+ * @covers Revision::getSha1
+ * @group Database
+ * @dataProvider dataGetSha1
+ */
+ public function testGetSha1( $text, $model, $expected_hash ) {
+ $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
+ $this->assertEquals( $expected_hash, $rev->getSha1() );
+ }
+
+ /**
+ * @covers Revision::__construct
+ */
+ public function testConstructWithText() {
+ $this->hideDeprecated( "Revision::getText" );
+
+ $rev = new Revision( array(
+ 'text' => 'hello world.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT
+ ) );
+
+ $this->assertNotNull( $rev->getText(), 'no content text' );
+ $this->assertNotNull( $rev->getContent(), 'no content object available' );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+ }
+
+ /**
+ * @covers Revision::__construct
+ */
+ public function testConstructWithContent() {
+ $this->hideDeprecated( "Revision::getText" );
+
+ $title = Title::newFromText( 'RevisionTest_testConstructWithContent' );
+
+ $rev = new Revision( array(
+ 'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ),
+ ) );
+
+ $this->assertNotNull( $rev->getText(), 'no content text' );
+ $this->assertNotNull( $rev->getContent(), 'no content object available' );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+ }
+
+ /**
+ * Tests whether $rev->getContent() returns a clone when needed.
+ *
+ * @group Database
+ * @covers Revision::getContent
+ */
+ public function testGetContentClone() {
+ $content = new RevisionTestModifyableContent( "foo" );
+
+ $rev = new Revision(
+ array(
+ 'id' => 42,
+ 'page' => 23,
+ 'title' => Title::newFromText( "testGetContentClone_dummy" ),
+
+ 'content' => $content,
+ 'length' => $content->getSize(),
+ 'comment' => "testing",
+ 'minor_edit' => false,
+ )
+ );
+
+ $content = $rev->getContent( Revision::RAW );
+ $content->setText( "bar" );
+
+ $content2 = $rev->getContent( Revision::RAW );
+ $this->assertNotSame( $content, $content2, "expected a clone" ); // content is mutable, expect clone
+ $this->assertEquals( "foo", $content2->getText() ); // clone should contain the original text
+
+ $content2->setText( "bla bla" );
+ $this->assertEquals( "bar", $content->getText() ); // clones should be independent
+ }
+
+
+ /**
+ * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
+ *
+ * @group Database
+ * @covers Revision::getContent
+ */
+ public function testGetContentUncloned() {
+ $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
+ $content = $rev->getContent( Revision::RAW );
+ $content2 = $rev->getContent( Revision::RAW );
+
+ // for immutable content like wikitext, this should be the same object
+ $this->assertSame( $content, $content2 );
+ }
+}
+
+class RevisionTestModifyableContent extends TextContent {
+ public function __construct( $text ) {
+ parent::__construct( $text, "RevisionTestModifyableContent" );
+ }
+
+ public function copy() {
+ return new RevisionTestModifyableContent( $this->mText );
+ }
+
+ public function getText() {
+ return $this->mText;
+ }
+
+ public function setText( $text ) {
+ $this->mText = $text;
+ }
+}
+
+class RevisionTestModifyableContentHandler extends TextContentHandler {
+
+ public function __construct() {
+ parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new RevisionTestModifyableContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new RevisionTestModifyableContent( '' );
+ }
+}
diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php
new file mode 100644
index 00000000..8516a4ce
--- /dev/null
+++ b/tests/phpunit/includes/SampleTest.php
@@ -0,0 +1,105 @@
+<?php
+
+class TestSample extends MediaWikiLangTestCase {
+
+ /**
+ * Anything that needs to happen before your tests should go here.
+ */
+ protected function setUp() {
+ // Be sure to do call the parent setup and teardown functions.
+ // This makes sure that all the various cleanup and restorations
+ // happen as they should (including the restoration for setMwGlobals).
+ parent::setUp();
+
+ // This sets the globals and will restore them automatically
+ // after each test.
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'en' ),
+ 'wgLanguageCode' => 'en',
+ ) );
+ }
+
+ /**
+ * Anything cleanup you need to do should go here.
+ */
+ protected function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Name tests so that PHPUnit can turn them into sentences 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
+ */
+ public function testTitleObjectStringConversion() {
+ $title = Title::newFromText( "text" );
+ $this->assertInstanceOf( 'Title', $title, "Title creation" );
+ $this->assertEquals( "Text", $title, "Automatic string conversion" );
+
+ $title = Title::newFromText( "text", NS_MEDIA );
+ $this->assertEquals( "Media:Text", $title, "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
+ *
+ * Note: Data providers are always called statically and outside setUp/tearDown!
+ */
+ public static 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_FILE, '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
+ */
+ public function testTitleObjectFromObject() {
+ $title = Title::newFromText( Title::newFromText( "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 00000000..81246d33
--- /dev/null
+++ b/tests/phpunit/includes/SanitizerTest.php
@@ -0,0 +1,302 @@
+<?php
+
+/**
+ * @todo Tests covering decodeCharReferences can be refactored into a single
+ * method and dataprovider.
+ */
+class SanitizerTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ AutoLoader::loadClass( 'Sanitizer' );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testDecodeNamedEntities() {
+ $this->assertEquals(
+ "\xc3\xa9cole",
+ Sanitizer::decodeCharReferences( '&eacute;cole' ),
+ 'decode named entities'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testDecodeNumericEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&#233;cole!" ),
+ 'decode numeric entities'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testDecodeMixedEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&eacute;cole!" ),
+ 'decode mixed numeric/named entities'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testDecodeMixedComplexEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas &#x108;io dans l'&eacute;cole)",
+ Sanitizer::decodeCharReferences(
+ "&#x108;io bonas dans l'&eacute;cole! (mais pas &amp;#x108;io dans l'&#38;eacute;cole)"
+ ),
+ 'decode mixed complex entities'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testInvalidAmpersand() {
+ $this->assertEquals(
+ 'a & b',
+ Sanitizer::decodeCharReferences( 'a & b' ),
+ 'Invalid ampersand'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testInvalidEntities() {
+ $this->assertEquals(
+ '&foo;',
+ Sanitizer::decodeCharReferences( '&foo;' ),
+ 'Invalid named entity'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testInvalidNumberedEntities() {
+ $this->assertEquals( UTF8_REPLACEMENT, Sanitizer::decodeCharReferences( "&#88888888888888;" ), 'Invalid numbered entity' );
+ }
+
+ /**
+ * @covers Sanitizer::removeHTMLtags
+ * @dataProvider provideHtml5Tags
+ *
+ * @param String $tag Name of an HTML5 element (ie: 'video')
+ * @param Boolean $escaped Wheter sanitizer let the tag in or escape it (ie: '&lt;video&gt;')
+ */
+ public function testRemovehtmltagsOnHtml5Tags( $tag, $escaped ) {
+ $this->setMwGlobals( array(
+ 'wgUseTidy' => false
+ ) );
+
+ if ( $escaped ) {
+ $this->assertEquals( "&lt;$tag&gt;",
+ Sanitizer::removeHTMLtags( "<$tag>" )
+ );
+ } else {
+ $this->assertEquals( "<$tag></$tag>\n",
+ Sanitizer::removeHTMLtags( "<$tag>" )
+ );
+ }
+ }
+
+ /**
+ * Provide HTML5 tags
+ */
+ public static function provideHtml5Tags() {
+ $ESCAPED = true; # We want tag to be escaped
+ $VERBATIM = false; # We want to keep the tag
+ return array(
+ array( 'data', $VERBATIM ),
+ array( 'mark', $VERBATIM ),
+ array( 'time', $VERBATIM ),
+ array( 'video', $ESCAPED ),
+ );
+ }
+
+ function dataRemoveHTMLtags() {
+ return array(
+ // former testSelfClosingTag
+ array(
+ '<div>Hello world</div />',
+ '<div>Hello world</div>',
+ 'Self-closing closing div'
+ ),
+ // Make sure special nested HTML5 semantics are not broken
+ // http://www.whatwg.org/html/text-level-semantics.html#the-kbd-element
+ array(
+ '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
+ '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
+ 'Nested <kbd>.'
+ ),
+ // http://www.whatwg.org/html/text-level-semantics.html#the-sub-and-sup-elements
+ array(
+ '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
+ '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
+ 'Nested <var>.'
+ ),
+ // http://www.whatwg.org/html/text-level-semantics.html#the-dfn-element
+ array(
+ '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
+ '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
+ '<abbr> inside <dfn>',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataRemoveHTMLtags
+ * @covers Sanitizer::removeHTMLtags
+ */
+ public function testRemoveHTMLtags( $input, $output, $msg = null ) {
+ $GLOBALS['wgUseTidy'] = false;
+ $this->assertEquals( $output, Sanitizer::removeHTMLtags( $input ), $msg );
+ }
+
+ /**
+ * @dataProvider provideTagAttributesToDecode
+ * @covers Sanitizer::decodeTagAttributes
+ */
+ public function testDecodeTagAttributes( $expected, $attributes, $message = '' ) {
+ $this->assertEquals( $expected,
+ Sanitizer::decodeTagAttributes( $attributes ),
+ $message
+ );
+ }
+
+ public static function provideTagAttributesToDecode() {
+ return array(
+ array( array( 'foo' => 'bar' ), 'foo=bar', 'Unquoted attribute' ),
+ array( array( 'foo' => 'bar' ), ' foo = bar ', 'Spaced attribute' ),
+ array( array( 'foo' => 'bar' ), 'foo="bar"', 'Double-quoted attribute' ),
+ array( array( 'foo' => 'bar' ), 'foo=\'bar\'', 'Single-quoted attribute' ),
+ array( array( 'foo' => 'bar', 'baz' => 'foo' ), 'foo=\'bar\' baz="foo"', 'Several attributes' ),
+ array( array( 'foo' => 'bar', 'baz' => 'foo' ), 'foo=\'bar\' baz="foo"', 'Several attributes' ),
+ array( array( 'foo' => 'bar', 'baz' => 'foo' ), 'foo=\'bar\' baz="foo"', 'Several attributes' ),
+ array( array( ':foo' => 'bar' ), ':foo=\'bar\'', 'Leading :' ),
+ array( array( '_foo' => 'bar' ), '_foo=\'bar\'', 'Leading _' ),
+ array( array( 'foo' => 'bar' ), 'Foo=\'bar\'', 'Leading capital' ),
+ array( array( 'foo' => 'BAR' ), 'FOO=BAR', 'Attribute keys are normalized to lowercase' ),
+
+ # Invalid beginning
+ array( array(), '-foo=bar', 'Leading - is forbidden' ),
+ array( array(), '.foo=bar', 'Leading . is forbidden' ),
+ array( array( 'foo-bar' => 'bar' ), 'foo-bar=bar', 'A - is allowed inside the attribute' ),
+ array( array( 'foo-' => 'bar' ), 'foo-=bar', 'A - is allowed inside the attribute' ),
+ array( array( 'foo.bar' => 'baz' ), 'foo.bar=baz', 'A . is allowed inside the attribute' ),
+ array( array( 'foo.' => 'baz' ), 'foo.=baz', 'A . is allowed as last character' ),
+ array( array( 'foo6' => 'baz' ), 'foo6=baz', 'Numbers are allowed' ),
+
+ # This bit is more relaxed than XML rules, but some extensions use
+ # it, like ProofreadPage (see bug 27539)
+ array( array( '1foo' => 'baz' ), '1foo=baz', 'Leading numbers are allowed' ),
+ array( array(), 'foo$=baz', 'Symbols are not allowed' ),
+ array( array(), 'foo@=baz', 'Symbols are not allowed' ),
+ array( array(), 'foo~=baz', 'Symbols are not allowed' ),
+ array( array( 'foo' => '1[#^`*%w/(' ), 'foo=1[#^`*%w/(', 'All kind of characters are allowed as values' ),
+ array( array( 'foo' => '1[#^`*%\'w/(' ), 'foo="1[#^`*%\'w/("', 'Double quotes are allowed if quoted by single quotes' ),
+ array( array( 'foo' => '1[#^`*%"w/(' ), 'foo=\'1[#^`*%"w/(\'', 'Single quotes are allowed if quoted by double quotes' ),
+ array( array( 'foo' => '&"' ), 'foo=&amp;&quot;', 'Special chars can be provided as entities' ),
+ array( array( 'foo' => '&foobar;' ), 'foo=&foobar;', 'Entity-like items are accepted' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDeprecatedAttributes
+ * @covers Sanitizer::fixTagAttributes
+ */
+ public function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl, $message = '' ) {
+ $this->assertEquals( " $inputAttr",
+ Sanitizer::fixTagAttributes( $inputAttr, $inputEl ),
+ $message
+ );
+ }
+
+ public static function provideDeprecatedAttributes() {
+ /** array( <attribute>, <element>, [message] ) */
+ return array(
+ array( 'clear="left"', 'br' ),
+ array( 'clear="all"', 'br' ),
+ array( 'width="100"', 'td' ),
+ array( 'nowrap="true"', 'td' ),
+ array( 'nowrap=""', 'td' ),
+ array( 'align="right"', 'td' ),
+ array( 'align="center"', 'table' ),
+ array( 'align="left"', 'tr' ),
+ array( 'align="center"', 'div' ),
+ array( 'align="left"', 'h1' ),
+ array( 'align="left"', 'span' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCssCommentsFixtures
+ * @covers Sanitizer::checkCss
+ */
+ public function testCssCommentsChecking( $expected, $css, $message = '' ) {
+ $this->assertEquals( $expected,
+ Sanitizer::checkCss( $css ),
+ $message
+ );
+ }
+
+ public static function provideCssCommentsFixtures() {
+ /** array( <expected>, <css>, [message] ) */
+ return array(
+ // Valid comments spanning entire input
+ array( '/**/', '/**/' ),
+ array( '/* comment */', '/* comment */' ),
+ // Weird stuff
+ array( ' ', '/****/' ),
+ array( ' ', '/* /* */' ),
+ array( 'display: block;', "display:/* foo */block;" ),
+ array( 'display: block;', "display:\\2f\\2a foo \\2a\\2f block;",
+ 'Backslash-escaped comments must be stripped (bug 28450)' ),
+ array( '', '/* unfinished comment structure',
+ 'Remove anything after a comment-start token' ),
+ array( '', "\\2f\\2a unifinished comment'",
+ 'Remove anything after a backslash-escaped comment-start token' ),
+ array( '/* insecure input */', 'filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'asdf.png\',sizingMethod=\'scale\');' ),
+ array( '/* insecure input */', '-ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'asdf.png\',sizingMethod=\'scale\')";' ),
+ array( '/* insecure input */', 'width: expression(1+1);' ),
+ array( '/* insecure input */', 'background-image: image(asdf.png);' ),
+ array( '/* insecure input */', 'background-image: -webkit-image(asdf.png);' ),
+ array( '/* insecure input */', 'background-image: -moz-image(asdf.png);' ),
+ array( '/* insecure input */', 'background-image: image-set("asdf.png" 1x, "asdf.png" 2x);' ),
+ array( '/* insecure input */', 'background-image: -webkit-image-set("asdf.png" 1x, "asdf.png" 2x);' ),
+ array( '/* insecure input */', 'background-image: -moz-image-set("asdf.png" 1x, "asdf.png" 2x);' ),
+ );
+ }
+
+ /**
+ * Test for support or lack of support for specific attributes in the attribute whitelist.
+ */
+ public static function provideAttributeSupport() {
+ /** array( <attributes>, <expected>, <message> ) */
+ return array(
+ array( 'div', ' role="presentation"', ' role="presentation"', 'Support for WAI-ARIA\'s role="presentation".' ),
+ array( 'div', ' role="main"', '', "Other WAI-ARIA roles are currently not supported." ),
+ );
+ }
+
+ /**
+ * @dataProvider provideAttributeSupport
+ * @covers Sanitizer::fixTagAttributes
+ */
+ public function testAttributeSupport( $tag, $attributes, $expected, $message ) {
+ $this->assertEquals( $expected,
+ Sanitizer::fixTagAttributes( $attributes, $tag ),
+ $message
+ );
+ }
+}
diff --git a/tests/phpunit/includes/SanitizerValidateEmailTest.php b/tests/phpunit/includes/SanitizerValidateEmailTest.php
new file mode 100644
index 00000000..f13e8382
--- /dev/null
+++ b/tests/phpunit/includes/SanitizerValidateEmailTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @covers Sanitizer::validateEmail
+ * @TODO all test methods in this class should be refactored and...
+ * use a single test method and a single data provider...
+ */
+class SanitizerValidateEmailTest extends MediaWikiTestCase {
+
+ private function checkEmail( $addr, $expected = true, $msg = '' ) {
+ if ( $msg == '' ) {
+ $msg = "Testing $addr";
+ }
+
+ $this->assertEquals(
+ $expected,
+ Sanitizer::validateEmail( $addr ),
+ $msg
+ );
+ }
+
+ private function valid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, true, $msg );
+ }
+
+ private function invalid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, false, $msg );
+ }
+
+ public function testEmailWellKnownUserAtHostDotTldAreValid() {
+ $this->valid( 'user@example.com' );
+ $this->valid( 'user@example.museum' );
+ }
+
+ public function testEmailWithUpperCaseCharactersAreValid() {
+ $this->valid( 'USER@example.com' );
+ $this->valid( 'user@EXAMPLE.COM' );
+ $this->valid( 'user@Example.com' );
+ $this->valid( 'USER@eXAMPLE.com' );
+ }
+
+ public function testEmailWithAPlusInUserName() {
+ $this->valid( 'user+sub@example.com' );
+ $this->valid( 'user+@example.com' );
+ }
+
+ public function testEmailDoesNotNeedATopLevelDomain() {
+ $this->valid( "user@localhost" );
+ $this->valid( "FooBar@localdomain" );
+ $this->valid( "nobody@mycompany" );
+ }
+
+ public function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() {
+ $this->invalid( " user@host.com" );
+ $this->invalid( "user@host.com " );
+ $this->invalid( "\tuser@host.com" );
+ $this->invalid( "user@host.com\t" );
+ }
+
+ public function testEmailWithWhiteSpacesAreInvalids() {
+ $this->invalid( "User user@host" );
+ $this->invalid( "first last@mycompany" );
+ $this->invalid( "firstlast@my company" );
+ }
+
+ /**
+ * bug 26948 : comma were matched by an incorrect regexp range
+ */
+ public function testEmailWithCommasAreInvalids() {
+ $this->invalid( "user,foo@example.org" );
+ $this->invalid( "userfoo@ex,ample.org" );
+ }
+
+ public function testEmailWithHyphens() {
+ $this->valid( "user-foo@example.org" );
+ $this->valid( "userfoo@ex-ample.org" );
+ }
+
+ public function testEmailDomainCanNotBeginWithDot() {
+ $this->invalid( "user@." );
+ $this->invalid( "user@.localdomain" );
+ $this->invalid( "user@localdomain." );
+ $this->valid( "user.@localdomain" );
+ $this->valid( ".@localdomain" );
+ $this->invalid( ".@a............" );
+ }
+
+ public function testEmailWithFunnyCharacters() {
+ $this->valid( "\$user!ex{this}@123.com" );
+ }
+
+ public function testEmailTopLevelDomainCanBeNumerical() {
+ $this->valid( "user@example.1234" );
+ }
+
+ public function testEmailWithoutAtSignIsInvalid() {
+ $this->invalid( 'useràexample.com' );
+ }
+
+ public function testEmailWithOneCharacterDomainIsValid() {
+ $this->valid( 'user@a' );
+ }
+}
diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php
new file mode 100644
index 00000000..053d8a7d
--- /dev/null
+++ b/tests/phpunit/includes/SiteConfigurationTest.php
@@ -0,0 +1,335 @@
+<?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 MediaWikiTestCase {
+
+ /**
+ * @var SiteConfiguration
+ */
+ protected $mConf;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mConf = new SiteConfiguration;
+
+ $this->mConf->suffixes = array( 'wikipedia' => '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' );
+ }
+
+ /**
+ * @covers SiteConfiguration::siteFromDB
+ */
+ public 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)'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::getLocalDatabases
+ */
+ public function testGetLocalDatabases() {
+ $this->assertEquals(
+ array( 'enwiki', 'dewiki', 'frwiki' ),
+ $this->mConf->getLocalDatabases(),
+ 'getLocalDatabases()'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::get
+ */
+ public function testGetConfVariables() {
+ $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)'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::siteFromDB
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::get
+ */
+ public function testParameterReplacement() {
+ $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'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::getAll
+ */
+ public function testGetAllGlobals() {
+ $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/StringUtilsTest.php b/tests/phpunit/includes/StringUtilsTest.php
new file mode 100644
index 00000000..89759e5c
--- /dev/null
+++ b/tests/phpunit/includes/StringUtilsTest.php
@@ -0,0 +1,147 @@
+<?php
+
+class StringUtilsTest extends MediaWikiTestCase {
+
+ /**
+ * This tests StringUtils::isUtf8 whenever we have the mbstring extension
+ * loaded.
+ *
+ * @covers StringUtils::isUtf8
+ * @dataProvider provideStringsForIsUtf8Check
+ */
+ public function testIsUtf8WithMbstring( $expected, $string ) {
+ if ( !function_exists( 'mb_check_encoding' ) ) {
+ $this->markTestSkipped( 'Test requires the mbstring PHP extension' );
+ }
+ $this->assertEquals( $expected,
+ StringUtils::isUtf8( $string ),
+ 'Testing string "' . $this->escaped( $string ) . '" with mb_check_encoding'
+ );
+ }
+
+ /**
+ * This tests StringUtils::isUtf8 making sure we use the pure PHP
+ * implementation used as a fallback when mb_check_encoding() is
+ * not available.
+ *
+ * @covers StringUtils::isUtf8
+ * @dataProvider provideStringsForIsUtf8Check
+ */
+ public function testIsUtf8WithPhpFallbackImplementation( $expected, $string ) {
+ $this->assertEquals( $expected,
+ StringUtils::isUtf8( $string, /** disable mbstring: */true ),
+ 'Testing string "' . $this->escaped( $string ) . '" with pure PHP implementation'
+ );
+ }
+
+ /**
+ * Print high range characters as an hexadecimal
+ */
+ function escaped( $string ) {
+ $escaped = '';
+ $length = strlen( $string );
+ for ( $i = 0; $i < $length; $i++ ) {
+ $char = $string[$i];
+ $val = ord( $char );
+ if ( $val > 127 ) {
+ $escaped .= '\x' . dechex( $val );
+ } else {
+ $escaped .= $char;
+ }
+ }
+
+ return $escaped;
+ }
+
+ /**
+ * See also "UTF-8 decoder capability and stress test" by
+ * Markus Kuhn:
+ * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ */
+ public static function provideStringsForIsUtf8Check() {
+ // Expected return values for StringUtils::isUtf8()
+ $PASS = true;
+ $FAIL = false;
+
+ return array(
+ 'some ASCII' => array( $PASS, 'Some ASCII' ),
+ 'euro sign' => array( $PASS, "Euro sign €" ),
+
+ 'first possible sequence 1 byte' => array( $PASS, "\x00" ),
+ 'first possible sequence 2 bytes' => array( $PASS, "\xc2\x80" ),
+ 'first possible sequence 3 bytes' => array( $PASS, "\xe0\xa0\x80" ),
+ 'first possible sequence 4 bytes' => array( $PASS, "\xf0\x90\x80\x80" ),
+ 'first possible sequence 5 bytes' => array( $FAIL, "\xf8\x88\x80\x80\x80" ),
+ 'first possible sequence 6 bytes' => array( $FAIL, "\xfc\x84\x80\x80\x80\x80" ),
+
+ 'last possible sequence 1 byte' => array( $PASS, "\x7f" ),
+ 'last possible sequence 2 bytes' => array( $PASS, "\xdf\xbf" ),
+ 'last possible sequence 3 bytes' => array( $PASS, "\xef\xbf\xbf" ),
+ 'last possible sequence 4 bytes (U+1FFFFF)' => array( $FAIL, "\xf7\xbf\xbf\xbf" ),
+ 'last possible sequence 5 bytes' => array( $FAIL, "\xfb\xbf\xbf\xbf\xbf" ),
+ 'last possible sequence 6 bytes' => array( $FAIL, "\xfd\xbf\xbf\xbf\xbf\xbf" ),
+
+ 'boundary 1' => array( $PASS, "\xed\x9f\xbf" ),
+ 'boundary 2' => array( $PASS, "\xee\x80\x80" ),
+ 'boundary 3' => array( $PASS, "\xef\xbf\xbd" ),
+ 'boundary 4' => array( $PASS, "\xf2\x80\x80\x80" ),
+ 'boundary 5 (U+FFFFF)' => array( $PASS, "\xf3\xbf\xbf\xbf" ),
+ 'boundary 6 (U+100000)' => array( $PASS, "\xf4\x80\x80\x80" ),
+ 'boundary 7 (U+10FFFF)' => array( $PASS, "\xf4\x8f\xbf\xbf" ),
+ 'boundary 8 (U+110000)' => array( $FAIL, "\xf4\x90\x80\x80" ),
+
+ 'malformed 1' => array( $FAIL, "\x80" ),
+ 'malformed 2' => array( $FAIL, "\xbf" ),
+ 'malformed 3' => array( $FAIL, "\x80\xbf" ),
+ 'malformed 4' => array( $FAIL, "\x80\xbf\x80" ),
+ 'malformed 5' => array( $FAIL, "\x80\xbf\x80\xbf" ),
+ 'malformed 6' => array( $FAIL, "\x80\xbf\x80\xbf\x80" ),
+ 'malformed 7' => array( $FAIL, "\x80\xbf\x80\xbf\x80\xbf" ),
+ 'malformed 8' => array( $FAIL, "\x80\xbf\x80\xbf\x80\xbf\x80" ),
+
+ 'last byte missing 1' => array( $FAIL, "\xc0" ),
+ 'last byte missing 2' => array( $FAIL, "\xe0\x80" ),
+ 'last byte missing 3' => array( $FAIL, "\xf0\x80\x80" ),
+ 'last byte missing 4' => array( $FAIL, "\xf8\x80\x80\x80" ),
+ 'last byte missing 5' => array( $FAIL, "\xfc\x80\x80\x80\x80" ),
+ 'last byte missing 6' => array( $FAIL, "\xdf" ),
+ 'last byte missing 7' => array( $FAIL, "\xef\xbf" ),
+ 'last byte missing 8' => array( $FAIL, "\xf7\xbf\xbf" ),
+ 'last byte missing 9' => array( $FAIL, "\xfb\xbf\xbf\xbf" ),
+ 'last byte missing 10' => array( $FAIL, "\xfd\xbf\xbf\xbf\xbf" ),
+
+ 'extra continuation byte 1' => array( $FAIL, "e\xaf" ),
+ 'extra continuation byte 2' => array( $FAIL, "\xc3\x89\xaf" ),
+ 'extra continuation byte 3' => array( $FAIL, "\xef\xbc\xa5\xaf" ),
+ 'extra continuation byte 4' => array( $FAIL, "\xf0\x9d\x99\xb4\xaf" ),
+
+ 'impossible bytes 1' => array( $FAIL, "\xfe" ),
+ 'impossible bytes 2' => array( $FAIL, "\xff" ),
+ 'impossible bytes 3' => array( $FAIL, "\xfe\xfe\xff\xff" ),
+
+ 'overlong sequences 1' => array( $FAIL, "\xc0\xaf" ),
+ 'overlong sequences 2' => array( $FAIL, "\xc1\xaf" ),
+ 'overlong sequences 3' => array( $FAIL, "\xe0\x80\xaf" ),
+ 'overlong sequences 4' => array( $FAIL, "\xf0\x80\x80\xaf" ),
+ 'overlong sequences 5' => array( $FAIL, "\xf8\x80\x80\x80\xaf" ),
+ 'overlong sequences 6' => array( $FAIL, "\xfc\x80\x80\x80\x80\xaf" ),
+
+ 'maximum overlong sequences 1' => array( $FAIL, "\xc1\xbf" ),
+ 'maximum overlong sequences 2' => array( $FAIL, "\xe0\x9f\xbf" ),
+ 'maximum overlong sequences 3' => array( $FAIL, "\xf0\x8f\xbf\xbf" ),
+ 'maximum overlong sequences 4' => array( $FAIL, "\xf8\x87\xbf\xbf" ),
+ 'maximum overlong sequences 5' => array( $FAIL, "\xfc\x83\xbf\xbf\xbf\xbf" ),
+
+ 'surrogates 1 (U+D799)' => array( $PASS, "\xed\x9f\xbf" ),
+ 'surrogates 2 (U+E000)' => array( $PASS, "\xee\x80\x80" ),
+ 'surrogates 3 (U+D800)' => array( $FAIL, "\xed\xa0\x80" ),
+ 'surrogates 4 (U+DBFF)' => array( $FAIL, "\xed\xaf\xbf" ),
+ 'surrogates 5 (U+DC00)' => array( $FAIL, "\xed\xb0\x80" ),
+ 'surrogates 6 (U+DFFF)' => array( $FAIL, "\xed\xbf\xbf" ),
+ 'surrogates 7 (U+D800 U+DC00)' => array( $FAIL, "\xed\xa0\x80\xed\xb0\x80" ),
+
+ 'noncharacters 1' => array( $PASS, "\xef\xbf\xbe" ),
+ 'noncharacters 2' => array( $PASS, "\xef\xbf\xbf" ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/TemplateCategoriesTest.php b/tests/phpunit/includes/TemplateCategoriesTest.php
new file mode 100644
index 00000000..fb63a564
--- /dev/null
+++ b/tests/phpunit/includes/TemplateCategoriesTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @group Database
+ */
+require __DIR__ . "/../../../maintenance/runJobs.php";
+
+class TemplateCategoriesTest extends MediaWikiLangTestCase {
+
+ /**
+ * @covers Title::getParentCategories
+ */
+ public function testTemplateCategories() {
+ $title = Title::newFromText( "Categorized from template" );
+ $page = WikiPage::factory( $title );
+ $user = new User();
+ $user->mRights = array( 'createpage', 'edit', 'purge' );
+
+ $page->doEditContent(
+ new WikitextContent( '{{Categorising template}}' ),
+ 'Create a page with a template',
+ 0,
+ false,
+ $user
+ );
+
+ $this->assertEquals(
+ array()
+ , $title->getParentCategories()
+ );
+
+ $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) );
+
+ $template->doEditContent(
+ new WikitextContent( '[[Category:Solved bugs]]' ),
+ 'Add a category through a template',
+ 0,
+ false,
+ $user
+ );
+
+ // Run the job queue
+ JobQueueGroup::destroySingletons();
+ $jobs = new RunJobs;
+ $jobs->loadParamsAndArgs( null, array( 'quiet' => true ), null );
+ $jobs->execute();
+
+ $this->assertEquals(
+ array( 'Category:Solved_bugs' => $title->getPrefixedText() )
+ , $title->getParentCategories()
+ );
+ }
+}
diff --git a/tests/phpunit/includes/TestUser.php b/tests/phpunit/includes/TestUser.php
new file mode 100644
index 00000000..23e65031
--- /dev/null
+++ b/tests/phpunit/includes/TestUser.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Wraps the user object, so we can also retain full access to properties like password if we log in via the API
+ */
+class TestUser {
+ public $username;
+ public $password;
+ public $email;
+ public $groups;
+ public $user;
+
+ public function __construct( $username, $realname = 'Real Name', $email = 'sample@example.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();
+ }
+}
diff --git a/tests/phpunit/includes/TimeAdjustTest.php b/tests/phpunit/includes/TimeAdjustTest.php
new file mode 100644
index 00000000..0b368c25
--- /dev/null
+++ b/tests/phpunit/includes/TimeAdjustTest.php
@@ -0,0 +1,41 @@
+<?php
+
+class TimeAdjustTest extends MediaWikiLangTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->iniSet( 'precision', 15 );
+ }
+
+ /**
+ * Test offset usage for a given Language::userAdjust
+ * @dataProvider dataUserAdjust
+ * @covers Language::userAdjust
+ */
+ public function testUserAdjust( $date, $localTZoffset, $expected ) {
+ global $wgContLang;
+
+ $this->setMwGlobals( 'wgLocalTZoffset', $localTZoffset );
+
+ $this->assertEquals(
+ strval( $expected ),
+ strval( $wgContLang->userAdjust( $date, '' ) ),
+ "User adjust {$date} by {$localTZoffset} minutes should give {$expected}"
+ );
+ }
+
+ public static function dataUserAdjust() {
+ return 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 ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/TimestampTest.php b/tests/phpunit/includes/TimestampTest.php
new file mode 100644
index 00000000..53388392
--- /dev/null
+++ b/tests/phpunit/includes/TimestampTest.php
@@ -0,0 +1,304 @@
+<?php
+
+/**
+ * Tests timestamp parsing and output.
+ */
+class TimestampTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ RequestContext::getMain()->setLanguage( Language::factory( 'en' ) );
+ }
+
+ /**
+ * Test parsing of valid timestamps and outputing to MW format.
+ * @dataProvider provideValidTimestamps
+ * @covers MWTimestamp::getTimestamp
+ */
+ public function testValidParse( $format, $original, $expected ) {
+ $timestamp = new MWTimestamp( $original );
+ $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) );
+ }
+
+ /**
+ * Test outputting valid timestamps to different formats.
+ * @dataProvider provideValidTimestamps
+ * @covers MWTimestamp::getTimestamp
+ */
+ public function testValidOutput( $format, $expected, $original ) {
+ $timestamp = new MWTimestamp( $original );
+ $this->assertEquals( $expected, (string)$timestamp->getTimestamp( $format ) );
+ }
+
+ /**
+ * Test an invalid timestamp.
+ * @expectedException TimestampException
+ * @covers MWTimestamp
+ */
+ public function testInvalidParse() {
+ new MWTimestamp( "This is not a timestamp." );
+ }
+
+ /**
+ * Test requesting an invalid output format.
+ * @expectedException TimestampException
+ * @covers MWTimestamp::getTimestamp
+ */
+ public function testInvalidOutput() {
+ $timestamp = new MWTimestamp( '1343761268' );
+ $timestamp->getTimestamp( 98 );
+ }
+
+ /**
+ * Returns a list of valid timestamps in the format:
+ * array( type, timestamp_of_type, timestamp_in_MW )
+ */
+ public static function provideValidTimestamps() {
+ return array(
+ // Various formats
+ array( TS_UNIX, '1343761268', '20120731190108' ),
+ array( TS_MW, '20120731190108', '20120731190108' ),
+ array( TS_DB, '2012-07-31 19:01:08', '20120731190108' ),
+ array( TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ),
+ array( TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ),
+ array( TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ),
+ array( TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ),
+ array( TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ),
+ array( TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ),
+ // Some extremes and weird values
+ array( TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ),
+ array( TS_UNIX, '-62135596801', '00001231235959' )
+ );
+ }
+
+ /**
+ * @dataProvider provideHumanTimestampTests
+ * @covers MWTimestamp::getHumanTimestamp
+ */
+ public function testHumanTimestamp(
+ $tsTime, // The timestamp to format
+ $currentTime, // The time to consider "now"
+ $timeCorrection, // The time offset to use
+ $dateFormat, // The date preference to use
+ $expectedOutput, // The expected output
+ $desc // Description
+ ) {
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->with( 'timecorrection' )
+ ->will( $this->returnValue( $timeCorrection ) );
+
+ $user->expects( $this->any() )
+ ->method( 'getDatePreference' )
+ ->will( $this->returnValue( $dateFormat ) );
+
+ $tsTime = new MWTimestamp( $tsTime );
+ $currentTime = new MWTimestamp( $currentTime );
+
+ $this->assertEquals(
+ $expectedOutput,
+ $tsTime->getHumanTimestamp( $currentTime, $user ),
+ $desc
+ );
+ }
+
+ public static function provideHumanTimestampTests() {
+ return array(
+ array(
+ '20111231170000',
+ '20120101000000',
+ 'Offset|0',
+ 'mdy',
+ 'Yesterday at 17:00',
+ '"Yesterday" across years',
+ ),
+ array(
+ '20120717190900',
+ '20120717190929',
+ 'Offset|0',
+ 'mdy',
+ 'just now',
+ '"Just now"',
+ ),
+ array(
+ '20120717190900',
+ '20120717191530',
+ 'Offset|0',
+ 'mdy',
+ '6 minutes ago',
+ 'X minutes ago',
+ ),
+ array(
+ '20121006173100',
+ '20121006173200',
+ 'Offset|0',
+ 'mdy',
+ '1 minute ago',
+ '"1 minute ago"',
+ ),
+ array(
+ '20120617190900',
+ '20120717190900',
+ 'Offset|0',
+ 'mdy',
+ 'June 17',
+ 'Another month'
+ ),
+ array(
+ '19910130151500',
+ '20120716193700',
+ 'Offset|0',
+ 'mdy',
+ '15:15, January 30, 1991',
+ 'Different year',
+ ),
+ array(
+ '20120101050000',
+ '20120101080000',
+ 'Offset|-360',
+ 'mdy',
+ 'Yesterday at 23:00',
+ '"Yesterday" across years with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120716184300',
+ 'Offset|-420',
+ 'mdy',
+ 'Saturday at 11:43',
+ 'Recent weekday with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120715040000',
+ 'Offset|-420',
+ 'mdy',
+ '11:43',
+ 'Today at another time with time correction',
+ ),
+ array(
+ '20120617190900',
+ '20120717190900',
+ 'Offset|0',
+ 'dmy',
+ '17 June',
+ 'Another month with dmy'
+ ),
+ array(
+ '20120617190900',
+ '20120717190900',
+ 'Offset|0',
+ 'ISO 8601',
+ '06-17',
+ 'Another month with ISO-8601'
+ ),
+ array(
+ '19910130151500',
+ '20120716193700',
+ 'Offset|0',
+ 'ISO 8601',
+ '1991-01-30T15:15:00',
+ 'Different year with ISO-8601',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRelativeTimestampTests
+ * @covers MWTimestamp::getRelativeTimestamp
+ */
+ public function testRelativeTimestamp(
+ $tsTime, // The timestamp to format
+ $currentTime, // The time to consider "now"
+ $timeCorrection, // The time offset to use
+ $dateFormat, // The date preference to use
+ $expectedOutput, // The expected output
+ $desc // Description
+ ) {
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->with( 'timecorrection' )
+ ->will( $this->returnValue( $timeCorrection ) );
+
+ $tsTime = new MWTimestamp( $tsTime );
+ $currentTime = new MWTimestamp( $currentTime );
+
+ $this->assertEquals(
+ $expectedOutput,
+ $tsTime->getRelativeTimestamp( $currentTime, $user ),
+ $desc
+ );
+ }
+
+ public static function provideRelativeTimestampTests() {
+ return array(
+ array(
+ '20111231170000',
+ '20120101000000',
+ 'Offset|0',
+ 'mdy',
+ '7 hours ago',
+ '"Yesterday" across years',
+ ),
+ array(
+ '20120717190900',
+ '20120717190929',
+ 'Offset|0',
+ 'mdy',
+ '29 seconds ago',
+ '"Just now"',
+ ),
+ array(
+ '20120717190900',
+ '20120717191530',
+ 'Offset|0',
+ 'mdy',
+ '6 minutes and 30 seconds ago',
+ 'Combination of multiple units',
+ ),
+ array(
+ '20121006173100',
+ '20121006173200',
+ 'Offset|0',
+ 'mdy',
+ '1 minute ago',
+ '"1 minute ago"',
+ ),
+ array(
+ '19910130151500',
+ '20120716193700',
+ 'Offset|0',
+ 'mdy',
+ '2 decades, 1 year, 168 days, 2 hours, 8 minutes and 48 seconds ago',
+ 'A long time ago',
+ ),
+ array(
+ '20120101050000',
+ '20120101080000',
+ 'Offset|-360',
+ 'mdy',
+ '3 hours ago',
+ '"Yesterday" across years with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120716184300',
+ 'Offset|-420',
+ 'mdy',
+ '2 days ago',
+ 'Recent weekday with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120715040000',
+ 'Offset|-420',
+ 'mdy',
+ '9 hours and 17 minutes ago',
+ 'Today at another time with time correction',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php
new file mode 100644
index 00000000..3079d73a
--- /dev/null
+++ b/tests/phpunit/includes/TitleMethodsTest.php
@@ -0,0 +1,300 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ *
+ * @note: We don't make assumptions about the main namespace.
+ * But we do expect the Help namespace to contain Wikitext.
+ */
+class TitleMethodsTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ global $wgContLang;
+
+ parent::setUp();
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgExtraNamespaces',
+ array(
+ 12302 => 'TEST-JS',
+ 12303 => 'TEST-JS_TALK',
+ )
+ );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgNamespaceContentModels',
+ array(
+ 12302 => CONTENT_MODEL_JAVASCRIPT,
+ )
+ );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ public function tearDown() {
+ global $wgContLang;
+
+ parent::tearDown();
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ public static function provideEquals() {
+ return array(
+ array( 'Main Page', 'Main Page', true ),
+ array( 'Main Page', 'Not The Main Page', false ),
+ array( 'Main Page', 'Project:Main Page', false ),
+ array( 'File:Example.png', 'Image:Example.png', true ),
+ array( 'Special:Version', 'Special:Version', true ),
+ array( 'Special:Version', 'Special:Recentchanges', false ),
+ array( 'Special:Version', 'Main Page', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideEquals
+ * @covers Title::equals
+ */
+ public function testEquals( $titleA, $titleB, $expectedBool ) {
+ $titleA = Title::newFromText( $titleA );
+ $titleB = Title::newFromText( $titleB );
+
+ $this->assertEquals( $expectedBool, $titleA->equals( $titleB ) );
+ $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) );
+ }
+
+ public static function provideInNamespace() {
+ return array(
+ array( 'Main Page', NS_MAIN, true ),
+ array( 'Main Page', NS_TALK, false ),
+ array( 'Main Page', NS_USER, false ),
+ array( 'User:Foo', NS_USER, true ),
+ array( 'User:Foo', NS_USER_TALK, false ),
+ array( 'User:Foo', NS_TEMPLATE, false ),
+ array( 'User_talk:Foo', NS_USER_TALK, true ),
+ array( 'User_talk:Foo', NS_USER, false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInNamespace
+ * @covers Title::inNamespace
+ */
+ public function testInNamespace( $title, $ns, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->inNamespace( $ns ) );
+ }
+
+ /**
+ * @covers Title::inNamespaces
+ */
+ public function testInNamespaces() {
+ $mainpage = Title::newFromText( 'Main Page' );
+ $this->assertTrue( $mainpage->inNamespaces( NS_MAIN, NS_USER ) );
+ $this->assertTrue( $mainpage->inNamespaces( array( NS_MAIN, NS_USER ) ) );
+ $this->assertTrue( $mainpage->inNamespaces( array( NS_USER, NS_MAIN ) ) );
+ $this->assertFalse( $mainpage->inNamespaces( array( NS_PROJECT, NS_TEMPLATE ) ) );
+ }
+
+ public static function provideHasSubjectNamespace() {
+ return array(
+ array( 'Main Page', NS_MAIN, true ),
+ array( 'Main Page', NS_TALK, true ),
+ array( 'Main Page', NS_USER, false ),
+ array( 'User:Foo', NS_USER, true ),
+ array( 'User:Foo', NS_USER_TALK, true ),
+ array( 'User:Foo', NS_TEMPLATE, false ),
+ array( 'User_talk:Foo', NS_USER_TALK, true ),
+ array( 'User_talk:Foo', NS_USER, true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHasSubjectNamespace
+ * @covers Title::hasSubjectNamespace
+ */
+ public function testHasSubjectNamespace( $title, $ns, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) );
+ }
+
+ public function dataGetContentModel() {
+ return array(
+ array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ),
+ array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ),
+ array( 'MediaWiki:Foo/bar.css', CONTENT_MODEL_CSS ),
+ array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'TEST-JS:Foo', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'TEST-JS:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'TEST-JS:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'TEST-JS_TALK:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetContentModel
+ * @covers Title::getContentModel
+ */
+ public function testGetContentModel( $title, $expectedModelId ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedModelId, $title->getContentModel() );
+ }
+
+ /**
+ * @dataProvider dataGetContentModel
+ * @covers Title::hasContentModel
+ */
+ public function testHasContentModel( $title, $expectedModelId ) {
+ $title = Title::newFromText( $title );
+ $this->assertTrue( $title->hasContentModel( $expectedModelId ) );
+ }
+
+ public static function provideIsCssOrJsPage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.js', false ),
+ array( 'Help:Foo/bar.js', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo/bar.js', false ),
+ array( 'User:Foo/bar.css', false ),
+ array( 'User talk:Foo/bar.css', false ),
+ array( 'User:Foo/bar.js.xxx', false ),
+ array( 'User:Foo/bar.xxx', false ),
+ array( 'MediaWiki:Foo.js', true ),
+ array( 'MediaWiki:Foo.css', true ),
+ array( 'MediaWiki:Foo.JS', false ),
+ array( 'MediaWiki:Foo.CSS', false ),
+ array( 'MediaWiki:Foo.css.xxx', false ),
+ array( 'TEST-JS:Foo', false ),
+ array( 'TEST-JS:Foo.js', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCssOrJsPage
+ * @covers Title::isCssOrJsPage
+ */
+ public function testIsCssOrJsPage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isCssOrJsPage() );
+ }
+
+ public static function provideIsCssJsSubpage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.js', false ),
+ array( 'Help:Foo/bar.js', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo/bar.js', true ),
+ array( 'User:Foo/bar.css', true ),
+ array( 'User talk:Foo/bar.css', false ),
+ array( 'User:Foo/bar.js.xxx', false ),
+ array( 'User:Foo/bar.xxx', false ),
+ array( 'MediaWiki:Foo.js', false ),
+ array( 'User:Foo/bar.JS', false ),
+ array( 'User:Foo/bar.CSS', false ),
+ array( 'TEST-JS:Foo', false ),
+ array( 'TEST-JS:Foo.js', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCssJsSubpage
+ * @covers Title::isCssJsSubpage
+ */
+ public function testIsCssJsSubpage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isCssJsSubpage() );
+ }
+
+ public static function provideIsCssSubpage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.css', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo.css', false ),
+ array( 'User:Foo/bar.js', false ),
+ array( 'User:Foo/bar.css', true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCssSubpage
+ * @covers Title::isCssSubpage
+ */
+ public function testIsCssSubpage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isCssSubpage() );
+ }
+
+ public static function provideIsJsSubpage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.css', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo.css', false ),
+ array( 'User:Foo/bar.js', true ),
+ array( 'User:Foo/bar.css', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsJsSubpage
+ * @covers Title::isJsSubpage
+ */
+ public function testIsJsSubpage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isJsSubpage() );
+ }
+
+ public static function provideIsWikitextPage() {
+ return array(
+ array( 'Help:Foo', true ),
+ array( 'Help:Foo.js', true ),
+ array( 'Help:Foo/bar.js', true ),
+ array( 'User:Foo', true ),
+ array( 'User:Foo.js', true ),
+ array( 'User:Foo/bar.js', false ),
+ array( 'User:Foo/bar.css', false ),
+ array( 'User talk:Foo/bar.css', true ),
+ array( 'User:Foo/bar.js.xxx', true ),
+ array( 'User:Foo/bar.xxx', true ),
+ array( 'MediaWiki:Foo.js', false ),
+ array( 'MediaWiki:Foo.css', false ),
+ array( 'MediaWiki:Foo/bar.css', false ),
+ array( 'User:Foo/bar.JS', true ),
+ array( 'User:Foo/bar.CSS', true ),
+ array( 'TEST-JS:Foo', false ),
+ array( 'TEST-JS:Foo.js', false ),
+ array( 'TEST-JS_TALK:Foo.js', true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsWikitextPage
+ * @covers Title::isWikitextPage
+ */
+ public function testIsWikitextPage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isWikitextPage() );
+ }
+}
diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php
new file mode 100644
index 00000000..f15c1772
--- /dev/null
+++ b/tests/phpunit/includes/TitlePermissionTest.php
@@ -0,0 +1,742 @@
+<?php
+
+/**
+ * @group Database
+ * @todo covers tags
+ */
+class TitlePermissionTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var string
+ */
+ protected $userName, $altUserName;
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var User
+ */
+ protected $user, $anonUser, $userUser, $altUser;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $langObj = Language::factory( 'en' );
+ $localZone = 'UTC';
+ $localOffset = date( 'Z' ) / 60;
+
+ $this->setMwGlobals( array(
+ 'wgMemc' => new EmptyBagOStuff,
+ 'wgContLang' => $langObj,
+ 'wgLanguageCode' => 'en',
+ 'wgLang' => $langObj,
+ 'wgLocaltimezone' => $localZone,
+ 'wgLocalTZoffset' => $localOffset,
+ 'wgNamespaceProtection' => array(
+ NS_MEDIAWIKI => 'editinterface',
+ ),
+ ) );
+
+ $this->userName = 'Useruser';
+ $this->altUserName = 'Altuseruser';
+ date_default_timezone_set( $localZone );
+
+ $this->title = Title::makeTitle( NS_MAIN, "Main Page" );
+ if ( !isset( $this->userUser ) || !( $this->userUser instanceOf User ) ) {
+ $this->userUser = User::newFromName( $this->userName );
+
+ if ( !$this->userUser->getID() ) {
+ $this->userUser = User::createNew( $this->userName, array(
+ "email" => "test@example.com",
+ "real_name" => "Test User" ) );
+ $this->userUser->load();
+ }
+
+ $this->altUser = User::newFromName( $this->altUserName );
+ if ( !$this->altUser->getID() ) {
+ $this->altUser = User::createNew( $this->altUserName, array(
+ "email" => "alttest@example.com",
+ "real_name" => "Test User Alt" ) );
+ $this->altUser->load();
+ }
+
+ $this->anonUser = User::newFromId( 0 );
+
+ $this->user = $this->userUser;
+ }
+ }
+
+ protected function setUserPerm( $perm ) {
+ // Setting member variables is evil!!!
+
+ if ( is_array( $perm ) ) {
+ $this->user->mRights = $perm;
+ } else {
+ $this->user->mRights = array( $perm );
+ }
+ }
+
+ protected function setTitle( $ns, $title = "Main_Page" ) {
+ $this->title = Title::makeTitle( $ns, $title );
+ }
+
+ protected function setUser( $userName = null ) {
+ if ( $userName === 'anon' ) {
+ $this->user = $this->anonUser;
+ } elseif ( $userName === null || $userName === $this->userName ) {
+ $this->user = $this->userUser;
+ } else {
+ $this->user = $this->altUser;
+ }
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testQuickPermissions() {
+ global $wgContLang;
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( "nocreatetext" ) ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreatetext' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreatetext' ) ), $res );
+
+ $this->setUser( $this->userName );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setUser( $this->userName );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenotallowed' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setUser( $this->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' ) ) );
+
+ if ( $this->isWikitextNS( NS_MAIN ) ) {
+ //NOTE: some content models don't allow moving
+ // @todo find a Wikitext namespace for testing
+
+ $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( $this->userName );
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ) );
+
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array() );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( 'move' );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setUserPerm( '' );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( array( 'movenotallowed' ) ), $res );
+ }
+
+ $this->setTitle( NS_USER );
+ $this->setUser( $this->userName );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setUserPerm( "move" );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( array( 'cant-move-to-user-page' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_USER, "User/subpage" );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setUserPerm( "move" );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->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 ) );
+
+ foreach ( array( "edit", "protect", "" ) as $action ) {
+ $this->setUserPerm( null );
+ $this->assertEquals( $check[$action][0],
+ $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
+
+ global $wgGroupPermissions;
+ $old = $wgGroupPermissions;
+ $wgGroupPermissions = array();
+
+ $this->assertEquals( $check[$action][1],
+ $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
+ $wgGroupPermissions = $old;
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][2],
+ $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][3],
+ $this->title->userCan( $action, $this->user, true ) );
+ $this->assertEquals( $check[$action][3],
+ $this->title->quickUserCan( $action, $this->user ) );
+ # count( User::getGroupsWithPermissions( $action ) ) < 1
+ }
+ }
+
+ protected function runGroupPermissions( $action, $result, $result2 = null ) {
+ global $wgGroupPermissions;
+
+ if ( $result2 === null ) {
+ $result2 = $result;
+ }
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result2, $res );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testSpecialsAndNSPermissions() {
+ global $wgNamespaceProtection;
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_SPECIAL );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $wgNamespaceProtection[NS_USER] = array( 'bogus' );
+
+ $this->setTitle( NS_USER );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $wgNamespaceProtection = null;
+
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'bogus', $this->user ) );
+
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'bogus', $this->user ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testCssAndJavascriptPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.js' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'mycustomjsprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomjsprotected' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomjsprotected' ) ),
+ array( array( 'badaccess-group0' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.css' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'mycustomcssprotected' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomcssprotected' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomcssprotected' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.js' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ),
+ array( array( 'badaccess-group0' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.css' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/tempo' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) )
+ );
+ }
+
+ protected function runCSSandJSPermissions( $result0, $result1, $result2, $result3, $result4 ) {
+ $this->setUserPerm( '' );
+ $this->assertEquals( $result0,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editmyusercss' );
+ $this->assertEquals( $result1,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editmyuserjs' );
+ $this->assertEquals( $result2,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editusercss' );
+ $this->assertEquals( $result3,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'edituserjs' );
+ $this->assertEquals( $result4,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editusercssjs' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( array( 'edituserjs', 'editusercss' ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testPageRestrictions() {
+ global $wgContLang;
+
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $this->setTitle( NS_MAIN );
+ $this->title->mRestrictionsLoaded = true;
+ $this->setUserPerm( "edit" );
+ $this->title->mRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+
+ $this->assertEquals( true,
+ $this->title->quickUserCan( 'edit', $this->user ) );
+ $this->title->mRestrictions = array( "edit" => array( 'bogus', "sysop", "protect", "" ),
+ "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->setUserPerm( "" );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->setUserPerm( array( "edit", "editprotected" ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array(
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+
+ $this->title->mCascadeRestriction = true;
+ $this->setUserPerm( "edit" );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'edit', $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+
+ $this->setUserPerm( array( "edit", "editprotected" ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'edit', $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ }
+
+ public function testCascadingSourcesRestrictions() {
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->setUserPerm( array( "edit", "bogus" ) );
+
+ $this->title->mCascadeSources = array( Title::makeTitle( NS_MAIN, "Bogus" ), Title::makeTitle( NS_MAIN, "UnBogus" ) );
+ $this->title->mCascadingRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( false,
+ $this->title->userCan( 'bogus', $this->user ) );
+ $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ),
+ array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ),
+ array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->assertEquals( true,
+ $this->title->userCan( 'edit', $this->user ) );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testActionPermissions() {
+ $this->setUserPerm( array( "createpage" ) );
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->title->mTitleProtection['pt_create_perm'] = '';
+ $this->title->mTitleProtection['pt_user'] = $this->user->getID();
+ $this->title->mTitleProtection['pt_expiry'] = wfGetDB( DB_SLAVE )->getInfinity();
+ $this->title->mTitleProtection['pt_reason'] = 'test';
+ $this->title->mCascadeRestriction = false;
+
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'create', $this->user ) );
+
+ $this->title->mTitleProtection['pt_create_perm'] = 'sysop';
+ $this->setUserPerm( array( 'createpage', 'protect' ) );
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'create', $this->user ) );
+
+ $this->setUserPerm( array( 'createpage', 'editprotected' ) );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'create', $this->user ) );
+
+ $this->setUserPerm( array( 'createpage' ) );
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'create', $this->user ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->setUserPerm( array( "move" ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move', $this->user ) );
+ $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+
+ $this->setTitle( NS_HELP, "test page" );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'move', $this->user ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-source-page' ) ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move', $this->user ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move-target', $this->user ) );
+ $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+
+ $this->setTitle( NS_HELP, "test page" );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'move-target', $this->user ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-target-page' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move-target', $this->user ) );
+ }
+
+ public function testUserBlock() {
+ global $wgEmailConfirmToEdit, $wgEmailAuthentication;
+ $wgEmailConfirmToEdit = true;
+ $wgEmailAuthentication = true;
+
+ $this->setUserPerm( array( "createpage", "move" ) );
+ $this->setTitle( NS_HELP, "test page" );
+
+ # $short
+ $this->assertEquals( array( array( 'confirmedittext' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $wgEmailConfirmToEdit = false;
+ $this->assertEquals( true, $this->title->userCan( 'move-target', $this->user ) );
+
+ # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'move-target',
+ $this->user ) );
+
+ global $wgLang;
+ $prev = time();
+ $now = time() + 120;
+ $this->user->mBlockedby = $this->user->getId();
+ $this->user->mBlock = new Block( '127.0.8.1', 0, $this->user->getId(),
+ 'no reason given', $prev + 3600, 1, 0 );
+ $this->user->mBlock->mTimestamp = 0;
+ $this->assertEquals( array( array( 'autoblockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, 'infinite', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ) ),
+ $this->title->getUserPermissionsErrors( 'move-target',
+ $this->user ) );
+
+ $this->assertEquals( false, $this->title->userCan( 'move-target', $this->user ) );
+ // quickUserCan should ignore user blocks
+ $this->assertEquals( true, $this->title->quickUserCan( 'move-target', $this->user ) );
+
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = -60;
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( '127.0.8.1', 0, $this->user->getId(),
+ 'no reason given', $now, 0, 10 );
+ $this->assertEquals( array( array( 'blockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->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 00000000..6bfe5453
--- /dev/null
+++ b/tests/phpunit/includes/TitleTest.php
@@ -0,0 +1,485 @@
+<?php
+
+/**
+ * @group Database
+ * ^--- needed for language cache stuff
+ */
+class TitleTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => Language::factory( 'en' ),
+ // User language
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgAllowUserJs' => false,
+ 'wgDefaultLanguageVariant' => false,
+ ) );
+ }
+
+ /**
+ * @covers Title::legalChars
+ */
+ public 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" );
+ }
+ }
+ }
+
+ /**
+ * See also mediawiki.Title.test.js
+ * @covers Title::secureAndSplit
+ * @todo This method should be split into 2 separate tests each with a provider
+ */
+ public function testSecureAndSplit() {
+ // Valid
+ foreach ( array(
+ 'Sandbox',
+ 'A "B"',
+ 'A \'B\'',
+ '.com',
+ '~',
+ '"',
+ '\'',
+ 'Talk:Sandbox',
+ 'Talk:Foo:Sandbox',
+ 'File:Example.svg',
+ 'File_talk:Example.svg',
+ 'Foo/.../Sandbox',
+ 'Sandbox/...',
+ 'A~~',
+ // Length is 256 total, but only title part matters
+ 'Category:' . str_repeat( 'x', 248 ),
+ str_repeat( 'x', 252 )
+ ) as $text ) {
+ $this->assertInstanceOf( 'Title', Title::newFromText( $text ), "Valid: $text" );
+ }
+
+ // Invalid
+ foreach ( array(
+ '',
+ '__ __',
+ ' __ ',
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ 'A [ B',
+ 'A ] B',
+ 'A { B',
+ 'A } B',
+ 'A < B',
+ 'A > B',
+ 'A | B',
+ // URL encoding
+ 'A%20B',
+ 'A%23B',
+ 'A%2523B',
+ // XML/HTML character entity references
+ // Note: Commented out because they are not marked invalid by the PHP test as
+ // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
+ //'A &eacute; B',
+ //'A &#233; B',
+ //'A &#x00E9; B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ 'Talk:File:Example.svg',
+ // Directory navigation
+ '.',
+ '..',
+ './Sandbox',
+ '../Sandbox',
+ 'Foo/./Sandbox',
+ 'Foo/../Sandbox',
+ 'Sandbox/.',
+ 'Sandbox/..',
+ // Tilde
+ 'A ~~~ Name',
+ 'A ~~~~ Signature',
+ 'A ~~~~~ Timestamp',
+ str_repeat( 'x', 256 ),
+ // Namespace prefix without actual title
+ // ':', // bug 54044
+ 'Talk:',
+ 'Category: ',
+ 'Category: #bar'
+ ) as $text ) {
+ $this->assertNull( Title::newFromText( $text ), "Invalid: $text" );
+ }
+ }
+
+ public static function provideConvertByteClassToUnicodeClass() {
+ return array(
+ array(
+ ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+',
+ ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTYf-\\xFF+',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\x66-\\xFD+',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTYf-y+',
+ 'QWERTYf-y+',
+ ),
+ array(
+ 'QWERTYf-\\x80+',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\x66-\\x80+\\x23',
+ 'QWERTYf-\\x7F+#\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\x66-\\x80+\\xD3',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\\\\\x99',
+ '\\\\\\u0080-\\uFFFF',
+ ),
+ array(
+ '-\\x99',
+ '\\-\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\-\\x99',
+ 'QWERTY\\-\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\\\x99',
+ '\\\\x99',
+ ),
+ array(
+ 'A-\\x9F',
+ 'A-\\x7F\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\x66-\\x77QWERTY\\x88-\\x91FXZ',
+ 'f-wQWERTYFXZ\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ',
+ 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideConvertByteClassToUnicodeClass
+ * @covers Title::convertByteClassToUnicodeClass
+ */
+ public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) {
+ $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) );
+ }
+
+ /**
+ * @dataProvider provideBug31100
+ * @covers Title::fixSpecialName
+ */
+ public function testBug31100FixSpecialName( $text, $expectedParam ) {
+ $title = Title::newFromText( $text );
+ $fixed = $title->fixSpecialName();
+ $stuff = explode( '/', $fixed->getDBkey(), 2 );
+ if ( count( $stuff ) == 2 ) {
+ $par = $stuff[1];
+ } else {
+ $par = null;
+ }
+ $this->assertEquals( $expectedParam, $par, "Bug 31100 regression check: Title->fixSpecialName() should preserve parameter" );
+ }
+
+ public static function provideBug31100() {
+ return array(
+ array( 'Special:Version', null ),
+ array( 'Special:Version/', '' ),
+ array( 'Special:Version/param', 'param' ),
+ );
+ }
+
+ /**
+ * Auth-less test of Title::isValidMoveOperation
+ *
+ * @group Database
+ * @param string $source
+ * @param string $target
+ * @param array|string|bool $expected Required error
+ * @dataProvider provideTestIsValidMoveOperation
+ * @covers Title::isValidMoveOperation
+ */
+ public function testIsValidMoveOperation( $source, $target, $expected ) {
+ $title = Title::newFromText( $source );
+ $nt = Title::newFromText( $target );
+ $errors = $title->isValidMoveOperation( $nt, false );
+ if ( $expected === true ) {
+ $this->assertTrue( $errors );
+ } else {
+ $errors = $this->flattenErrorsArray( $errors );
+ foreach ( (array)$expected as $error ) {
+ $this->assertContains( $error, $errors );
+ }
+ }
+ }
+
+ /**
+ * Provides test parameter values for testIsValidMoveOperation()
+ */
+ public function dataTestIsValidMoveOperation() {
+ return array(
+ array( 'Test', 'Test', 'selfmove' ),
+ array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
+ );
+ }
+
+ /**
+ * Auth-less test of Title::userCan
+ *
+ * @param array $whitelistRegexp
+ * @param string $source
+ * @param string $action
+ * @param array|string|bool $expected Required error
+ *
+ * @covers Title::checkReadPermissions
+ * @dataProvider dataWgWhitelistReadRegexp
+ */
+ public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {
+ // $wgWhitelistReadRegexp must be an array. Since the provided test cases
+ // usually have only one regex, it is more concise to write the lonely regex
+ // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp
+ // type requisite.
+ if ( is_string( $whitelistRegexp ) ) {
+ $whitelistRegexp = array( $whitelistRegexp );
+ }
+
+ $title = Title::newFromDBkey( $source );
+
+ global $wgGroupPermissions;
+ $oldPermissions = $wgGroupPermissions;
+ // Disallow all so we can ensure our regex works
+ $wgGroupPermissions = array();
+ $wgGroupPermissions['*']['read'] = false;
+
+ global $wgWhitelistRead;
+ $oldWhitelist = $wgWhitelistRead;
+ // Undo any LocalSettings explicite whitelists so they won't cause a
+ // failing test to succeed. Set it to some random non sense just
+ // to make sure we properly test Title::checkReadPermissions()
+ $wgWhitelistRead = array( 'some random non sense title' );
+
+ global $wgWhitelistReadRegexp;
+ $oldWhitelistRegexp = $wgWhitelistReadRegexp;
+ $wgWhitelistReadRegexp = $whitelistRegexp;
+
+ // Just use $wgUser which in test is a user object for '127.0.0.1'
+ global $wgUser;
+ // Invalidate user rights cache to take in account $wgGroupPermissions
+ // change above.
+ $wgUser->clearInstanceCache();
+ $errors = $title->userCan( $action, $wgUser );
+
+ // Restore globals
+ $wgGroupPermissions = $oldPermissions;
+ $wgWhitelistRead = $oldWhitelist;
+ $wgWhitelistReadRegexp = $oldWhitelistRegexp;
+
+ if ( is_bool( $expected ) ) {
+ # Forge the assertion message depending on the assertion expectation
+ $allowableness = $expected
+ ? " should be allowed"
+ : " should NOT be allowed";
+ $this->assertEquals( $expected, $errors, "User action '$action' on [[$source]] $allowableness." );
+ } else {
+ $errors = $this->flattenErrorsArray( $errors );
+ foreach ( (array)$expected as $error ) {
+ $this->assertContains( $error, $errors );
+ }
+ }
+ }
+
+ /**
+ * Provides test parameter values for testWgWhitelistReadRegexp()
+ */
+ public function dataWgWhitelistReadRegexp() {
+ $ALLOWED = true;
+ $DISALLOWED = false;
+
+ return array(
+ // Everything, if this doesn't work, we're really in trouble
+ array( '/.*/', 'Main_Page', 'read', $ALLOWED ),
+ array( '/.*/', 'Main_Page', 'edit', $DISALLOWED ),
+
+ // We validate against the title name, not the db key
+ array( '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ),
+ // Main page
+ array( '/^Main/', 'Main_Page', 'read', $ALLOWED ),
+ array( '/^Main.*/', 'Main_Page', 'read', $ALLOWED ),
+ // With spaces
+ array( '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ),
+ // Unicode multibyte
+ // ...without unicode modifier
+ array( '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ),
+ // ...with unicode modifier
+ array( '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ),
+ // Case insensitive
+ array( '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ),
+ array( '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ),
+
+ // From DefaultSettings.php:
+ array( "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ),
+ array( "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ),
+
+ // With namespaces:
+ array( '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ),
+ array( null, 'Special:Newpages', 'read', $DISALLOWED ),
+
+ );
+ }
+
+ public function flattenErrorsArray( $errors ) {
+ $result = array();
+ foreach ( $errors as $error ) {
+ $result[] = $error[0];
+ }
+
+ return $result;
+ }
+
+ public static function provideTestIsValidMoveOperation() {
+ return array(
+ array( 'Test', 'Test', 'selfmove' ),
+ array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPageViewLanguage
+ * @covers Title::getPageViewLanguage
+ */
+ public function testGetPageViewLanguage( $expected, $titleText, $contLang, $lang, $variant, $msg = '' ) {
+ global $wgLanguageCode, $wgContLang, $wgLang, $wgDefaultLanguageVariant, $wgAllowUserJs;
+
+ // Setup environnement for this test
+ $wgLanguageCode = $contLang;
+ $wgContLang = Language::factory( $contLang );
+ $wgLang = Language::factory( $lang );
+ $wgDefaultLanguageVariant = $variant;
+ $wgAllowUserJs = true;
+
+ $title = Title::newFromText( $titleText );
+ $this->assertInstanceOf( 'Title', $title,
+ "Test must be passed a valid title text, you gave '$titleText'"
+ );
+ $this->assertEquals( $expected,
+ $title->getPageViewLanguage()->getCode(),
+ $msg
+ );
+ }
+
+ public static function provideGetPageViewLanguage() {
+ # Format:
+ # - expected
+ # - Title name
+ # - wgContLang (expected in most case)
+ # - wgLang (on some specific pages)
+ # - wgDefaultLanguageVariant
+ # - Optional message
+ return array(
+ array( 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ),
+ array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ),
+ array( 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ),
+
+ array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ),
+
+ array( 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ),
+
+ array( 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ),
+
+ );
+ }
+
+ /**
+ * @dataProvider provideBaseTitleCases
+ * @covers Title::getBaseText
+ */
+ public function testGetBaseText( $title, $expected, $msg = '' ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expected,
+ $title->getBaseText(),
+ $msg
+ );
+ }
+
+ public static function provideBaseTitleCases() {
+ return array(
+ # Title, expected base, optional message
+ array( 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ),
+ array( 'User:Foo/Bar/Baz', 'Foo/Bar' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRootTitleCases
+ * @covers Title::getRootText
+ */
+ public function testGetRootText( $title, $expected, $msg = '' ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expected,
+ $title->getRootText(),
+ $msg
+ );
+ }
+
+ public static function provideRootTitleCases() {
+ return array(
+ # Title, expected base, optional message
+ array( 'User:John_Doe/subOne/subTwo', 'John Doe' ),
+ array( 'User:Foo/Bar/Baz', 'Foo' ),
+ );
+ }
+
+ /**
+ * @todo Handle $wgNamespacesWithSubpages cases
+ * @dataProvider provideSubpageTitleCases
+ * @covers Title::getSubpageText
+ */
+ public function testGetSubpageText( $title, $expected, $msg = '' ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expected,
+ $title->getSubpageText(),
+ $msg
+ );
+ }
+
+ public static function provideSubpageTitleCases() {
+ return array(
+ # Title, expected base, optional message
+ array( 'User:John_Doe/subOne/subTwo', 'subTwo' ),
+ array( 'User:John_Doe/subOne', 'subOne' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/UIDGeneratorTest.php b/tests/phpunit/includes/UIDGeneratorTest.php
new file mode 100644
index 00000000..8f78ae51
--- /dev/null
+++ b/tests/phpunit/includes/UIDGeneratorTest.php
@@ -0,0 +1,98 @@
+<?php
+
+class UIDGeneratorTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provider_testTimestampedUID
+ * @covers UIDGenerator::newTimestampedUID128
+ * @covers UIDGenerator::newTimestampedUID88
+ */
+ public function testTimestampedUID( $method, $digitlen, $bits, $tbits, $hostbits ) {
+ $id = call_user_func( array( 'UIDGenerator', $method ) );
+ $this->assertEquals( true, ctype_digit( $id ), "UID made of digit characters" );
+ $this->assertLessThanOrEqual( $digitlen, strlen( $id ),
+ "UID has the right number of digits" );
+ $this->assertLessThanOrEqual( $bits, strlen( wfBaseConvert( $id, 10, 2 ) ),
+ "UID has the right number of bits" );
+
+ $ids = array();
+ for ( $i = 0; $i < 300; $i++ ) {
+ $ids[] = call_user_func( array( 'UIDGenerator', $method ) );
+ }
+
+ $lastId = array_shift( $ids );
+ if ( $hostbits ) {
+ $lastHost = substr( wfBaseConvert( $lastId, 10, 2, $bits ), -$hostbits );
+ }
+
+ $this->assertArrayEquals( array_unique( $ids ), $ids, "All generated IDs are unique." );
+
+ foreach ( $ids as $id ) {
+ $id_bin = wfBaseConvert( $id, 10, 2 );
+ $lastId_bin = wfBaseConvert( $lastId, 10, 2 );
+
+ $this->assertGreaterThanOrEqual(
+ substr( $id_bin, 0, $tbits ),
+ substr( $lastId_bin, 0, $tbits ),
+ "New ID timestamp ($id_bin) >= prior one ($lastId_bin)." );
+
+ if ( $hostbits ) {
+ $this->assertEquals(
+ substr( $id_bin, 0, -$hostbits ),
+ substr( $lastId_bin, 0, -$hostbits ),
+ "Host ID of ($id_bin) is same as prior one ($lastId_bin)." );
+ }
+
+ $lastId = $id;
+ }
+ }
+
+ /**
+ * array( method, length, bits, hostbits )
+ * NOTE: When adding a new method name here please update the covers tags for the tests!
+ */
+ public static function provider_testTimestampedUID() {
+ return array(
+ array( 'newTimestampedUID128', 39, 128, 46, 48 ),
+ array( 'newTimestampedUID128', 39, 128, 46, 48 ),
+ array( 'newTimestampedUID88', 27, 88, 46, 32 ),
+ );
+ }
+
+ /**
+ * @covers UIDGenerator::newUUIDv4
+ */
+ public function testUUIDv4() {
+ for ( $i = 0; $i < 100; $i++ ) {
+ $id = UIDGenerator::newUUIDv4();
+ $this->assertEquals( true,
+ preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ),
+ "UID $id has the right format" );
+ }
+ }
+
+ /**
+ * @covers UIDGenerator::newRawUUIDv4
+ */
+ public function testRawUUIDv4() {
+ for ( $i = 0; $i < 100; $i++ ) {
+ $id = UIDGenerator::newRawUUIDv4();
+ $this->assertEquals( true,
+ preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+ "UID $id has the right format" );
+ }
+ }
+
+ /**
+ * @covers UIDGenerator::newRawUUIDv4
+ */
+ public function testRawUUIDv4QuickRand() {
+ for ( $i = 0; $i < 100; $i++ ) {
+ $id = UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND );
+ $this->assertEquals( true,
+ preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+ "UID $id has the right format" );
+ }
+ }
+
+}
diff --git a/tests/phpunit/includes/UserMailerTest.php b/tests/phpunit/includes/UserMailerTest.php
new file mode 100644
index 00000000..278edfaa
--- /dev/null
+++ b/tests/phpunit/includes/UserMailerTest.php
@@ -0,0 +1,14 @@
+<?php
+
+class UserMailerTest extends MediaWikiLangTestCase {
+
+ /**
+ * @covers UserMailer::quotedPrintable
+ */
+ public function testQuotedPrintable() {
+ $this->assertEquals(
+ "=?UTF-8?Q?=C4=88u=20legebla=3F?=",
+ UserMailer::quotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) );
+ }
+
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php
new file mode 100644
index 00000000..ff33e825
--- /dev/null
+++ b/tests/phpunit/includes/UserTest.php
@@ -0,0 +1,237 @@
+<?php
+
+define( 'NS_UNITTEST', 5600 );
+define( 'NS_UNITTEST_TALK', 5601 );
+
+/**
+ * @group Database
+ */
+class UserTest extends MediaWikiTestCase {
+ /**
+ * @var User
+ */
+ protected $user;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgGroupPermissions' => array(),
+ 'wgRevokePermissions' => array(),
+ ) );
+
+ $this->setUpPermissionGlobals();
+
+ $this->user = new User;
+ $this->user->addGroup( 'unittesters' );
+ }
+
+ private function setUpPermissionGlobals() {
+ global $wgGroupPermissions, $wgRevokePermissions;
+
+ # Data for regular $wgGroupPermissions test
+ $wgGroupPermissions['unittesters'] = array(
+ 'test' => true,
+ 'runtest' => true,
+ 'writetest' => false,
+ 'nukeworld' => false,
+ );
+ $wgGroupPermissions['testwriters'] = array(
+ 'test' => true,
+ 'writetest' => true,
+ 'modifytest' => true,
+ );
+
+ # Data for regular $wgRevokePermissions test
+ $wgRevokePermissions['formertesters'] = array(
+ 'runtest' => true,
+ );
+
+ # For the options test
+ $wgGroupPermissions['*'] = array(
+ 'editmyoptions' => true,
+ );
+ }
+
+ /**
+ * @covers User::getGroupPermissions
+ */
+ public function testGroupPermissions() {
+ $rights = User::getGroupPermissions( array( 'unittesters' ) );
+ $this->assertContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+
+ $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) );
+ $this->assertContains( 'runtest', $rights );
+ $this->assertContains( 'writetest', $rights );
+ $this->assertContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+
+ /**
+ * @covers User::getGroupPermissions
+ */
+ public function testRevokePermissions() {
+ $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) );
+ $this->assertNotContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+
+ /**
+ * @covers User::getRights
+ */
+ public function testUserPermissions() {
+ $rights = $this->user->getRights();
+ $this->assertContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+
+ /**
+ * @dataProvider provideGetGroupsWithPermission
+ * @covers User::getGroupsWithPermission
+ */
+ public function testGetGroupsWithPermission( $expected, $right ) {
+ $result = User::getGroupsWithPermission( $right );
+ sort( $result );
+ sort( $expected );
+
+ $this->assertEquals( $expected, $result, "Groups with permission $right" );
+ }
+
+ public static function provideGetGroupsWithPermission() {
+ return array(
+ array(
+ array( 'unittesters', 'testwriters' ),
+ 'test'
+ ),
+ array(
+ array( 'unittesters' ),
+ 'runtest'
+ ),
+ array(
+ array( 'testwriters' ),
+ 'writetest'
+ ),
+ array(
+ array( 'testwriters' ),
+ 'modifytest'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUserNames
+ * @covers User::isValidUserName
+ */
+ public function testIsValidUserName( $username, $result, $message ) {
+ $this->assertEquals( $this->user->isValidUserName( $username ), $result, $message );
+ }
+
+ public static function provideUserNames() {
+ return array(
+ array( '', false, 'Empty string' ),
+ array( ' ', false, 'Blank space' ),
+ array( 'abcd', false, 'Starts with small letter' ),
+ array( 'Ab/cd', false, 'Contains slash' ),
+ array( 'Ab cd', true, 'Whitespace' ),
+ array( '192.168.1.1', false, 'IP' ),
+ array( 'User:Abcd', false, 'Reserved Namespace' ),
+ array( '12abcd232', true, 'Starts with Numbers' ),
+ array( '?abcd', true, 'Start with ? mark' ),
+ array( '#abcd', false, 'Start with #' ),
+ array( 'Abcdകഖഗഘ', true, ' Mixed scripts' ),
+ array( 'ജോസ്‌തോമസ്', false, 'ZWNJ- Format control character' ),
+ array( 'Ab cd', false, ' Ideographic space' ),
+ );
+ }
+
+ /**
+ * Test, if for all rights a right- message exist,
+ * which is used on Special:ListGroupRights as help text
+ * Extensions and core
+ */
+ public function testAllRightsWithMessage() {
+ //Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights
+ $allRights = User::getAllRights();
+ $allMessageKeys = Language::getMessageKeysFor( 'en' );
+
+ $rightsWithMessage = array();
+ foreach ( $allMessageKeys as $message ) {
+ // === 0: must be at beginning of string (position 0)
+ if ( strpos( $message, 'right-' ) === 0 ) {
+ $rightsWithMessage[] = substr( $message, strlen( 'right-' ) );
+ }
+ }
+
+ sort( $allRights );
+ sort( $rightsWithMessage );
+
+ $this->assertEquals(
+ $allRights,
+ $rightsWithMessage,
+ 'Each user rights (core/extensions) has a corresponding right- message.'
+ );
+ }
+
+ /**
+ * Test User::editCount
+ * @group medium
+ * @covers User::getEditCount
+ */
+ public function testEditCount() {
+ $user = User::newFromName( 'UnitTestUser' );
+ $user->loadDefaults();
+ $user->addToDatabase();
+
+ // let the user have a few (3) edits
+ $page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) );
+ for ( $i = 0; $i < 3; $i++ ) {
+ $page->doEdit( (string)$i, 'test', 0, false, $user );
+ }
+
+ $user->clearInstanceCache();
+ $this->assertEquals( 3, $user->getEditCount(), 'After three edits, the user edit count should be 3' );
+
+ // increase the edit count and clear the cache
+ $user->incEditCount();
+
+ $user->clearInstanceCache();
+ $this->assertEquals( 4, $user->getEditCount(), 'After increasing the edit count manually, the user edit count should be 4' );
+ }
+
+ /**
+ * Test changing user options.
+ * @covers User::setOption
+ * @covers User::getOption
+ */
+ public function testOptions() {
+ $user = User::newFromName( 'UnitTestUser' );
+ $user->addToDatabase();
+
+ $user->setOption( 'someoption', 'test' );
+ $user->setOption( 'cols', 200 );
+ $user->saveSettings();
+
+ $user = User::newFromName( 'UnitTestUser' );
+ $this->assertEquals( 'test', $user->getOption( 'someoption' ) );
+ $this->assertEquals( 200, $user->getOption( 'cols' ) );
+ }
+
+ /**
+ * Bug 37963
+ * Make sure defaults are loaded when setOption is called.
+ * @covers User::loadOptions
+ */
+ public function testAnonOptions() {
+ global $wgDefaultUserOptions;
+ $this->user->setOption( 'someoption', 'test' );
+ $this->assertEquals( $wgDefaultUserOptions['cols'], $this->user->getOption( 'cols' ) );
+ $this->assertEquals( 'test', $this->user->getOption( 'someoption' ) );
+ }
+}
diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php
new file mode 100644
index 00000000..f8ed14b6
--- /dev/null
+++ b/tests/phpunit/includes/WebRequestTest.php
@@ -0,0 +1,310 @@
+<?php
+
+/**
+ * @group WebRequest
+ */
+class WebRequestTest extends MediaWikiTestCase {
+ protected $oldServer;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->oldServer = $_SERVER;
+ }
+
+ protected function tearDown() {
+ $_SERVER = $this->oldServer;
+
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider provideDetectServer
+ * @covers WebRequest::detectServer
+ */
+ public function testDetectServer( $expected, $input, $description ) {
+ $_SERVER = $input;
+ $result = WebRequest::detectServer();
+ $this->assertEquals( $expected, $result, $description );
+ }
+
+ public static function provideDetectServer() {
+ return array(
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x'
+ ),
+ 'Host header'
+ ),
+ array(
+ 'https://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'HTTPS' => 'on',
+ ),
+ 'Host header with secure'
+ ),
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'SERVER_PORT' => 80,
+ ),
+ 'Default SERVER_PORT',
+ ),
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'HTTPS' => 'off',
+ ),
+ 'Secure off'
+ ),
+ array(
+ 'http://y',
+ array(
+ 'SERVER_NAME' => 'y',
+ ),
+ 'Server name'
+ ),
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'SERVER_NAME' => 'y',
+ ),
+ 'Host server name precedence'
+ ),
+ array(
+ 'http://[::1]:81',
+ array(
+ 'HTTP_HOST' => '[::1]',
+ 'SERVER_NAME' => '::1',
+ 'SERVER_PORT' => '81',
+ ),
+ 'Apache bug 26005'
+ ),
+ array(
+ 'http://localhost',
+ array(
+ 'SERVER_NAME' => '[2001'
+ ),
+ 'Kind of like lighttpd per commit message in MW r83847',
+ ),
+ array(
+ 'http://[2a01:e35:2eb4:1::2]:777',
+ array(
+ 'SERVER_NAME' => '[2a01:e35:2eb4:1::2]:777'
+ ),
+ 'Possible lighttpd environment per bug 14977 comment 13',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetIP
+ * @covers WebRequest::getIP
+ */
+ public function testGetIP( $expected, $input, $squid, $xffList, $private, $description ) {
+ $_SERVER = $input;
+ $this->setMwGlobals( array(
+ 'wgSquidServersNoPurge' => $squid,
+ 'wgUsePrivateIPs' => $private,
+ 'wgHooks' => array(
+ 'IsTrustedProxy' => array(
+ function( &$ip, &$trusted ) use ( $xffList ) {
+ $trusted = $trusted || in_array( $ip, $xffList );
+ return true;
+ }
+ )
+ )
+ ) );
+
+ $request = new WebRequest();
+ $result = $request->getIP();
+ $this->assertEquals( $expected, $result, $description );
+ }
+
+ public static function provideGetIP() {
+ return array(
+ array(
+ '127.0.0.1',
+ array(
+ 'REMOTE_ADDR' => '127.0.0.1'
+ ),
+ array(),
+ array(),
+ false,
+ 'Simple IPv4'
+ ),
+ array(
+ '::1',
+ array(
+ 'REMOTE_ADDR' => '::1'
+ ),
+ array(),
+ array(),
+ false,
+ 'Simple IPv6'
+ ),
+ array(
+ '12.0.0.1',
+ array(
+ 'REMOTE_ADDR' => 'abcd:0001:002:03:4:555:6666:7777',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.1, abcd:0001:002:03:4:555:6666:7777',
+ ),
+ array( 'ABCD:1:2:3:4:555:6666:7777' ),
+ array(),
+ false,
+ 'IPv6 normalisation'
+ ),
+ array(
+ '12.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array(),
+ false,
+ 'With X-Forwaded-For'
+ ),
+ array(
+ '12.0.0.1',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array(),
+ false,
+ 'With X-Forwaded-For and disallowed server'
+ ),
+ array(
+ '12.0.0.2',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1' ),
+ array(),
+ false,
+ 'With multiple X-Forwaded-For and only one allowed server'
+ ),
+ array(
+ '10.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array(),
+ false,
+ 'With X-Forwaded-For and private IP (from cache proxy)'
+ ),
+ array(
+ '10.0.0.4',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2', '10.0.0.3' ),
+ array(),
+ true,
+ 'With X-Forwaded-For and private IP (allowed)'
+ ),
+ array(
+ '10.0.0.4',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array( '10.0.0.3' ),
+ true,
+ 'With X-Forwaded-For and private IP (allowed)'
+ ),
+ array(
+ '10.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array( '10.0.0.3' ),
+ false,
+ 'With X-Forwaded-For and private IP (disallowed)'
+ ),
+ array(
+ '12.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array( '12.0.0.1', '12.0.0.2' ),
+ false,
+ 'With X-Forwaded-For'
+ ),
+ array(
+ '12.0.0.2',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array( '12.0.0.1' ),
+ false,
+ 'With multiple X-Forwaded-For and only one allowed server'
+ ),
+ array(
+ '12.0.0.2',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array( '12.0.0.2' ),
+ false,
+ 'With X-Forwaded-For and private IP and hook (disallowed)'
+ ),
+ );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers WebRequest::getIP
+ */
+ public function testGetIpLackOfRemoteAddrThrowAnException() {
+ $request = new WebRequest();
+ # Next call throw an exception about lacking an IP
+ $request->getIP();
+ }
+
+ public static function provideLanguageData() {
+ return array(
+ array( '', array(), 'Empty Accept-Language header' ),
+ array( 'en', array( 'en' => 1 ), 'One language' ),
+ array( 'en, ar', array( 'en' => 1, 'ar' => 1 ), 'Two languages listed in appearance order.' ),
+ array( 'zh-cn,zh-tw', array( 'zh-cn' => 1, 'zh-tw' => 1 ), 'Two equally prefered languages, listed in appearance order per rfc3282. Checks c9119' ),
+ array( 'es, en; q=0.5', array( 'es' => 1, 'en' => '0.5' ), 'Spanish as first language and English and second' ),
+ array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Less prefered language first' ),
+ array( 'fr, en; q=0.5, es', array( 'fr' => 1, 'es' => 1, 'en' => '0.5' ), 'Three languages' ),
+ array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Two languages' ),
+ array( 'en, zh;q=0', array( 'en' => 1 ), "It's Chinese to me" ),
+ array( 'es; q=1, pt;q=0.7, it; q=0.6, de; q=0.1, ru;q=0', array( 'es' => '1', 'pt' => '0.7', 'it' => '0.6', 'de' => '0.1' ), 'Preference for romance languages' ),
+ array( 'en-gb, en-us; q=1', array( 'en-gb' => 1, 'en-us' => '1' ), 'Two equally prefered English variants' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideLanguageData
+ * @covers WebRequest::getAcceptLang
+ */
+ public function testAcceptLang( $acceptLanguageHeader, $expectedLanguages, $description ) {
+ $_SERVER = array( 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader );
+ $request = new WebRequest();
+ $this->assertSame( $request->getAcceptLang(), $expectedLanguages, $description );
+ }
+}
diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php
new file mode 100644
index 00000000..e0d786b9
--- /dev/null
+++ b/tests/phpunit/includes/WikiPageTest.php
@@ -0,0 +1,1074 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ * @group medium
+ **/
+class WikiPageTest extends MediaWikiLangTestCase {
+
+ protected $pages_to_delete;
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge(
+ $this->tablesUsed,
+ array( 'page',
+ 'revision',
+ 'text',
+
+ 'recentchanges',
+ 'logging',
+
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks' ) );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ $this->pages_to_delete = array();
+
+ LinkCache::singleton()->clear(); # avoid cached redirect status, etc
+ }
+
+ protected function tearDown() {
+ foreach ( $this->pages_to_delete as $p ) {
+ /* @var $p WikiPage */
+
+ try {
+ if ( $p->exists() ) {
+ $p->doDeleteArticle( "testing done." );
+ }
+ } catch ( MWException $ex ) {
+ // fail silently
+ }
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * @param Title $title
+ * @param String $model
+ * @return WikiPage
+ */
+ protected function newPage( $title, $model = null ) {
+ if ( is_string( $title ) ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+ }
+
+ $p = new WikiPage( $title );
+
+ $this->pages_to_delete[] = $p;
+
+ return $p;
+ }
+
+ /**
+ * @param String|Title|WikiPage $page
+ * @param String $text
+ * @param int $model
+ *
+ * @return WikiPage
+ */
+ protected function createPage( $page, $text, $model = null ) {
+ if ( is_string( $page ) || $page instanceof Title ) {
+ $page = $this->newPage( $page, $model );
+ }
+
+ $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+ $page->doEditContent( $content, "testing", EDIT_NEW );
+
+ return $page;
+ }
+
+ /**
+ * @covers WikiPage::doEditContent
+ */
+ public function testDoEditContent() {
+ $page = $this->newPage( "WikiPageTest_testDoEditContent" );
+ $title = $page->getTitle();
+
+ $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+ . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
+ $title, CONTENT_MODEL_WIKITEXT );
+
+ $page->doEditContent( $content, "[[testing]] 1" );
+
+ $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+ $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
+ $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
+ $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
+
+ $id = $page->getId();
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getContent();
+ $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+
+ # ------------------------
+ $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+ . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
+ $title, CONTENT_MODEL_WIKITEXT );
+
+ $page->doEditContent( $content, "testing 2" );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getContent();
+ $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::doEdit
+ */
+ public function testDoEdit() {
+ $this->hideDeprecated( "WikiPage::doEdit" );
+ $this->hideDeprecated( "WikiPage::getText" );
+ $this->hideDeprecated( "Revision::getText" );
+
+ //NOTE: assume help namespace will default to wikitext
+ $title = Title::newFromText( "Help:WikiPageTest_testDoEdit" );
+
+ $page = $this->newPage( $title );
+
+ $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+ . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.";
+
+ $page->doEdit( $text, "[[testing]] 1" );
+
+ $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+ $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
+ $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
+ $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
+
+ $id = $page->getId();
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getText();
+ $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' );
+
+ # ------------------------
+ $text = "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+ . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.";
+
+ $page->doEdit( $text, "testing 2" );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getText();
+ $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::doQuickEdit
+ */
+ public function testDoQuickEdit() {
+ global $wgUser;
+
+ $this->hideDeprecated( "WikiPage::doQuickEdit" );
+
+ //NOTE: assume help namespace will default to wikitext
+ $page = $this->createPage( "Help:WikiPageTest_testDoQuickEdit", "original text" );
+
+ $text = "quick text";
+ $page->doQuickEdit( $text, $wgUser, "testing q" );
+
+ # ---------------------
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $text, $page->getText() );
+ }
+
+ /**
+ * @covers WikiPage::doQuickEditContent
+ */
+ public function testDoQuickEditContent() {
+ global $wgUser;
+
+ $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text", CONTENT_MODEL_WIKITEXT );
+
+ $content = ContentHandler::makeContent( "quick text", $page->getTitle(), CONTENT_MODEL_WIKITEXT );
+ $page->doQuickEditContent( $content, $wgUser, "testing q" );
+
+ # ---------------------
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertTrue( $content->equals( $page->getContent() ) );
+ }
+
+ /**
+ * @covers WikiPage::doDeleteArticle
+ */
+ public function testDoDeleteArticle() {
+ $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo", CONTENT_MODEL_WIKITEXT );
+ $id = $page->getId();
+
+ $page->doDeleteArticle( "testing deletion" );
+
+ $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" );
+ $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
+ $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" );
+ $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" );
+ $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" );
+
+ $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
+ $this->assertFalse( $t->exists(), "Title::exists should return false after page was deleted" );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::doDeleteUpdates
+ */
+ public function testDoDeleteUpdates() {
+ $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo", CONTENT_MODEL_WIKITEXT );
+ $id = $page->getId();
+
+ $page->doDeleteUpdates( $id );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::getRevision
+ */
+ public function testGetRevision() {
+ $page = $this->newPage( "WikiPageTest_testGetRevision" );
+
+ $rev = $page->getRevision();
+ $this->assertNull( $rev );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $rev = $page->getRevision();
+
+ $this->assertEquals( $page->getLatest(), $rev->getId() );
+ $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
+ }
+
+ /**
+ * @covers WikiPage::getContent
+ */
+ public function testGetContent() {
+ $page = $this->newPage( "WikiPageTest_testGetContent" );
+
+ $content = $page->getContent();
+ $this->assertNull( $content );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $content = $page->getContent();
+ $this->assertEquals( "some text", $content->getNativeData() );
+ }
+
+ /**
+ * @covers WikiPage::getText
+ */
+ public function testGetText() {
+ $this->hideDeprecated( "WikiPage::getText" );
+
+ $page = $this->newPage( "WikiPageTest_testGetText" );
+
+ $text = $page->getText();
+ $this->assertFalse( $text );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $text = $page->getText();
+ $this->assertEquals( "some text", $text );
+ }
+
+ /**
+ * @covers WikiPage::getRawText
+ */
+ public function testGetRawText() {
+ $this->hideDeprecated( "WikiPage::getRawText" );
+
+ $page = $this->newPage( "WikiPageTest_testGetRawText" );
+
+ $text = $page->getRawText();
+ $this->assertFalse( $text );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $text = $page->getRawText();
+ $this->assertEquals( "some text", $text );
+ }
+
+ /**
+ * @covers WikiPage::getContentModel
+ */
+ public function testGetContentModel() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
+ }
+
+ /**
+ * @covers WikiPage::getContentHandler
+ */
+ public function testGetContentHandler() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
+ }
+
+ /**
+ * @covers WikiPage::exists
+ */
+ public function testExists() {
+ $page = $this->newPage( "WikiPageTest_testExists" );
+ $this->assertFalse( $page->exists() );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+ $this->assertTrue( $page->exists() );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertTrue( $page->exists() );
+
+ # -----------------
+ $page->doDeleteArticle( "done testing" );
+ $this->assertFalse( $page->exists() );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertFalse( $page->exists() );
+ }
+
+ public static function provideHasViewableContent() {
+ return array(
+ array( 'WikiPageTest_testHasViewableContent', false, true ),
+ array( 'Special:WikiPageTest_testHasViewableContent', false ),
+ array( 'MediaWiki:WikiPageTest_testHasViewableContent', false ),
+ array( 'Special:Userlogin', true ),
+ array( 'MediaWiki:help', true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHasViewableContent
+ * @covers WikiPage::hasViewableContent
+ */
+ public function testHasViewableContent( $title, $viewable, $create = false ) {
+ $page = $this->newPage( $title );
+ $this->assertEquals( $viewable, $page->hasViewableContent() );
+
+ if ( $create ) {
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+ $this->assertTrue( $page->hasViewableContent() );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertTrue( $page->hasViewableContent() );
+ }
+ }
+
+ public static function provideGetRedirectTarget() {
+ return array(
+ array( 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ),
+ array( 'WikiPageTest_testGetRedirectTarget_2', CONTENT_MODEL_WIKITEXT, "#REDIRECT [[hello world]]", "Hello world" ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetRedirectTarget
+ * @covers WikiPage::getRedirectTarget
+ */
+ public function testGetRedirectTarget( $title, $model, $text, $target ) {
+ $page = $this->createPage( $title, $text, $model );
+
+ # sanity check, because this test seems to fail for no reason for some people.
+ $c = $page->getContent();
+ $this->assertEquals( 'WikitextContent', get_class( $c ) );
+
+ # now, test the actual redirect
+ $t = $page->getRedirectTarget();
+ $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
+ }
+
+ /**
+ * @dataProvider provideGetRedirectTarget
+ * @covers WikiPage::isRedirect
+ */
+ public function testIsRedirect( $title, $model, $text, $target ) {
+ $page = $this->createPage( $title, $text, $model );
+ $this->assertEquals( !is_null( $target ), $page->isRedirect() );
+ }
+
+ public static function provideIsCountable() {
+ return array(
+
+ // any
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '',
+ 'any',
+ true
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'any',
+ true
+ ),
+
+ // comma
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'comma',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo, bar',
+ 'comma',
+ true
+ ),
+
+ // link
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'link',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo [[bar]]',
+ 'link',
+ true
+ ),
+
+ // redirects
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '#REDIRECT [[bar]]',
+ 'any',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '#REDIRECT [[bar]]',
+ 'comma',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '#REDIRECT [[bar]]',
+ 'link',
+ false
+ ),
+
+ // not a content namespace
+ array( 'Talk:WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'any',
+ false
+ ),
+ array( 'Talk:WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo, bar',
+ 'comma',
+ false
+ ),
+ array( 'Talk:WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo [[bar]]',
+ 'link',
+ false
+ ),
+
+ // not a content namespace, different model
+ array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+ null,
+ 'Foo',
+ 'any',
+ false
+ ),
+ array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+ null,
+ 'Foo, bar',
+ 'comma',
+ false
+ ),
+ array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+ null,
+ 'Foo [[bar]]',
+ 'link',
+ false
+ ),
+ );
+ }
+
+
+ /**
+ * @dataProvider provideIsCountable
+ * @covers WikiPage::isCountable
+ */
+ public function testIsCountable( $title, $model, $text, $mode, $expected ) {
+ global $wgContentHandlerUseDB;
+
+ $this->setMwGlobals( 'wgArticleCountMethod', $mode );
+
+ $title = Title::newFromText( $title );
+
+ if ( !$wgContentHandlerUseDB && $model && ContentHandler::getDefaultModelFor( $title ) != $model ) {
+ $this->markTestSkipped( "Can not use non-default content model $model for "
+ . $title->getPrefixedDBkey() . " with \$wgContentHandlerUseDB disabled." );
+ }
+
+ $page = $this->createPage( $title, $text, $model );
+ $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+ array( 'pl_from' => $page->getId() ), __METHOD__ );
+
+ $editInfo = $page->prepareContentForEdit( $page->getContent() );
+
+ $v = $page->isCountable();
+ $w = $page->isCountable( $editInfo );
+
+ $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true )
+ . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
+
+ $this->assertEquals( $expected, $w, "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true )
+ . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
+ }
+
+ public static function provideGetParserOutput() {
+ return array(
+ array( CONTENT_MODEL_WIKITEXT, "hello ''world''\n", "<p>hello <i>world</i></p>" ),
+ // @todo more...?
+ );
+ }
+
+ /**
+ * @dataProvider provideGetParserOutput
+ * @covers WikiPage::getParserOutput
+ */
+ public function testGetParserOutput( $model, $text, $expectedHtml ) {
+ $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text, $model );
+
+ $opt = $page->makeParserOptions( 'canonical' );
+ $po = $page->getParserOutput( $opt );
+ $text = $po->getText();
+
+ $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments
+ $text = preg_replace( '!\s*(</p>)!sm', '\1', $text ); # don't let tidy confuse us
+
+ $this->assertEquals( $expectedHtml, $text );
+
+ return $po;
+ }
+
+ /**
+ * @covers WikiPage::getParserOutput
+ */
+ public function testGetParserOutput_nonexisting() {
+ static $count = 0;
+ $count++;
+
+ $page = new WikiPage( new Title( "WikiPageTest_testGetParserOutput_nonexisting_$count" ) );
+
+ $opt = new ParserOptions();
+ $po = $page->getParserOutput( $opt );
+
+ $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." );
+ }
+
+ /**
+ * @covers WikiPage::getParserOutput
+ */
+ public function testGetParserOutput_badrev() {
+ $page = $this->createPage( 'WikiPageTest_testGetParserOutput', "dummy", CONTENT_MODEL_WIKITEXT );
+
+ $opt = new ParserOptions();
+ $po = $page->getParserOutput( $opt, $page->getLatest() + 1234 );
+
+ // @todo would be neat to also test deleted revision
+
+ $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." );
+ }
+
+ static $sections =
+
+ "Intro
+
+== stuff ==
+hello world
+
+== test ==
+just a test
+
+== foo ==
+more stuff
+";
+
+
+ public function dataReplaceSection() {
+ //NOTE: assume the Help namespace to contain wikitext
+ return array(
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "0",
+ "No more",
+ null,
+ trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) )
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "",
+ "No more",
+ null,
+ "No more"
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "2",
+ "== TEST ==\nmore fun",
+ null,
+ trim( preg_replace( '/^== test ==.*== foo ==/sm',
+ "== TEST ==\nmore fun\n\n== foo ==",
+ WikiPageTest::$sections ) )
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "8",
+ "No more",
+ null,
+ trim( WikiPageTest::$sections )
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "new",
+ "No more",
+ "New",
+ trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikiPage::replaceSection
+ */
+ public function testReplaceSection( $title, $model, $text, $section, $with, $sectionTitle, $expected ) {
+ $this->hideDeprecated( "WikiPage::replaceSection" );
+
+ $page = $this->createPage( $title, $text, $model );
+ $text = $page->replaceSection( $section, $with, $sectionTitle );
+ $text = trim( $text );
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikiPage::replaceSectionContent
+ */
+ public function testReplaceSectionContent( $title, $model, $text, $section, $with, $sectionTitle, $expected ) {
+ $page = $this->createPage( $title, $text, $model );
+
+ $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
+ $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
+
+ $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
+ }
+
+ /* @todo FIXME: fix this!
+ public function testGetUndoText() {
+ $this->checkHasDiff3();
+
+ $text = "one";
+ $page = $this->createPage( "WikiPageTest_testGetUndoText", $text );
+ $rev1 = $page->getRevision();
+
+ $text .= "\n\ntwo";
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two");
+ $rev2 = $page->getRevision();
+
+ $text .= "\n\nthree";
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three");
+ $rev3 = $page->getRevision();
+
+ $text .= "\n\nfour";
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section four");
+ $rev4 = $page->getRevision();
+
+ $text .= "\n\nfive";
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section five");
+ $rev5 = $page->getRevision();
+
+ $text .= "\n\nsix";
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section six");
+ $rev6 = $page->getRevision();
+
+ $undo6 = $page->getUndoText( $rev6 );
+ if ( $undo6 === false ) $this->fail( "getUndoText failed for rev6" );
+ $this->assertEquals( "one\n\ntwo\n\nthree\n\nfour\n\nfive", $undo6 );
+
+ $undo3 = $page->getUndoText( $rev4, $rev2 );
+ if ( $undo3 === false ) $this->fail( "getUndoText failed for rev4..rev2" );
+ $this->assertEquals( "one\n\ntwo\n\nfive", $undo3 );
+
+ $undo2 = $page->getUndoText( $rev2 );
+ if ( $undo2 === false ) $this->fail( "getUndoText failed for rev2" );
+ $this->assertEquals( "one\n\nfive", $undo2 );
+ }
+ */
+
+ /**
+ * @todo FIXME: this is a better rollback test than the one below, but it keeps failing in jenkins for some reason.
+ */
+ public function broken_testDoRollback() {
+ $admin = new User();
+ $admin->setName( "Admin" );
+
+ $text = "one";
+ $page = $this->newPage( "WikiPageTest_testDoRollback" );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+ "section one", EDIT_NEW, false, $admin );
+
+ $user1 = new User();
+ $user1->setName( "127.0.1.11" );
+ $text .= "\n\ntwo";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section two", 0, false, $user1 );
+
+ $user2 = new User();
+ $user2->setName( "127.0.2.13" );
+ $text .= "\n\nthree";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section three", 0, false, $user2 );
+
+ # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing
+ # or not committed under some circumstances. so, make sure the last revision has the right user name.
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->assertEquals( 3, Revision::countByPageId( $dbr, $page->getId() ) );
+
+ $page = new WikiPage( $page->getTitle() );
+ $rev3 = $page->getRevision();
+ $this->assertEquals( '127.0.2.13', $rev3->getUserText() );
+
+ $rev2 = $rev3->getPrevious();
+ $this->assertEquals( '127.0.1.11', $rev2->getUserText() );
+
+ $rev1 = $rev2->getPrevious();
+ $this->assertEquals( 'Admin', $rev1->getUserText() );
+
+ # now, try the actual rollback
+ $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
+ $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user2->getName() ), null );
+ $errors = $page->doRollback( $user2->getName(), "testing revert", $token, false, $details, $admin );
+
+ if ( $errors ) {
+ $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) );
+ }
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
+ "rollback did not revert to the correct revision" );
+ $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
+ }
+
+ /**
+ * @todo FIXME: the above rollback test is better, but it keeps failing in jenkins for some reason.
+ * @covers WikiPage::doRollback
+ */
+ public function testDoRollback() {
+ $admin = new User();
+ $admin->setName( "Admin" );
+
+ $text = "one";
+ $page = $this->newPage( "WikiPageTest_testDoRollback" );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ "section one", EDIT_NEW, false, $admin );
+ $rev1 = $page->getRevision();
+
+ $user1 = new User();
+ $user1->setName( "127.0.1.11" );
+ $text .= "\n\ntwo";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ "adding section two", 0, false, $user1 );
+
+ # now, try the rollback
+ $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
+ $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user1->getName() ), null );
+ $errors = $page->doRollback( $user1->getName(), "testing revert", $token, false, $details, $admin );
+
+ if ( $errors ) {
+ $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) );
+ }
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
+ "rollback did not revert to the correct revision" );
+ $this->assertEquals( "one", $page->getContent()->getNativeData() );
+ }
+
+ public static function provideGetAutosummary() {
+ return array(
+ array(
+ 'Hello there, world!',
+ '#REDIRECT [[Foo]]',
+ 0,
+ '/^Redirected page .*Foo/'
+ ),
+
+ array(
+ null,
+ 'Hello world!',
+ EDIT_NEW,
+ '/^Created page .*Hello/'
+ ),
+
+ array(
+ 'Hello there, world!',
+ '',
+ 0,
+ '/^Blanked/'
+ ),
+
+ array(
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
+ labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et
+ ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
+ 'Hello world!',
+ 0,
+ '/^Replaced .*Hello/'
+ ),
+
+ array(
+ 'foo',
+ 'bar',
+ 0,
+ '/^$/'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAutoSummary
+ * @covers WikiPage::getAutosummary
+ */
+ public function testGetAutosummary( $old, $new, $flags, $expected ) {
+ $this->hideDeprecated( "WikiPage::getAutosummary" );
+
+ $page = $this->newPage( "WikiPageTest_testGetAutosummary" );
+
+ $summary = $page->getAutosummary( $old, $new, $flags );
+
+ $this->assertTrue( (bool)preg_match( $expected, $summary ),
+ "Autosummary didn't match expected pattern $expected: $summary" );
+ }
+
+ public static function provideGetAutoDeleteReason() {
+ return array(
+ array(
+ array(),
+ false,
+ false
+ ),
+
+ array(
+ array(
+ array( "first edit", null ),
+ ),
+ "/first edit.*only contributor/",
+ false
+ ),
+
+ array(
+ array(
+ array( "first edit", null ),
+ array( "second edit", null ),
+ ),
+ "/second edit.*only contributor/",
+ true
+ ),
+
+ array(
+ array(
+ array( "first edit", "127.0.2.22" ),
+ array( "second edit", "127.0.3.33" ),
+ ),
+ "/second edit/",
+ true
+ ),
+
+ array(
+ array(
+ array( "first edit: "
+ . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
+ . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. "
+ . "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea "
+ . "takimata sanctus est Lorem ipsum dolor sit amet.'", null ),
+ ),
+ '/first edit:.*\.\.\."/',
+ false
+ ),
+
+ array(
+ array(
+ array( "first edit", "127.0.2.22" ),
+ array( "", "127.0.3.33" ),
+ ),
+ "/before blanking.*first edit/",
+ true
+ ),
+
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAutoDeleteReason
+ * @covers WikiPage::getAutoDeleteReason
+ */
+ public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
+ global $wgUser;
+
+ //NOTE: assume Help namespace to contain wikitext
+ $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
+
+ $c = 1;
+
+ foreach ( $edits as $edit ) {
+ $user = new User();
+
+ if ( !empty( $edit[1] ) ) {
+ $user->setName( $edit[1] );
+ } else {
+ $user = $wgUser;
+ }
+
+ $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
+
+ $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
+
+ $c += 1;
+ }
+
+ $reason = $page->getAutoDeleteReason( $hasHistory );
+
+ if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
+ $this->assertEquals( $expectedResult, $reason );
+ } else {
+ $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
+ "Autosummary didn't match expected pattern $expectedResult: $reason" );
+ }
+
+ $this->assertEquals( $expectedHistory, $hasHistory,
+ "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
+
+ $page->doDeleteArticle( "done" );
+ }
+
+ public static function providePreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providePreSaveTransform
+ * @covers WikiPage::preSaveTransform
+ */
+ public function testPreSaveTransform( $text, $expected ) {
+ $this->hideDeprecated( 'WikiPage::preSaveTransform' );
+ $user = new User();
+ $user->setName( "127.0.0.1" );
+
+ //NOTE: assume Help namespace to contain wikitext
+ $page = $this->newPage( "Help:WikiPageTest_testPreloadTransform" );
+ $text = $page->preSaveTransform( $text, $user );
+
+ $this->assertEquals( $expected, $text );
+ }
+}
diff --git a/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php
new file mode 100644
index 00000000..2a723e85
--- /dev/null
+++ b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ */
+class WikiPageTest_ContentHandlerUseDB extends WikiPageTest {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgContentHandlerUseDB', false );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $page_table = $dbw->tableName( 'page' );
+ $revision_table = $dbw->tableName( 'revision' );
+ $archive_table = $dbw->tableName( 'archive' );
+
+ if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) {
+ $dbw->query( "alter table $page_table drop column page_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_format" );
+ $dbw->query( "alter table $archive_table drop column ar_content_model" );
+ $dbw->query( "alter table $archive_table drop column ar_content_format" );
+ }
+ }
+
+ /**
+ * @covers WikiPage::getContentModel
+ */
+ public function testGetContentModel() {
+ $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+ $page = new WikiPage( $page->getTitle() );
+
+ // NOTE: since the content model is not recorded in the database,
+ // we expect to get the default, namely CONTENT_MODEL_WIKITEXT
+ $this->assertEquals( CONTENT_MODEL_WIKITEXT, $page->getContentModel() );
+ }
+
+ /**
+ * @covers WikiPage::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+ // NOTE: since the content model is not recorded in the database,
+ // we expect to get the default, namely CONTENT_MODEL_WIKITEXT
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( 'WikitextContentHandler', get_class( $page->getContentHandler() ) );
+ }
+}
diff --git a/tests/phpunit/includes/XmlJsTest.php b/tests/phpunit/includes/XmlJsTest.php
new file mode 100644
index 00000000..161468e2
--- /dev/null
+++ b/tests/phpunit/includes/XmlJsTest.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlJs extends MediaWikiTestCase {
+
+ /**
+ * @covers XmlJsCode::__construct
+ * @dataProvider provideConstruction
+ */
+ public function testConstruction( $value ) {
+ $obj = new XmlJsCode( $value );
+ $this->assertEquals( $value, $obj->value );
+ }
+
+ public function provideConstruction(){
+ return array(
+ array( null ),
+ array( '' ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php
new file mode 100644
index 00000000..56d28b54
--- /dev/null
+++ b/tests/phpunit/includes/XmlSelectTest.php
@@ -0,0 +1,173 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlSelectTest extends MediaWikiTestCase {
+
+ /**
+ * @var XmlSelect
+ */
+ protected $select;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( array(
+ 'wgWellFormedXml' => true,
+ ) );
+ $this->select = new XmlSelect();
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ $this->select = null;
+ }
+
+ /**
+ * @covers XmlSelect::__construct
+ */
+ public function testConstructWithoutParameters() {
+ $this->assertEquals( '<select></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * Parameters are $name (false), $id (false), $default (false)
+ * @dataProvider provideConstructionParameters
+ * @covers XmlSelect::__construct
+ */
+ public function testConstructParameters( $name, $id, $default, $expected ) {
+ $this->select = new XmlSelect( $name, $id, $default );
+ $this->assertEquals( $expected, $this->select->getHTML() );
+ }
+
+ /**
+ * Provide parameters for testConstructParameters() which use three
+ * parameters:
+ * - $name (default: false)
+ * - $id (default: false)
+ * - $default (default: false)
+ * Provides a fourth parameters representing the expected HTML output
+ */
+ public static function provideConstructionParameters() {
+ return array(
+ /**
+ * Values are set following a 3-bit Gray code where two successive
+ * values differ by only one value.
+ * See http://en.wikipedia.org/wiki/Gray_code
+ */
+ # $name $id $default
+ array( false, false, false, '<select></select>' ),
+ array( false, false, 'foo', '<select></select>' ),
+ array( false, 'id', 'foo', '<select id="id"></select>' ),
+ array( false, 'id', false, '<select id="id"></select>' ),
+ array( 'name', 'id', false, '<select name="name" id="id"></select>' ),
+ array( 'name', 'id', 'foo', '<select name="name" id="id"></select>' ),
+ array( 'name', false, 'foo', '<select name="name"></select>' ),
+ array( 'name', false, false, '<select name="name"></select>' ),
+ );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOption() {
+ $this->select->addOption( 'foo' );
+ $this->assertEquals( '<select><option value="foo">foo</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOptionWithDefault() {
+ $this->select->addOption( 'foo', true );
+ $this->assertEquals( '<select><option value="1">foo</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOptionWithFalse() {
+ $this->select->addOption( 'foo', false );
+ $this->assertEquals( '<select><option value="foo">foo</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOptionWithValueZero() {
+ $this->select->addOption( 'foo', 0 );
+ $this->assertEquals( '<select><option value="0">foo</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * @covers XmlSelect::setDefault
+ */
+ public function testSetDefault() {
+ $this->select->setDefault( 'bar1' );
+ $this->select->addOption( 'foo1' );
+ $this->select->addOption( 'bar1' );
+ $this->select->addOption( 'foo2' );
+ $this->assertEquals(
+ '<select><option value="foo1">foo1</option>' . "\n" .
+ '<option value="bar1" selected="">bar1</option>' . "\n" .
+ '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * Adding default later on should set the correct selection or
+ * raise an exception.
+ * To handle this, we need to render the options in getHtml()
+ * @covers XmlSelect::setDefault
+ */
+ public function testSetDefaultAfterAddingOptions() {
+ $this->select->addOption( 'foo1' );
+ $this->select->addOption( 'bar1' );
+ $this->select->addOption( 'foo2' );
+ $this->select->setDefault( 'bar1' ); # setting default after adding options
+ $this->assertEquals(
+ '<select><option value="foo1">foo1</option>' . "\n" .
+ '<option value="bar1" selected="">bar1</option>' . "\n" .
+ '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * @covers XmlSelect::setAttribute
+ * @covers XmlSelect::getAttribute
+ */
+ public function testGetAttributes() {
+ # create some attributes
+ $this->select->setAttribute( 'dummy', 0x777 );
+ $this->select->setAttribute( 'string', 'euro €' );
+ $this->select->setAttribute( 1911, 'razor' );
+
+ # verify we can retrieve them
+ $this->assertEquals(
+ $this->select->getAttribute( 'dummy' ),
+ 0x777
+ );
+ $this->assertEquals(
+ $this->select->getAttribute( 'string' ),
+ 'euro €'
+ );
+ $this->assertEquals(
+ $this->select->getAttribute( 1911 ),
+ 'razor'
+ );
+
+ # inexistant keys should give us 'null'
+ $this->assertEquals(
+ $this->select->getAttribute( 'I DO NOT EXIT' ),
+ null
+ );
+
+ # verify string / integer
+ $this->assertEquals(
+ $this->select->getAttribute( '1911' ),
+ 'razor'
+ );
+ $this->assertEquals(
+ $this->select->getAttribute( 'dummy' ),
+ 0x777
+ );
+ }
+}
diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php
new file mode 100644
index 00000000..8205029f
--- /dev/null
+++ b/tests/phpunit/includes/XmlTest.php
@@ -0,0 +1,402 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $langObj = Language::factory( 'en' );
+ $langObj->setNamespaces( array(
+ -2 => 'Media',
+ -1 => 'Special',
+ 0 => '',
+ 1 => 'Talk',
+ 2 => 'User',
+ 3 => 'User_talk',
+ 4 => 'MyWiki',
+ 5 => 'MyWiki_Talk',
+ 6 => 'File',
+ 7 => 'File_talk',
+ 8 => 'MediaWiki',
+ 9 => 'MediaWiki_talk',
+ 10 => 'Template',
+ 11 => 'Template_talk',
+ 100 => 'Custom',
+ 101 => 'Custom_talk',
+ ) );
+
+ $this->setMwGlobals( array(
+ 'wgLang' => $langObj,
+ 'wgWellFormedXml' => true,
+ ) );
+ }
+
+ /**
+ * @covers Xml::expandAttributes
+ */
+ 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'
+ );
+ }
+
+ /**
+ * @covers Xml::expandAttributes
+ */
+ public function testExpandAttributesException() {
+ $this->setExpectedException( 'MWException' );
+ Xml::expandAttributes( 'string' );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementOpen() {
+ $this->assertEquals(
+ '<element>',
+ Xml::element( 'element', null, null ),
+ 'Opening element with no attributes'
+ );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementEmpty() {
+ $this->assertEquals(
+ '<element />',
+ Xml::element( 'element', null, '' ),
+ 'Terminated empty element'
+ );
+ }
+
+ /**
+ * @covers Xml::input
+ */
+ public function testElementInputCanHaveAValueOfZero() {
+ $this->assertEquals(
+ '<input name="name" value="0" />',
+ Xml::input( 'name', false, 0 ),
+ 'Input with a value of 0 (bug 23797)'
+ );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementEscaping() {
+ $this->assertEquals(
+ '<element>hello &lt;there&gt; you &amp; you</element>',
+ Xml::element( 'element', null, 'hello <there> you & you' ),
+ 'Element with no attributes and content that needs escaping'
+ );
+ }
+
+ /**
+ * @covers Xml::escapeTagsOnly
+ */
+ public function testEscapeTagsOnly() {
+ $this->assertEquals( '&quot;&gt;&lt;', Xml::escapeTagsOnly( '"><' ),
+ 'replace " > and < with their HTML entitites'
+ );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementAttributes() {
+ $this->assertEquals(
+ '<element key="value" <>="&lt;&gt;">',
+ Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ),
+ 'Element attributes, keys are not escaped'
+ );
+ }
+
+ /**
+ * @covers Xml::openElement
+ */
+ public function testOpenElement() {
+ $this->assertEquals(
+ '<element k="v">',
+ Xml::openElement( 'element', array( 'k' => 'v' ) ),
+ 'openElement() shortcut'
+ );
+ }
+
+ /**
+ * @covers Xml::closeElement
+ */
+ public function testCloseElement() {
+ $this->assertEquals( '</element>', Xml::closeElement( 'element' ), 'closeElement() shortcut' );
+ }
+
+ /**
+ * @covers Xml::dateMenu
+ */
+ public function testDateMenu() {
+ $curYear = intval( gmdate( 'Y' ) );
+ $prevYear = $curYear - 1;
+
+ $curMonth = intval( gmdate( 'n' ) );
+ $prevMonth = $curMonth - 1;
+ if ( $prevMonth == 0 ) {
+ $prevMonth = 12;
+ }
+ $nextMonth = $curMonth + 1;
+ if ( $nextMonth == 13 ) {
+ $nextMonth = 1;
+ }
+
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> <input id="year" maxlength="4" size="7" type="number" value="2011" name="year" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" .
+ '<option value="1">January</option>' . "\n" .
+ '<option value="2" selected="">February</option>' . "\n" .
+ '<option value="3">March</option>' . "\n" .
+ '<option value="4">April</option>' . "\n" .
+ '<option value="5">May</option>' . "\n" .
+ '<option value="6">June</option>' . "\n" .
+ '<option value="7">July</option>' . "\n" .
+ '<option value="8">August</option>' . "\n" .
+ '<option value="9">September</option>' . "\n" .
+ '<option value="10">October</option>' . "\n" .
+ '<option value="11">November</option>' . "\n" .
+ '<option value="12">December</option></select>',
+ Xml::dateMenu( 2011, 02 ),
+ "Date menu for february 2011"
+ );
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> <input id="year" maxlength="4" size="7" type="number" value="2011" name="year" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" .
+ '<option value="1">January</option>' . "\n" .
+ '<option value="2">February</option>' . "\n" .
+ '<option value="3">March</option>' . "\n" .
+ '<option value="4">April</option>' . "\n" .
+ '<option value="5">May</option>' . "\n" .
+ '<option value="6">June</option>' . "\n" .
+ '<option value="7">July</option>' . "\n" .
+ '<option value="8">August</option>' . "\n" .
+ '<option value="9">September</option>' . "\n" .
+ '<option value="10">October</option>' . "\n" .
+ '<option value="11">November</option>' . "\n" .
+ '<option value="12">December</option></select>',
+ Xml::dateMenu( 2011, -1 ),
+ "Date menu with negative month for 'All'"
+ );
+ $this->assertEquals(
+ Xml::dateMenu( $curYear, $curMonth ),
+ Xml::dateMenu( '', $curMonth ),
+ "Date menu year is the current one when not specified"
+ );
+
+ $wantedYear = $nextMonth == 1 ? $curYear : $prevYear;
+ $this->assertEquals(
+ Xml::dateMenu( $wantedYear, $nextMonth ),
+ Xml::dateMenu( '', $nextMonth ),
+ "Date menu next month is 11 months ago"
+ );
+
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> <input id="year" maxlength="4" size="7" type="number" name="year" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" .
+ '<option value="1">January</option>' . "\n" .
+ '<option value="2">February</option>' . "\n" .
+ '<option value="3">March</option>' . "\n" .
+ '<option value="4">April</option>' . "\n" .
+ '<option value="5">May</option>' . "\n" .
+ '<option value="6">June</option>' . "\n" .
+ '<option value="7">July</option>' . "\n" .
+ '<option value="8">August</option>' . "\n" .
+ '<option value="9">September</option>' . "\n" .
+ '<option value="10">October</option>' . "\n" .
+ '<option value="11">November</option>' . "\n" .
+ '<option value="12">December</option></select>',
+ Xml::dateMenu( '', '' ),
+ "Date menu with neither year or month"
+ );
+ }
+
+ /**
+ * @covers Xml::textarea
+ */
+ public function testTextareaNoContent() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="40" rows="5"></textarea>',
+ Xml::textarea( 'name', '' ),
+ 'textarea() with not content'
+ );
+ }
+
+ /**
+ * @covers Xml::textarea
+ */
+ public function testTextareaAttribs() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="20" rows="10">&lt;txt&gt;</textarea>',
+ Xml::textarea( 'name', '<txt>', 20, 10 ),
+ 'textarea() with custom attribs'
+ );
+ }
+
+ /**
+ * @covers Xml::label
+ */
+ public function testLabelCreation() {
+ $this->assertEquals(
+ '<label for="id">name</label>',
+ Xml::label( 'name', 'id' ),
+ 'label() with no attribs'
+ );
+ }
+
+ /**
+ * @covers Xml::label
+ */
+ public function testLabelAttributeCanOnlyBeClassOrTitle() {
+ $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" title="nice tooltip">name</label>',
+ Xml::label( 'name', 'id', array( 'title' => 'nice tooltip' ) ),
+ 'label() can get a title attribute'
+ );
+ $this->assertEquals(
+ '<label for="id" class="nice" title="nice tooltip">name</label>',
+ Xml::label( 'name', 'id', array(
+ 'generated' => true,
+ 'class' => 'nice',
+ 'title' => 'nice tooltip',
+ 'anotherattr' => 'value',
+ )
+ ),
+ 'label() skip all attributes but "class" and "title"'
+ );
+ }
+
+ /**
+ * @covers Xml::languageSelector
+ */
+ public function testLanguageSelector() {
+ $select = Xml::languageSelector( 'en', true, null,
+ array( 'id' => 'testlang' ), wfMessage( 'yourlanguage' ) );
+ $this->assertEquals(
+ '<label for="testlang">Language:</label>',
+ $select[0]
+ );
+ }
+
+ /**
+ * @covers Xml::escapeJsString
+ */
+ public function testEscapeJsStringSpecialChars() {
+ $this->assertEquals(
+ '\\\\\r\n',
+ Xml::escapeJsString( "\\\r\n" ),
+ 'escapeJsString() with special characters'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarBoolean() {
+ $this->assertEquals(
+ 'true',
+ Xml::encodeJsVar( true ),
+ 'encodeJsVar() with boolean'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarNull() {
+ $this->assertEquals(
+ 'null',
+ Xml::encodeJsVar( null ),
+ 'encodeJsVar() with null'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarObject() {
+ $this->assertEquals(
+ '{"a":"a","b":1}',
+ Xml::encodeJsVar( (object)array( 'a' => 'a', 'b' => 1 ) ),
+ 'encodeJsVar() with object'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarInt() {
+ $this->assertEquals(
+ '123456',
+ Xml::encodeJsVar( 123456 ),
+ 'encodeJsVar() with int'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarFloat() {
+ $this->assertEquals(
+ '1.23456',
+ Xml::encodeJsVar( 1.23456 ),
+ 'encodeJsVar() with float'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarIntString() {
+ $this->assertEquals(
+ '"123456"',
+ Xml::encodeJsVar( '123456' ),
+ 'encodeJsVar() with int-like string'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarFloatString() {
+ $this->assertEquals(
+ '"1.23456"',
+ Xml::encodeJsVar( '1.23456' ),
+ 'encodeJsVar() with float-like string'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/XmlTypeCheckTest.php b/tests/phpunit/includes/XmlTypeCheckTest.php
new file mode 100644
index 00000000..8d6f1ed7
--- /dev/null
+++ b/tests/phpunit/includes/XmlTypeCheckTest.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * PHPUnit tests for XMLTypeCheck.
+ * @author physikerwelt
+ * @group Xml
+ * @covers XMLTypeCheck
+ */
+class XmlTypeCheckTest extends MediaWikiTestCase {
+ const WELL_FORMED_XML = "<root><child /></root>";
+ const MAL_FORMED_XML = "<root><child /></error>";
+
+ /**
+ * @covers XMLTypeCheck::newFromString
+ * @covers XMLTypeCheck::getRootElement
+ */
+ public function testWellFormedXML() {
+ $testXML = XmlTypeCheck::newFromString( self::WELL_FORMED_XML );
+ $this->assertTrue( $testXML->wellFormed );
+ $this->assertEquals( 'root', $testXML->getRootElement() );
+ }
+
+ /**
+ * @covers XMLTypeCheck::newFromString
+ */
+ public function testMalFormedXML() {
+ $testXML = XmlTypeCheck::newFromString( self::MAL_FORMED_XML );
+ $this->assertFalse( $testXML->wellFormed );
+ }
+
+}
diff --git a/tests/phpunit/includes/ZipDirectoryReaderTest.php b/tests/phpunit/includes/ZipDirectoryReaderTest.php
new file mode 100644
index 00000000..2627a417
--- /dev/null
+++ b/tests/phpunit/includes/ZipDirectoryReaderTest.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @covers ZipDirectoryReader
+ * NOTE: this test is more like an integration test than a unit test
+ */
+class ZipDirectoryReaderTest extends MediaWikiTestCase {
+ protected $zipDir;
+ protected $entries;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->zipDir = __DIR__ . '/../data/zip';
+ }
+
+ function zipCallback( $entry ) {
+ $this->entries[] = $entry;
+ }
+
+ function readZipAssertError( $file, $error, $assertMessage ) {
+ $this->entries = array();
+ $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) );
+ $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
+ }
+
+ function readZipAssertSuccess( $file, $assertMessage ) {
+ $this->entries = array();
+ $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) );
+ $this->assertTrue( $status->isOK(), $assertMessage );
+ }
+
+ public function testEmpty() {
+ $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+ }
+
+ public function testMultiDisk0() {
+ $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+ 'Split zip error' );
+ }
+
+ public function testNoSignature() {
+ $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+ 'No signature should give "wrong format" error' );
+ }
+
+ public function testSimple() {
+ $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+ $this->assertEquals( $this->entries, array( array(
+ 'name' => 'Class.class',
+ 'mtime' => '20010115000000',
+ 'size' => 1,
+ ) ) );
+ }
+
+ public function testBadCentralEntrySignature() {
+ $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+ 'Bad central entry error' );
+ }
+
+ public function testTrailingBytes() {
+ $this->readZipAssertError( 'trail.zip', 'zip-bad',
+ 'Trailing bytes error' );
+ }
+
+ public function testWrongCDStart() {
+ $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+ 'Wrong CD start disk error' );
+ }
+
+
+ public function testCentralDirectoryGap() {
+ $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+ 'CD gap error' );
+ }
+
+ public function testCentralDirectoryTruncated() {
+ $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+ 'CD truncated error (should hit unpack() overrun)' );
+ }
+
+ public function testLooksLikeZip64() {
+ $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
+ 'A file which looks like ZIP64 but isn\'t, should give error' );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiAccountCreationTest.php b/tests/phpunit/includes/api/ApiAccountCreationTest.php
new file mode 100644
index 00000000..68f80ac9
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiAccountCreationTest.php
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * @group Database
+ * @group API
+ * @group medium
+ */
+class ApiCreateAccountTest extends ApiTestCase {
+ function setUp() {
+ parent::setUp();
+ LoginForm::setCreateaccountToken();
+ $this->setMwGlobals( array( 'wgEnableEmail' => true ) );
+ }
+
+ /**
+ * Test the account creation API with a valid request. Also
+ * make sure the new account can log in and is valid.
+ *
+ * This test does multiple API requests so it might end up being
+ * a bit slow. Raise the default timeout.
+ * @group medium
+ */
+ public function testValid() {
+ global $wgServer;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+
+ $password = User::randomPassword();
+
+ $ret = $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'Apitestnew',
+ 'password' => $password,
+ 'email' => 'test@domain.test',
+ 'realname' => 'Test Name'
+ ) );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( 'bool', $result );
+ $this->assertNotInternalType( 'null', $result['createaccount'] );
+
+ // Should first ask for token.
+ $a = $result['createaccount'];
+ $this->assertEquals( 'needtoken', $a['result'] );
+ $token = $a['token'];
+
+ // Finally create the account
+ $ret = $this->doApiRequest(
+ array(
+ 'action' => 'createaccount',
+ 'name' => 'Apitestnew',
+ 'password' => $password,
+ 'token' => $token,
+ 'email' => 'test@domain.test',
+ 'realname' => 'Test Name'
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( 'bool', $result );
+ $this->assertEquals( 'success', $result['createaccount']['result'] );
+
+ // Try logging in with the new user.
+ $ret = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => 'Apitestnew',
+ 'lgpassword' => $password,
+ ) );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( 'bool', $result );
+ $this->assertNotInternalType( 'null', $result['login'] );
+
+ $a = $result['login']['result'];
+ $this->assertEquals( 'NeedToken', $a );
+ $token = $result['login']['token'];
+
+ $ret = $this->doApiRequest(
+ array(
+ 'action' => 'login',
+ 'lgtoken' => $token,
+ 'lgname' => 'Apitestnew',
+ 'lgpassword' => $password,
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( 'bool', $result );
+ $a = $result['login']['result'];
+
+ $this->assertEquals( 'Success', $a );
+
+ // log out to destroy the session
+ $ret = $this->doApiRequest(
+ array(
+ 'action' => 'logout',
+ ),
+ $ret[2]
+ );
+ $this->assertEquals( array(), $ret[0] );
+ }
+
+ /**
+ * Make sure requests with no names are invalid.
+ * @expectedException UsageException
+ */
+ public function testNoName() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'token' => LoginForm::getCreateaccountToken(),
+ 'password' => 'password',
+ ) );
+ }
+
+ /**
+ * Make sure requests with no password are invalid.
+ * @expectedException UsageException
+ */
+ public function testNoPassword() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'testName',
+ 'token' => LoginForm::getCreateaccountToken(),
+ ) );
+ }
+
+ /**
+ * Make sure requests with existing users are invalid.
+ * @expectedException UsageException
+ */
+ public function testExistingUser() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'Apitestsysop',
+ 'token' => LoginForm::getCreateaccountToken(),
+ 'password' => 'password',
+ 'email' => 'test@domain.test',
+ ) );
+ }
+
+ /**
+ * Make sure requests with invalid emails are invalid.
+ * @expectedException UsageException
+ */
+ public function testInvalidEmail() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'Test User',
+ 'token' => LoginForm::getCreateaccountToken(),
+ 'password' => 'password',
+ 'email' => 'invalid',
+ ) );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php
new file mode 100644
index 00000000..8afb748a
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiBlockTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiBlockTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function getTokens() {
+ return $this->getTokenList( self::$users['sysop'] );
+ }
+
+ function addDBData() {
+ $user = User::newFromName( 'UTApiBlockee' );
+
+ if ( $user->getId() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTApiBlockeePassword' );
+
+ $user->saveSettings();
+ }
+ }
+
+ /**
+ * This test has probably always been broken and use an invalid token
+ * Bug tracking brokenness is https://bugzilla.wikimedia.org/35646
+ *
+ * Root cause is https://gerrit.wikimedia.org/r/3434
+ * Which made the Block/Unblock API to actually verify the token
+ * previously always considered valid (bug 34212).
+ */
+ public function testMakeNormalBlock() {
+ $tokens = $this->getTokens();
+
+ $user = User::newFromName( 'UTApiBlockee' );
+
+ if ( !$user->getId() ) {
+ $this->markTestIncomplete( "The user UTApiBlockee does not exist" );
+ }
+
+ if ( !array_key_exists( 'blocktoken', $tokens ) ) {
+ $this->markTestIncomplete( "No block token found" );
+ }
+
+ $this->doApiRequest( array(
+ 'action' => 'block',
+ 'user' => 'UTApiBlockee',
+ 'reason' => 'Some reason',
+ 'token' => $tokens['blocktoken'] ), null, false, self::$users['sysop']->user );
+
+ $block = Block::newFromTarget( 'UTApiBlockee' );
+
+ $this->assertTrue( !is_null( $block ), 'Block is valid' );
+
+ $this->assertEquals( 'UTApiBlockee', (string)$block->getTarget() );
+ $this->assertEquals( 'Some reason', $block->mReason );
+ $this->assertEquals( 'infinity', $block->mExpiry );
+ }
+
+ /**
+ * Attempting to block without a token should give a UsageException with
+ * error message:
+ * "The token parameter must be set"
+ *
+ * @dataProvider provideBlockUnblockAction
+ * @expectedException UsageException
+ */
+ public function testBlockingActionWithNoToken( $action ) {
+ $this->doApiRequest(
+ array(
+ 'action' => $action,
+ 'user' => 'UTApiBlockee',
+ 'reason' => 'Some reason',
+ ),
+ null,
+ false,
+ self::$users['sysop']->user
+ );
+ }
+
+ /**
+ * Just provide the 'block' and 'unblock' action to test both API calls
+ */
+ public static function provideBlockUnblockAction() {
+ return array(
+ array( 'block' ),
+ array( 'unblock' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php
new file mode 100644
index 00000000..0c49b12b
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiEditPageTest.php
@@ -0,0 +1,417 @@
+<?php
+
+/**
+ * Tests for MediaWiki api.php?action=edit.
+ *
+ * @author Daniel Kinzler
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiEditPageTest extends ApiTestCase {
+
+ public function setUp() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::setUp();
+
+ $wgExtraNamespaces[12312] = 'Dummy';
+ $wgExtraNamespaces[12313] = 'Dummy_talk';
+
+ $wgNamespaceContentModels[12312] = "testing";
+ $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+
+ $this->doLogin();
+ }
+
+ public function tearDown() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ unset( $wgExtraNamespaces[12312] );
+ unset( $wgExtraNamespaces[12313] );
+
+ unset( $wgNamespaceContentModels[12312] );
+ unset( $wgContentHandlers["testing"] );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+
+ parent::tearDown();
+ }
+
+ public function testEdit() {
+ $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
+
+ // -- test new page --------------------------------------------
+ $apiResult = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ) );
+ $apiResult = $apiResult[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // -- test existing page, no change ----------------------------
+ $data = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ) );
+
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
+ $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
+
+ // -- test existing page, with change --------------------------
+ $data = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'different text'
+ ) );
+
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
+
+ $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
+ $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
+ $this->assertNotEquals(
+ $data[0]['edit']['newrevid'],
+ $data[0]['edit']['oldrevid'],
+ "revision id should change after edit"
+ );
+ }
+
+ public function testNonTextEdit() {
+ $name = 'Dummy:ApiEditPageTest_testNonTextEdit';
+ $data = serialize( 'some bla bla text' );
+
+ // -- test new page --------------------------------------------
+ $apiResult = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $data, ) );
+ $apiResult = $apiResult[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // validate resulting revision
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $this->assertEquals( "testing", $page->getContentModel() );
+ $this->assertEquals( $data, $page->getContent()->serialize() );
+ }
+
+ public static function provideEditAppend() {
+ return array(
+ array( #0: append
+ 'foo', 'append', 'bar', "foobar"
+ ),
+ array( #1: prepend
+ 'foo', 'prepend', 'bar', "barfoo"
+ ),
+ array( #2: append to empty page
+ '', 'append', 'foo', "foo"
+ ),
+ array( #3: prepend to empty page
+ '', 'prepend', 'foo', "foo"
+ ),
+ array( #4: append to non-existing page
+ null, 'append', 'foo', "foo"
+ ),
+ array( #5: prepend to non-existing page
+ null, 'prepend', 'foo', "foo"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideEditAppend
+ */
+ public function testEditAppend( $text, $op, $append, $expected ) {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditAppend_$count";
+
+ // -- create page (or not) -----------------------------------------
+ if ( $text !== null ) {
+ if ( $text === '' ) {
+ // can't create an empty page, so create it with some content
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => '(dummy)', ) );
+ }
+
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $text, ) );
+
+ $this->assertEquals( 'Success', $re['edit']['result'] ); // sanity
+ }
+
+ // -- try append/prepend --------------------------------------------
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ $op . 'text' => $append, ) );
+
+ $this->assertEquals( 'Success', $re['edit']['result'] );
+
+ // -- validate -----------------------------------------------------
+ $page = new WikiPage( Title::newFromText( $name ) );
+ $content = $page->getContent();
+ $this->assertNotNull( $content, 'Page should have been created' );
+
+ $text = $content->getNativeData();
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ /**
+ * Test editing of sections
+ */
+ public function testEditSection() {
+ $name = 'Help:ApiEditPageTest_testEditSection';
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
+ // Preload the page with some text
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), 'summary' );
+
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => '1',
+ 'text' => "==section 1==\nnew content 1",
+ ) );
+ $this->assertEquals( 'Success', $re['edit']['result'] );
+ $newtext = WikiPage::factory( Title::newFromText( $name) )->getContent( Revision::RAW )->getNativeData();
+ $this->assertEquals( $newtext, "==section 1==\nnew content 1\n\n==section 2==\ncontent2" );
+
+ // Test that we raise a 'nosuchsection' error
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => '9999',
+ 'text' => 'text',
+ ) );
+ $this->fail( "Should have raised a UsageException" );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( $e->getCodeString(), 'nosuchsection' );
+ }
+ }
+
+ /**
+ * Test action=edit&section=new
+ * Run it twice so we test adding a new section on a
+ * page that doesn't exist (bug 52830) and one that
+ * does exist
+ */
+ public function testEditNewSection() {
+ $name = 'Help:ApiEditPageTest_testEditNewSection';
+
+ // Test on a page that does not already exist
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => 'new',
+ 'text' => 'test',
+ 'summary' => 'header',
+ ));
+
+ $this->assertEquals( 'Success', $re['edit']['result'] );
+ // Check the page text is correct
+ $text = WikiPage::factory( Title::newFromText( $name ) )->getContent( Revision::RAW )->getNativeData();
+ $this->assertEquals( $text, "== header ==\n\ntest" );
+
+ // Now on one that does
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ list( $re2 ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => 'new',
+ 'text' => 'test',
+ 'summary' => 'header',
+ ));
+
+ $this->assertEquals( 'Success', $re2['edit']['result'] );
+ $text = WikiPage::factory( Title::newFromText( $name ) )->getContent( Revision::RAW )->getNativeData();
+ $this->assertEquals( $text, "== header ==\n\ntest\n\n== header ==\n\ntest" );
+ }
+
+ public function testEditConflict() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_$count";
+ $title = Title::newFromText( $name );
+
+ $page = WikiPage::factory( $title );
+
+ // base edit
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // conflicting edit
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $page, '20120101020202' );
+
+ // try to save edit, expect conflict
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ ), null, self::$users['sysop']->user );
+
+ $this->fail( 'edit conflict expected' );
+ } catch ( UsageException $ex ) {
+ $this->assertEquals( 'editconflict', $ex->getCodeString() );
+ }
+ }
+
+ public function testEditConflict_redirect() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_redirect_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEditConflict_redirect_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // conflicting edit to redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit; should work, because we follow the redirect
+ list( $re, , ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'redirect' => true,
+ ), null, self::$users['sysop']->user );
+
+ $this->assertEquals( 'Success', $re['edit']['result'],
+ "no edit conflict expected when following redirect" );
+
+ // try again, without following the redirect. Should fail.
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ ), null, self::$users['sysop']->user );
+
+ $this->fail( 'edit conflict expected' );
+ } catch ( UsageException $ex ) {
+ $this->assertEquals( 'editconflict', $ex->getCodeString() );
+ }
+ }
+
+ public function testEditConflict_bug41990() {
+ static $count = 0;
+ $count++;
+
+ /*
+ * bug 41990: if the target page has a newer revision than the redirect, then editing the
+ * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
+ * caused an edit conflict to be detected.
+ */
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+ $baseTime = $rpage->getRevision()->getTimestamp();
+
+ // new edit to content
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit; should work, following the redirect.
+ list( $re, , ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'redirect' => true,
+ ), null, self::$users['sysop']->user );
+
+ $this->assertEquals( 'Success', $re['edit']['result'],
+ "no edit conflict expected here" );
+ }
+
+ protected function forceRevisionDate( WikiPage $page, $timestamp ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'revision',
+ array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ),
+ array( 'rev_id' => $page->getLatest() ) );
+
+ $page->clear();
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php
new file mode 100644
index 00000000..ad1e73ab
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiOptionsTest.php
@@ -0,0 +1,420 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiOptionsTest extends MediaWikiLangTestCase {
+
+ private $mTested, $mUserMock, $mContext, $mSession;
+
+ private $mOldGetPreferencesHooks = false;
+
+ private static $Success = array( 'options' => 'success' );
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mUserMock = $this->getMockBuilder( 'User' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // Set up groups and rights
+ $this->mUserMock->expects( $this->any() )
+ ->method( 'getEffectiveGroups' )->will( $this->returnValue( array( '*', 'user' ) ) );
+ $this->mUserMock->expects( $this->any() )
+ ->method( 'isAllowed' )->will( $this->returnValue( true ) );
+
+ // Set up callback for User::getOptionKinds
+ $this->mUserMock->expects( $this->any() )
+ ->method( 'getOptionKinds' )->will( $this->returnCallback( array( $this, 'getOptionKinds' ) ) );
+
+ // Create a new context
+ $this->mContext = new DerivativeContext( new RequestContext() );
+ $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) );
+ $this->mContext->setUser( $this->mUserMock );
+
+ $main = new ApiMain( $this->mContext );
+
+ // Empty session
+ $this->mSession = array();
+
+ $this->mTested = new ApiOptions( $main, 'options' );
+
+ global $wgHooks;
+ if ( !isset( $wgHooks['GetPreferences'] ) ) {
+ $wgHooks['GetPreferences'] = array();
+ }
+ $this->mOldGetPreferencesHooks = $wgHooks['GetPreferences'];
+ $wgHooks['GetPreferences'][] = array( $this, 'hookGetPreferences' );
+ }
+
+ protected function tearDown() {
+ global $wgHooks;
+
+ if ( $this->mOldGetPreferencesHooks !== false ) {
+ $wgHooks['GetPreferences'] = $this->mOldGetPreferencesHooks;
+ $this->mOldGetPreferencesHooks = false;
+ }
+
+ parent::tearDown();
+ }
+
+ public function hookGetPreferences( $user, &$preferences ) {
+ $preferences = array();
+
+ foreach ( array( 'name', 'willBeNull', 'willBeEmpty', 'willBeHappy' ) as $k ) {
+ $preferences[$k] = array(
+ 'type' => 'text',
+ 'section' => 'test',
+ 'label' => '&#160;',
+ );
+ }
+
+ $preferences['testmultiselect'] = array(
+ 'type' => 'multiselect',
+ 'options' => array(
+ 'Test' => array(
+ '<span dir="auto">Some HTML here for option 1</span>' => 'opt1',
+ '<span dir="auto">Some HTML here for option 2</span>' => 'opt2',
+ '<span dir="auto">Some HTML here for option 3</span>' => 'opt3',
+ '<span dir="auto">Some HTML here for option 4</span>' => 'opt4',
+ ),
+ ),
+ 'section' => 'test',
+ 'label' => '&#160;',
+ 'prefix' => 'testmultiselect-',
+ 'default' => array(),
+ );
+
+ return true;
+ }
+
+ public function getOptionKinds( IContextSource $context, $options = null ) {
+ // Match with above.
+ $kinds = array(
+ 'name' => 'registered',
+ 'willBeNull' => 'registered',
+ 'willBeEmpty' => 'registered',
+ 'willBeHappy' => 'registered',
+ 'testmultiselect-opt1' => 'registered-multiselect',
+ 'testmultiselect-opt2' => 'registered-multiselect',
+ 'testmultiselect-opt3' => 'registered-multiselect',
+ 'testmultiselect-opt4' => 'registered-multiselect',
+ );
+
+ if ( $options === null ) {
+ return $kinds;
+ }
+
+ $mapping = array();
+ foreach ( $options as $key => $value ) {
+ if ( isset( $kinds[$key] ) ) {
+ $mapping[$key] = $kinds[$key];
+ } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
+ $mapping[$key] = 'userjs';
+ } else {
+ $mapping[$key] = 'unused';
+ }
+ }
+
+ return $mapping;
+ }
+
+ private function getSampleRequest( $custom = array() ) {
+ $request = array(
+ 'token' => '123ABC',
+ 'change' => null,
+ 'optionname' => null,
+ 'optionvalue' => null,
+ );
+
+ return array_merge( $request, $custom );
+ }
+
+ private function executeQuery( $request ) {
+ $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) );
+ $this->mTested->execute();
+
+ return $this->mTested->getResult()->getData();
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ public function testNoToken() {
+ $request = $this->getSampleRequest( array( 'token' => null ) );
+
+ $this->executeQuery( $request );
+ }
+
+ public function testAnon() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'isAnon' )
+ ->will( $this->returnValue( true ) );
+
+ try {
+ $request = $this->getSampleRequest();
+
+ $this->executeQuery( $request );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'notloggedin', $e->getCodeString() );
+ $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() );
+
+ return;
+ }
+ $this->fail( "UsageException was not thrown" );
+ }
+
+ public function testNoOptionname() {
+ try {
+ $request = $this->getSampleRequest( array( 'optionvalue' => '1' ) );
+
+ $this->executeQuery( $request );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'nooptionname', $e->getCodeString() );
+ $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() );
+
+ return;
+ }
+ $this->fail( "UsageException was not thrown" );
+ }
+
+ public function testNoChanges() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'setOption' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'saveSettings' );
+
+ try {
+ $request = $this->getSampleRequest();
+
+ $this->executeQuery( $request );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'nochanges', $e->getCodeString() );
+ $this->assertEquals( 'No changes were requested', $e->getMessage() );
+
+ return;
+ }
+ $this->fail( "UsageException was not thrown" );
+ }
+
+ public function testReset() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'resetOptions' )
+ ->with( $this->equalTo( array( 'all' ) ) );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'setOption' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'reset' => '' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testResetKinds() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'resetOptions' )
+ ->with( $this->equalTo( array( 'registered' ) ) );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'setOption' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'reset' => '', 'resetkinds' => 'registered' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testOptionWithValue() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'optionname' => 'name', 'optionvalue' => 'value' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testOptionResetValue() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'name' ), $this->identicalTo( null ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'optionname' => 'name' ) );
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testChange() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 2 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 4 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) );
+
+ $this->mUserMock->expects( $this->at( 5 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 6 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) );
+
+ $this->mUserMock->expects( $this->at( 7 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 8 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testResetChangeOption() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 4 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 5 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) );
+
+ $this->mUserMock->expects( $this->at( 6 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 7 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $args = array(
+ 'reset' => '',
+ 'change' => 'willBeHappy=Happy',
+ 'optionname' => 'name',
+ 'optionvalue' => 'value'
+ );
+
+ $response = $this->executeQuery( $this->getSampleRequest( $args ) );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testMultiSelect() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 3 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) );
+
+ $this->mUserMock->expects( $this->at( 4 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) );
+
+ $this->mUserMock->expects( $this->at( 5 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) );
+
+ $this->mUserMock->expects( $this->at( 6 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'testmultiselect-opt1=1|testmultiselect-opt2|testmultiselect-opt3=|testmultiselect-opt4=0'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testUnknownOption() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'unknownOption=1'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( array(
+ 'options' => 'success',
+ 'warnings' => array(
+ 'options' => array(
+ '*' => "Validation error for 'unknownOption': not a valid preference"
+ )
+ )
+ ), $response );
+ }
+
+ public function testUserjsOption() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 3 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'userjs-option' ), $this->equalTo( '1' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'userjs-option=1'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php
new file mode 100644
index 00000000..2d714e65
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiParseTest.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiParseTest extends ApiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ public function testParseNonexistentPage() {
+ $somePage = mt_rand();
+
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'parse',
+ 'page' => $somePage ) );
+
+ $this->fail( "API did not return an error when parsing a nonexistent page" );
+ } catch ( UsageException $ex ) {
+ $this->assertEquals( 'missingtitle', $ex->getCodeString(),
+ "Parse request for nonexistent page must give 'missingtitle' error: " . var_export( $ex->getMessageArray(), true ) );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php
new file mode 100644
index 00000000..28b5ff4d
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiPurgeTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiPurgeTest extends ApiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ /**
+ * @group Broken
+ */
+ public function testPurgeMainPage() {
+ if ( !Title::newFromText( 'UTPage' )->exists() ) {
+ $this->markTestIncomplete( "The article [[UTPage]] does not exist" );
+ }
+
+ $somePage = mt_rand();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'purge',
+ 'titles' => 'UTPage|' . $somePage . '|%5D' ) );
+
+ $this->assertArrayHasKey( 'purge', $data[0],
+ "Must receive a 'purge' result from API" );
+
+ $this->assertEquals( 3, count( $data[0]['purge'] ),
+ "Purge request for three articles should give back three results received: " . var_export( $data[0]['purge'], true ) );
+
+ $pages = array( 'UTPage' => 'purged', $somePage => 'missing', '%5D' => 'invalid' );
+ foreach ( $data[0]['purge'] as $v ) {
+ $this->assertArrayHasKey( $pages[$v['title']], $v );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php
new file mode 100644
index 00000000..472f8c4a
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTest.php
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiTest extends ApiTestCase {
+
+ public function testRequireOnlyOneParameterDefault() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => false ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ public function testRequireOnlyOneParameterZero() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => 0 ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ public 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
+ */
+ public 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->assertNotInternalType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ }
+
+ /**
+ * Test result of attempted login with an empty username
+ */
+ public function testApiLoginNoName() {
+ $data = $this->doApiRequest( array( 'action' => 'login',
+ 'lgname' => '', 'lgpassword' => self::$users['sysop']->password,
+ ) );
+ $this->assertEquals( 'NoName', $data[0]['login']['result'] );
+ }
+
+ public function testApiLoginBadPass() {
+ global $wgServer;
+
+ $user = self::$users['sysop'];
+ $user->user->logOut();
+
+ 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->assertNotInternalType( "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" => "badnowayinhell",
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "WrongPass", $a );
+ }
+
+ public function testApiLoginGoodPass() {
+ global $wgServer;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+
+ $user = self::$users['sysop'];
+ $user->user->logOut();
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgname" => $user->username,
+ "lgpassword" => $user->password,
+ )
+ );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( "bool", $result );
+ $this->assertNotInternalType( "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,
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "Success", $a );
+ }
+
+ /**
+ * @group Broken
+ */
+ public 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' );
+ }
+ $user = self::$users['sysop'];
+
+ $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml",
+ array( "method" => "POST",
+ "postData" => array(
+ "lgname" => $user->username,
+ "lgpassword" => $user->password
+ )
+ )
+ );
+ $req->execute();
+
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $req->getContent() );
+ $this->assertNotInternalType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ $this->assertNotInternalType( "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" => $user->username,
+ "lgpassword" => $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=' . $user->userName . '; .*Token=/', $serializedCookie );
+
+ return $cj;
+ }
+
+ public function testRunLogin() {
+ $sysopUser = self::$users['sysop'];
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => $sysopUser->username,
+ 'lgpassword' => $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" => $sysopUser->username,
+ "lgpassword" => $sysopUser->password ), $data[2] );
+
+ $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;
+ }
+
+ public function testGettingToken() {
+ foreach ( self::$users as $user ) {
+ $this->runTokenTest( $user );
+ }
+ }
+
+ function runTokenTest( $user ) {
+ $tokens = $this->getTokenList( $user );
+
+ $rights = $user->user->getRights();
+
+ $this->assertArrayHasKey( 'edittoken', $tokens );
+ $this->assertArrayHasKey( 'movetoken', $tokens );
+
+ if ( isset( $rights['delete'] ) ) {
+ $this->assertArrayHasKey( 'deletetoken', $tokens );
+ }
+
+ if ( isset( $rights['block'] ) ) {
+ $this->assertArrayHasKey( 'blocktoken', $tokens );
+ $this->assertArrayHasKey( 'unblocktoken', $tokens );
+ }
+
+ if ( isset( $rights['protect'] ) ) {
+ $this->assertArrayHasKey( 'protecttoken', $tokens );
+ }
+
+ return $tokens;
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php
new file mode 100644
index 00000000..94ef9c68
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestCase.php
@@ -0,0 +1,253 @@
+<?php
+
+abstract class ApiTestCase extends MediaWikiLangTestCase {
+ protected static $apiUrl;
+
+ /**
+ * @var ApiTestContext
+ */
+ protected $apiContext;
+
+ protected function setUp() {
+ global $wgServer;
+
+ parent::setUp();
+ self::$apiUrl = $wgServer . wfScript( 'api' );
+
+ ApiQueryInfo::resetTokenCache(); // tokens are invalid because we cleared the session
+
+ self::$users = array(
+ 'sysop' => new TestUser(
+ 'Apitestsysop',
+ 'Api Test Sysop',
+ 'api_test_sysop@example.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new TestUser(
+ 'Apitestuser',
+ 'Api Test User',
+ 'api_test_user@example.com',
+ array()
+ )
+ );
+
+ $this->setMwGlobals( array(
+ 'wgMemc' => new EmptyBagOStuff(),
+ 'wgAuth' => new StubObject( 'wgAuth', 'AuthPlugin' ),
+ 'wgRequest' => new FauxRequest( array() ),
+ 'wgUser' => self::$users['sysop']->user,
+ ) );
+
+ $this->apiContext = new ApiTestContext();
+ }
+
+ /**
+ * Edits or creates a page/revision
+ * @param $pageName string page title
+ * @param $text string content of the page
+ * @param $summary string optional summary string for the revision
+ * @param $defaultNs int optional namespace id
+ * @return array as returned by WikiPage::doEditContent()
+ */
+ protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
+ $title = Title::newFromText( $pageName, $defaultNs );
+ $page = WikiPage::factory( $title );
+
+ return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
+ }
+
+ /**
+ * Does the API request and returns the result.
+ *
+ * The returned value is an array containing
+ * - the result data (array)
+ * - the request (WebRequest)
+ * - the session data of the request (array)
+ * - if $appendModule is true, the Api module $module
+ *
+ * @param array $params
+ * @param array|null $session
+ * @param bool $appendModule
+ * @param User|null $user
+ *
+ * @return array
+ */
+ protected function doApiRequest( array $params, array $session = null, $appendModule = false, User $user = null ) {
+ global $wgRequest, $wgUser;
+
+ if ( is_null( $session ) ) {
+ // re-use existing global session by default
+ $session = $wgRequest->getSessionArray();
+ }
+
+ // set up global environment
+ if ( $user ) {
+ $wgUser = $user;
+ }
+
+ $wgRequest = new FauxRequest( $params, true, $session );
+ RequestContext::getMain()->setRequest( $wgRequest );
+
+ // set up local environment
+ $context = $this->apiContext->newTestContext( $wgRequest, $wgUser );
+
+ $module = new ApiMain( $context, true );
+
+ // run it!
+ $module->execute();
+
+ // construct result
+ $results = array(
+ $module->getResultData(),
+ $context->getRequest(),
+ $context->getRequest()->getSessionArray()
+ );
+
+ if ( $appendModule ) {
+ $results[] = $module;
+ }
+
+ return $results;
+ }
+
+ /**
+ * 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 Array: key-value API params
+ * @param $session Array|null: session array
+ * @param $user User|null A User object for the context
+ * @return result of the API call
+ * @throws Exception in case wsToken is not set in the session
+ */
+ protected function doApiRequestWithToken( array $params, array $session = null, User $user = null ) {
+ global $wgRequest;
+
+ if ( $session === null ) {
+ $session = $wgRequest->getSessionArray();
+ }
+
+ if ( $session['wsToken'] ) {
+ // add edit token to fake session
+ $session['wsEditToken'] = $session['wsToken'];
+ // add token to request parameters
+ $params['token'] = md5( $session['wsToken'] ) . User::EDIT_TOKEN_SUFFIX;
+
+ return $this->doApiRequest( $params, $session, false, $user );
+ } else {
+ throw new Exception( "request data not in right format" );
+ }
+ }
+
+ protected function doLogin( $user = 'sysop' ) {
+ if ( !array_key_exists( $user, self::$users ) ) {
+ throw new MWException( "Can not log in to undefined user $user" );
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => self::$users[ $user ]->username,
+ 'lgpassword' => self::$users[ $user ]->password ) );
+
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest(
+ array(
+ 'action' => 'login',
+ 'lgtoken' => $token,
+ 'lgname' => self::$users[ $user ]->username,
+ 'lgpassword' => self::$users[ $user ]->password,
+ ),
+ $data[2]
+ );
+
+ return $data;
+ }
+
+ protected function getTokenList( $user, $session = null ) {
+ $data = $this->doApiRequest( array(
+ 'action' => 'tokens',
+ 'type' => 'edit|delete|protect|move|block|unblock|watch'
+ ), $session, false, $user->user );
+
+ if ( !array_key_exists( 'tokens', $data[0] ) ) {
+ throw new MWException( 'Api failed to return a token list' );
+ }
+
+ return $data[0]['tokens'];
+ }
+
+ public function testApiTestGroup() {
+ $groups = PHPUnit_Util_Test::getGroups( get_class( $this ) );
+ $constraint = PHPUnit_Framework_Assert::logicalOr(
+ $this->contains( 'medium' ),
+ $this->contains( 'large' )
+ );
+ $this->assertThat( $groups, $constraint,
+ 'ApiTestCase::setUp can be slow, tests must be "medium" or "large"'
+ );
+ }
+}
+
+class UserWrapper {
+ public $userName;
+ public $password;
+ public $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();
+ }
+}
+
+class MockApi extends ApiBase {
+ public function execute() {
+ }
+
+ public function getVersion() {
+ }
+
+ public function __construct() {
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'filename' => null,
+ 'enablechunks' => false,
+ 'sessionkey' => null,
+ );
+ }
+}
+
+class ApiTestContext extends RequestContext {
+
+ /**
+ * Returns a DerivativeContext with the request variables in place
+ *
+ * @param $request WebRequest request object including parameters and session
+ * @param $user User or null
+ * @return DerivativeContext
+ */
+ public function newTestContext( WebRequest $request, User $user = null ) {
+ $context = new DerivativeContext( $this );
+ $context->setRequest( $request );
+ if ( $user !== null ) {
+ $context->setUser( $user );
+ }
+
+ return $context;
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php
new file mode 100644
index 00000000..7e18b6ed
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestCaseUpload.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * * Abstract class to support upload tests
+ */
+
+abstract class ApiTestCaseUpload extends ApiTestCase {
+ /**
+ * Fixture -- run before every test
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgEnableUploads' => true,
+ 'wgEnableAPI' => true,
+ ) );
+
+ wfSetupSession();
+
+ $this->clearFakeUploads();
+ }
+
+ protected function tearDown() {
+ $this->clearTempUpload();
+
+ parent::tearDown();
+ }
+
+ /**
+ * 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;
+ }
+ $page = WikiPage::factory( $title );
+ $page->doDeleteArticle( "removing for test" );
+
+ // see if it now doesn't exist; reload
+ $title = Title::newFromText( $title->getText(), 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 = FSFile::getSha1Base36FromPath( $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;
+ }
+
+ function fakeUploadChunk( $fieldName, $fileName, $type, & $chunkData ) {
+ $tmpName = tempnam( wfTempDir(), "" );
+ // copy the chunk data to temp location:
+ if ( !file_put_contents( $tmpName, $chunkData ) ) {
+ throw new Exception( "couldn't copy chunk data 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
+ );
+ }
+
+ function clearTempUpload() {
+ if ( isset( $_FILES['file']['tmp_name'] ) ) {
+ $tmp = $_FILES['file']['tmp_name'];
+ if ( file_exists( $tmp ) ) {
+ unlink( $tmp );
+ }
+ }
+ }
+
+ /**
+ * Remove traces of previous fake uploads
+ */
+ function clearFakeUploads() {
+ $_FILES = array();
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php
new file mode 100644
index 00000000..1540af55
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiUploadTest.php
@@ -0,0 +1,561 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ */
+
+/**
+ * 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: port the other Upload tests, and other API tests to this framework
+
+require_once 'ApiTestCaseUpload.php';
+
+/**
+ * @group Database
+ * @group Broken
+ * Broken test, reports false errors from time to time.
+ * See https://bugzilla.wikimedia.org/26169
+ *
+ * This is pretty sucky... needs to be prettified.
+ */
+class ApiUploadTest extends ApiTestCaseUpload {
+ /**
+ * Testing login
+ * XXX this is a funny way of getting session context
+ */
+ public function testLogin() {
+ $user = self::$users['uploader'];
+
+ $params = array(
+ 'action' => 'login',
+ 'lgname' => $user->username,
+ 'lgpassword' => $user->password
+ );
+ list( $result, , $session ) = $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, $session );
+ $this->assertArrayHasKey( "login", $result );
+ $this->assertArrayHasKey( "result", $result['login'] );
+ $this->assertEquals( "Success", $result['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $result['login'] );
+
+ $this->assertNotEmpty( $session, 'API Login must return a session' );
+
+ 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 ) {
+ $exception = false;
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'upload',
+ ), $session, self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "One of the parameters filekey, file, url, statuskey is required",
+ $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUpload( $session ) {
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $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,
+ self::$users['uploader']->user );
+ } 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 ) {
+ $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, self::$users['uploader']->user );
+ } 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 ) {
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ // 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,
+ self::$users['uploader']->user );
+ } 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,
+ self::$users['uploader']->user ); // FIXME: leaks a temporary file
+ } 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 ) {
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $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, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } 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 ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user ); // FIXME: leaks a temporary file
+ } 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 ) {
+ $this->setMwGlobals( array(
+ 'wgUser' => self::$users['uploader']->user, // @todo FIXME: still used somewhere
+ ) );
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $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, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user ); // FIXME: leaks a temporary file
+ } 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']['filekey'] ) );
+ $this->assertEquals( $result['upload']['sessionkey'], $result['upload']['filekey'] );
+ $filekey = $result['upload']['filekey'];
+
+ // 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',
+ 'filekey' => $filekey,
+ 'filename' => $fileName,
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName, altered",
+ );
+
+ $this->clearFakeUploads();
+ $exception = false;
+ try {
+ list( $result ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception, "No UsageException exception." );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadChunks( $session ) {
+ $this->setMwGlobals( array(
+ 'wgUser' => self::$users['uploader']->user, // @todo FIXME: still used somewhere
+ ) );
+
+ $chunkSize = 1048576;
+ // Download a large image file
+ // ( using RandomImageGenerator for large files is not stable )
+ $mimeType = 'image/jpeg';
+ $url = 'http://upload.wikimedia.org/wikipedia/commons/e/ed/Oberaargletscher_from_Oberaar%2C_2010_07.JPG';
+ $filePath = wfTempDir() . '/Oberaargletscher_from_Oberaar.jpg';
+ try {
+ // Only download if the file is not avaliable in the temp location:
+ if ( !is_file( $filePath ) ) {
+ copy( $url, $filePath );
+ }
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $fileSize = filesize( $filePath );
+ $fileName = basename( $filePath );
+
+ $this->deleteFileByFileName( $fileName );
+ $this->deleteFileByContent( $filePath );
+
+ // Base upload params:
+ $params = array(
+ 'action' => 'upload',
+ 'stash' => 1,
+ 'filename' => $fileName,
+ 'filesize' => $fileSize,
+ 'offset' => 0,
+ );
+
+ // Upload chunks
+ $chunkSessionKey = false;
+ $resultOffset = 0;
+ // Open the file:
+ $handle = @fopen( $filePath, "r" );
+ if ( $handle === false ) {
+ $this->markTestIncomplete( "could not open file: $filePath" );
+ }
+ while ( !feof( $handle ) ) {
+ // Get the current chunk
+ $chunkData = @fread( $handle, $chunkSize );
+
+ // Upload the current chunk into the $_FILE object:
+ $this->fakeUploadChunk( 'chunk', 'blob', $mimeType, $chunkData );
+
+ // Check for chunkSessionKey
+ if ( !$chunkSessionKey ) {
+ // Upload fist chunk ( and get the session key )
+ try {
+ list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+ // Make sure we got a valid chunk continue:
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+ // If we don't get a session key mark test incomplete.
+ if ( !isset( $result['upload']['filekey'] ) ) {
+ $this->markTestIncomplete( "no filekey provided" );
+ }
+ $chunkSessionKey = $result['upload']['filekey'];
+ $this->assertEquals( 'Continue', $result['upload']['result'] );
+ // First chunk should have chunkSize == offset
+ $this->assertEquals( $chunkSize, $result['upload']['offset'] );
+ $resultOffset = $result['upload']['offset'];
+ continue;
+ }
+ // Filekey set to chunk session
+ $params['filekey'] = $chunkSessionKey;
+ // Update the offset ( always add chunkSize for subquent chunks should be in-sync with $result['upload']['offset'] )
+ $params['offset'] += $chunkSize;
+ // Make sure param offset is insync with resultOffset:
+ $this->assertEquals( $resultOffset, $params['offset'] );
+ // Upload current chunk
+ try {
+ list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+ // Make sure we got a valid chunk continue:
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+
+ // Check if we were on the last chunk:
+ if ( $params['offset'] + $chunkSize >= $fileSize ) {
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ break;
+ } else {
+ $this->assertEquals( 'Continue', $result['upload']['result'] );
+ // update $resultOffset
+ $resultOffset = $result['upload']['offset'];
+ }
+ }
+ fclose( $handle );
+
+ // Check that we got a valid file result:
+ wfDebug( __METHOD__ . " hohoh filesize {$fileSize} info {$result['upload']['imageinfo']['size']}\n\n" );
+ $this->assertEquals( $fileSize, $result['upload']['imageinfo']['size'] );
+ $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+ $filekey = $result['upload']['filekey'];
+
+ // Now we should try to release the file from stash
+ $params = array(
+ 'action' => 'upload',
+ 'filekey' => $filekey,
+ 'filename' => $fileName,
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName, altered",
+ );
+ $this->clearFakeUploads();
+ $exception = false;
+ try {
+ list( $result ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ // don't remove downloaded temporary file for fast subquent tests.
+ //unlink( $filePath );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php
new file mode 100644
index 00000000..028ea9ff
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiWatchTest.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @todo This test suite is severly broken and need a full review
+ */
+class ApiWatchTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function getTokens() {
+ return $this->getTokenList( self::$users['sysop'] );
+ }
+
+ /**
+ */
+ public function testWatchEdit() {
+ $tokens = $this->getTokens();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'edit',
+ 'title' => 'Help:UTPage', // Help namespace is hopefully wikitext
+ 'text' => 'new text',
+ 'token' => $tokens['edittoken'],
+ 'watchlist' => 'watch' ) );
+ $this->assertArrayHasKey( 'edit', $data[0] );
+ $this->assertArrayHasKey( 'result', $data[0]['edit'] );
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testWatchEdit
+ */
+ public function testWatchClear() {
+ $tokens = $this->getTokens();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ) );
+
+ 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,
+ 'token' => $tokens['watchtoken'] ) );
+ }
+ }
+ $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;
+ }
+
+ /**
+ */
+ public function testWatchProtect() {
+ $tokens = $this->getTokens();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'protect',
+ 'token' => $tokens['protecttoken'],
+ 'title' => 'Help:UTPage',
+ 'protections' => 'edit=sysop',
+ 'watchlist' => 'unwatch' ) );
+
+ $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] );
+ }
+
+ /**
+ */
+ public function testGetRollbackToken() {
+ $this->getTokens();
+
+ if ( !Title::newFromText( 'Help:UTPage' )->exists() ) {
+ $this->markTestSkipped( "The article [[Help:UTPage]] does not exist" ); //TODO: just create it?
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'prop' => 'revisions',
+ 'titles' => 'Help:UTPage',
+ 'rvtoken' => 'rollback' ) );
+
+ $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->markTestSkipped( "Target page (Help:UTPage) 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;
+ }
+
+ /**
+ * @group Broken
+ * Broken because there is currently no revision info in the $pageinfo
+ *
+ * @depends testGetRollbackToken
+ */
+ public function testWatchRollback( $data ) {
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+ $revinfo = $pageinfo['revisions'][0];
+
+ try {
+ $data = $this->doApiRequest( array(
+ 'action' => 'rollback',
+ 'title' => 'Help:UTPage',
+ 'user' => $revinfo['user'],
+ 'token' => $pageinfo['rollbacktoken'],
+ 'watchlist' => 'watch' ) );
+
+ $this->assertArrayHasKey( 'rollback', $data[0] );
+ $this->assertArrayHasKey( 'title', $data[0]['rollback'] );
+ } catch ( UsageException $ue ) {
+ if ( $ue->getCodeString() == 'onlyauthor' ) {
+ $this->markTestIncomplete( "Only one author to 'Help:UTPage', cannot test rollback" );
+ } else {
+ $this->fail( "Received error '" . $ue->getCodeString() . "'" );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/PrefixUniquenessTest.php b/tests/phpunit/includes/api/PrefixUniquenessTest.php
new file mode 100644
index 00000000..d9be85e3
--- /dev/null
+++ b/tests/phpunit/includes/api/PrefixUniquenessTest.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Checks that all API query modules, core and extensions, have unique prefixes.
+ *
+ * @group API
+ */
+class PrefixUniquenessTest extends MediaWikiTestCase {
+ public function testPrefixes() {
+ $main = new ApiMain( new FauxRequest() );
+ $query = new ApiQuery( $main, 'foo', 'bar' );
+ $modules = $query->getModuleManager()->getNamesWithClasses();
+ $prefixes = array();
+
+ foreach ( $modules as $name => $class ) {
+ $module = new $class( $main, $name );
+ $prefix = $module->getModulePrefix();
+ if ( isset( $prefixes[$prefix] ) ) {
+ $this->fail( "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" );
+ }
+ $prefixes[$module->getModulePrefix()] = $class;
+ }
+ $this->assertTrue( true ); // dummy call to make this test non-incomplete
+ }
+}
diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php
new file mode 100644
index 00000000..59756b21
--- /dev/null
+++ b/tests/phpunit/includes/api/RandomImageGenerator.php
@@ -0,0 +1,468 @@
+<?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 triangles)
+ *
+ * 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 $shapesToDraw = 5;
+
+ /**
+ * Orientations: 0th row, 0th column, Exif orientation code, rotation 2x2 matrix that is opposite of orientation
+ * n.b. we do not handle the 'flipped' orientations, which is why there is no entry for 2, 4, 5, or 7. Those
+ * seem to be rare in real images anyway
+ * (we also would need a non-symmetric shape for the images to test those, like a letter F)
+ */
+ private static $orientations = array(
+ array(
+ '0thRow' => 'top',
+ '0thCol' => 'left',
+ 'exifCode' => 1,
+ 'counterRotation' => array( array( 1, 0 ), array( 0, 1 ) )
+ ),
+ array(
+ '0thRow' => 'bottom',
+ '0thCol' => 'right',
+ 'exifCode' => 3,
+ 'counterRotation' => array( array( -1, 0 ), array( 0, -1 ) )
+ ),
+ array(
+ '0thRow' => 'right',
+ '0thCol' => 'top',
+ 'exifCode' => 6,
+ 'counterRotation' => array( array( 0, 1 ), array( 1, 0 ) )
+ ),
+ array(
+ '0thRow' => 'left',
+ '0thCol' => 'bottom',
+ 'exifCode' => 8,
+ 'counterRotation' => array( array( 0, -1 ), array( -1, 0 ) )
+ )
+ );
+
+
+ public function __construct( $options = array() ) {
+ foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight', 'shapesToDraw' ) 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',
+ __DIR__ . '/words.txt'
+ ) 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" );
+ }
+ }
+
+ /**
+ * 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 );
+ $imageWriteMethod = $this->getImageWriteMethod( $format );
+ foreach ( $filenames as $filename ) {
+ $this->{$imageWriteMethod}( $this->getImageSpec(), $format, $filename );
+ }
+
+ return $filenames;
+ }
+
+
+ /**
+ * Figure out how we write images. This is a factor of both format and the local system
+ * @param $format (a typical extension like 'svg', 'jpg', etc.)
+ */
+ function getImageWriteMethod( $format ) {
+ global $wgUseImageMagick, $wgImageMagickConvertCommand;
+ if ( $format === 'svg' ) {
+ return 'writeSvg';
+ } else {
+ // figure out how to write images
+ global $wgExiv2Command;
+ if ( class_exists( 'Imagick' ) && $wgExiv2Command && is_executable( $wgExiv2Command ) ) {
+ return 'writeImageWithApi';
+ } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) {
+ return 'writeImageWithCommandLine';
+ }
+ }
+ throw new Exception( "RandomImageGenerator: could not find a suitable method to write images in '$format' format" );
+ }
+
+ /**
+ * 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 upward pointing triangles 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->shapesToDraw; $i++ ) {
+ $radius = mt_rand( 0, $diagonalLength / 4 );
+ if ( $radius == 0 ) {
+ continue;
+ }
+ $originX = mt_rand( -1 * $radius, $spec['width'] + $radius );
+ $originY = mt_rand( -1 * $radius, $spec['height'] + $radius );
+ $angle = mt_rand( 0, ( 3.141592 / 2 ) * $radius ) / $radius;
+ $legDeltaX = round( $radius * sin( $angle ) );
+ $legDeltaY = round( $radius * cos( $angle ) );
+
+ $draw = array();
+ $draw['fill'] = $this->getRandomColor();
+ $draw['shape'] = array(
+ array( 'x' => $originX, 'y' => $originY - $radius ),
+ array( 'x' => $originX + $legDeltaX, 'y' => $originY + $legDeltaY ),
+ array( 'x' => $originX - $legDeltaX, 'y' => $originY + $legDeltaY ),
+ array( 'x' => $originX, 'y' => $originY - $radius )
+ );
+ $draws[] = $draw;
+ }
+
+ $spec['draws'] = $draws;
+
+ return $spec;
+ }
+
+ /**
+ * Given array( array('x' => 10, 'y' => 20), array( 'x' => 30, y=> 5 ) )
+ * returns "10,20 30,5"
+ * Useful for SVG and imagemagick command line arguments
+ * @param $shape: Array of arrays, each array containing x & y keys mapped to numeric values
+ * @return string
+ */
+ static function shapePointsToString( $shape ) {
+ $points = array();
+ foreach ( $shape as $point ) {
+ $points[] = $point['x'] . ',' . $point['y'];
+ }
+
+ return join( " ", $points );
+ }
+
+ /**
+ * Based on image specification, write a very simple SVG file to disk.
+ * Ignores the background spec because transparency is cool. :)
+ * @param $spec: spec describing background and shapes to draw
+ * @param $format: file format to write (which is obviously always svg here)
+ * @param $filename: filename to write to
+ */
+ public function writeSvg( $spec, $format, $filename ) {
+ $svg = new SimpleXmlElement( '<svg/>' );
+ $svg->addAttribute( 'xmlns', 'http://www.w3.org/2000/svg' );
+ $svg->addAttribute( 'version', '1.1' );
+ $svg->addAttribute( 'width', $spec['width'] );
+ $svg->addAttribute( 'height', $spec['height'] );
+ $g = $svg->addChild( 'g' );
+ foreach ( $spec['draws'] as $drawSpec ) {
+ $shape = $g->addChild( 'polygon' );
+ $shape->addAttribute( 'fill', $drawSpec['fill'] );
+ $shape->addAttribute( 'points', self::shapePointsToString( $drawSpec['shape'] ) );
+ }
+
+ if ( !$fh = fopen( $filename, 'w' ) ) {
+ throw new Exception( "couldn't open $filename for writing" );
+ }
+ fwrite( $fh, $svg->asXML() );
+ if ( !fclose( $fh ) ) {
+ throw new Exception( "couldn't close $filename" );
+ }
+ }
+
+ /**
+ * 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 ) {
+ // this is a hack because I can't get setImageOrientation() to work. See below.
+ global $wgExiv2Command;
+
+ $image = new Imagick();
+ /**
+ * If the format is 'jpg', will also add a random orientation -- the image will be drawn rotated with triangle points
+ * facing in some direction (0, 90, 180 or 270 degrees) and a countering rotation should turn the triangle points upward again
+ */
+ $orientation = self::$orientations[0]; // default is normal orientation
+ if ( $format == 'jpg' ) {
+ $orientation = self::$orientations[array_rand( self::$orientations )];
+ $spec = self::rotateImageSpec( $spec, $orientation['counterRotation'] );
+ }
+
+ $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) );
+
+ foreach ( $spec['draws'] as $drawSpec ) {
+ $draw = new ImagickDraw();
+ $draw->setFillColor( $drawSpec['fill'] );
+ $draw->polygon( $drawSpec['shape'] );
+ $image->drawImage( $draw );
+ }
+
+ $image->setImageFormat( $format );
+
+ // this doesn't work, even though it's documented to do so...
+ // $image->setImageOrientation( $orientation['exifCode'] );
+
+ $image->writeImage( $filename );
+
+ // because the above setImageOrientation call doesn't work... nor can I get an external imagemagick binary to do this either...
+ // hacking this for now (only works if you have exiv2 installed, a program to read and manipulate exif)
+ if ( $wgExiv2Command ) {
+ $cmd = wfEscapeShellArg( $wgExiv2Command )
+ . " -M "
+ . wfEscapeShellArg( "set Exif.Image.Orientation " . $orientation['exifCode'] )
+ . " "
+ . wfEscapeShellArg( $filename );
+
+ $retval = 0;
+ $err = wfShellExec( $cmd, $retval );
+ if ( $retval !== 0 ) {
+ print "Error with $cmd: $retval, $err\n";
+ }
+ }
+ }
+
+ /**
+ * Given an image specification, produce rotated version
+ * This is used when simulating a rotated image capture with Exif orientation
+ * @param $spec Object returned by getImageSpec
+ * @param $matrix 2x2 transformation matrix
+ * @return transformed Spec
+ */
+ private static function rotateImageSpec( &$spec, $matrix ) {
+ $tSpec = array();
+ $dims = self::matrixMultiply2x2( $matrix, $spec['width'], $spec['height'] );
+ $correctionX = 0;
+ $correctionY = 0;
+ if ( $dims['x'] < 0 ) {
+ $correctionX = abs( $dims['x'] );
+ }
+ if ( $dims['y'] < 0 ) {
+ $correctionY = abs( $dims['y'] );
+ }
+ $tSpec['width'] = abs( $dims['x'] );
+ $tSpec['height'] = abs( $dims['y'] );
+ $tSpec['fill'] = $spec['fill'];
+ $tSpec['draws'] = array();
+ foreach ( $spec['draws'] as $draw ) {
+ $tDraw = array(
+ 'fill' => $draw['fill'],
+ 'shape' => array()
+ );
+ foreach ( $draw['shape'] as $point ) {
+ $tPoint = self::matrixMultiply2x2( $matrix, $point['x'], $point['y'] );
+ $tPoint['x'] += $correctionX;
+ $tPoint['y'] += $correctionY;
+ $tDraw['shape'][] = $tPoint;
+ }
+ $tSpec['draws'][] = $tDraw;
+ }
+
+ return $tSpec;
+ }
+
+ /**
+ * Given a matrix and a pair of images, return new position
+ * @param $matrix: 2x2 rotation matrix
+ * @param $x: x-coordinate number
+ * @param $y: y-coordinate number
+ * @return Array transformed with properties x, y
+ */
+ private static function matrixMultiply2x2( $matrix, $x, $y ) {
+ return array(
+ 'x' => $x * $matrix[0][0] + $y * $matrix[0][1],
+ 'y' => $x * $matrix[1][0] + $y * $matrix[1][1]
+ );
+ }
+
+
+ /**
+ * 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) polygon 41,39 44,57 50,57 41,39' \
+ * -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 shapes 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'];
+ $polygon = self::shapePointsToString( $draw['shape'] );
+ $drawCommand = "fill $fill polygon $polygon";
+ $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 or SVG, 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/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
new file mode 100644
index 00000000..a0bbb2dc
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiFormatPhpTest extends ApiFormatTestBase {
+
+ public function testValidPhpSyntax() {
+
+ $data = $this->apiRequest( 'php', array( 'action' => 'query', 'meta' => 'siteinfo' ) );
+
+ $this->assertInternalType( 'array', unserialize( $data ) );
+ $this->assertGreaterThan( 0, count( (array)$data ) );
+ }
+}
diff --git a/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/tests/phpunit/includes/api/format/ApiFormatTestBase.php
new file mode 100644
index 00000000..153f2cf4
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatTestBase.php
@@ -0,0 +1,22 @@
+<?php
+
+abstract class ApiFormatTestBase extends ApiTestCase {
+ protected function apiRequest( $format, $params, $data = null ) {
+ $data = parent::doApiRequest( $params, $data, true );
+
+ $module = $data[3];
+
+ $printer = $module->createPrinterByName( $format );
+ $printer->setUnescapeAmps( false );
+
+ $printer->initPrinter( false );
+
+ ob_start();
+ $printer->execute();
+ $out = ob_get_clean();
+
+ $printer->closePrinter();
+
+ return $out;
+ }
+}
diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php
new file mode 100644
index 00000000..87f5c4c0
--- /dev/null
+++ b/tests/phpunit/includes/api/generateRandomImages.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Bootstrapping for test image file generation
+ *
+ * @file
+ */
+
+// Start up MediaWiki in command-line mode
+require_once __DIR__ . "/../../../../maintenance/Maintenance.php";
+require __DIR__ . "/RandomImageGenerator.php";
+
+class GenerateRandomImages extends Maintenance {
+
+ public function getDbType() {
+ return Maintenance::DB_NONE;
+ }
+
+ public function execute() {
+
+ $getOptSpec = array(
+ 'dictionaryFile::',
+ 'minWidth::',
+ 'maxWidth::',
+ 'minHeight::',
+ 'maxHeight::',
+ 'shapesToDraw::',
+ 'shape::',
+
+ '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 );
+ }
+}
+
+$maintClass = 'GenerateRandomImages';
+require RUN_MAINTENANCE_IF_MAIN;
diff --git a/tests/phpunit/includes/api/query/ApiQueryBasicTest.php b/tests/phpunit/includes/api/query/ApiQueryBasicTest.php
new file mode 100644
index 00000000..1a2aa832
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryBasicTest.php
@@ -0,0 +1,395 @@
+<?php
+/**
+ *
+ *
+ * Created on Feb 6, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+require_once 'ApiQueryTestBase.php';
+
+/** These tests validate basic functionality of the api query module
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiQueryBasicTest extends ApiQueryTestBase {
+ /**
+ * Create a set of pages. These must not change, otherwise the tests might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ function addDBData() {
+ try {
+ if ( Title::newFromText( 'AQBT-All' )->exists() ) {
+ return;
+ }
+
+ // Ordering is important, as it will be returned in the same order as stored in the index
+ $this->editPage( 'AQBT-All', '[[Category:AQBT-Cat]] [[AQBT-Links]] {{AQBT-T}}' );
+ $this->editPage( 'AQBT-Categories', '[[Category:AQBT-Cat]]' );
+ $this->editPage( 'AQBT-Links', '[[AQBT-All]] [[AQBT-Categories]] [[AQBT-Templates]]' );
+ $this->editPage( 'AQBT-Templates', '{{AQBT-T}}' );
+ $this->editPage( 'AQBT-T', 'Content', '', NS_TEMPLATE );
+
+ // Refresh due to the bug with listing transclusions as links if they don't exist
+ $this->editPage( 'AQBT-All', '[[Category:AQBT-Cat]] [[AQBT-Links]] {{AQBT-T}}' );
+ $this->editPage( 'AQBT-Templates', '{{AQBT-T}}' );
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ private static $links = array(
+ array( 'prop' => 'links', 'titles' => 'AQBT-All' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All',
+ 'links' => array(
+ array( 'ns' => 0, 'title' => 'AQBT-Links' ),
+ )
+ )
+ ) )
+ );
+
+ private static $templates = array(
+ array( 'prop' => 'templates', 'titles' => 'AQBT-All' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All',
+ 'templates' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ),
+ )
+ )
+ ) )
+ );
+
+ private static $categories = array(
+ array( 'prop' => 'categories', 'titles' => 'AQBT-All' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All',
+ 'categories' => array(
+ array( 'ns' => 14, 'title' => 'Category:AQBT-Cat' ),
+ )
+ )
+ ) )
+ );
+
+ private static $allpages = array(
+ array( 'list' => 'allpages', 'apprefix' => 'AQBT-' ),
+ array( 'allpages' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'pageid' => 2, 'ns' => 0, 'title' => 'AQBT-Categories' ),
+ array( 'pageid' => 3, 'ns' => 0, 'title' => 'AQBT-Links' ),
+ array( 'pageid' => 4, 'ns' => 0, 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $alllinks = array(
+ array( 'list' => 'alllinks', 'alprefix' => 'AQBT-' ),
+ array( 'alllinks' => array(
+ array( 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'ns' => 0, 'title' => 'AQBT-Categories' ),
+ array( 'ns' => 0, 'title' => 'AQBT-Links' ),
+ array( 'ns' => 0, 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $alltransclusions = array(
+ array( 'list' => 'alltransclusions', 'atprefix' => 'AQBT-' ),
+ array( 'alltransclusions' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ),
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ),
+ ) )
+ );
+
+ private static $allcategories = array(
+ array( 'list' => 'allcategories', 'acprefix' => 'AQBT-' ),
+ array( 'allcategories' => array(
+ array( '*' => 'AQBT-Cat' ),
+ ) )
+ );
+
+ private static $backlinks = array(
+ array( 'list' => 'backlinks', 'bltitle' => 'AQBT-Links' ),
+ array( 'backlinks' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ ) )
+ );
+
+ private static $embeddedin = array(
+ array( 'list' => 'embeddedin', 'eititle' => 'Template:AQBT-T' ),
+ array( 'embeddedin' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'pageid' => 4, 'ns' => 0, 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $categorymembers = array(
+ array( 'list' => 'categorymembers', 'cmtitle' => 'Category:AQBT-Cat' ),
+ array( 'categorymembers' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'pageid' => 2, 'ns' => 0, 'title' => 'AQBT-Categories' ),
+ ) )
+ );
+
+ private static $generatorAllpages = array(
+ array( 'generator' => 'allpages', 'gapprefix' => 'AQBT-' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All' ),
+ '2' => array(
+ 'pageid' => 2,
+ 'ns' => 0,
+ 'title' => 'AQBT-Categories' ),
+ '3' => array(
+ 'pageid' => 3,
+ 'ns' => 0,
+ 'title' => 'AQBT-Links' ),
+ '4' => array(
+ 'pageid' => 4,
+ 'ns' => 0,
+ 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $generatorLinks = array(
+ array( 'generator' => 'links', 'titles' => 'AQBT-Links' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All' ),
+ '2' => array(
+ 'pageid' => 2,
+ 'ns' => 0,
+ 'title' => 'AQBT-Categories' ),
+ '4' => array(
+ 'pageid' => 4,
+ 'ns' => 0,
+ 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $generatorLinksPropLinks = array(
+ array( 'prop' => 'links' ),
+ array( 'pages' => array(
+ '1' => array( 'links' => array(
+ array( 'ns' => 0, 'title' => 'AQBT-Links' ),
+ ) )
+ ) )
+ );
+
+ private static $generatorLinksPropTemplates = array(
+ array( 'prop' => 'templates' ),
+ array( 'pages' => array(
+ '1' => array( 'templates' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ) ) ),
+ '4' => array( 'templates' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ) ) ),
+ ) )
+ );
+
+ /**
+ * Test basic props
+ */
+ public function testProps() {
+ $this->check( self::$links );
+ $this->check( self::$templates );
+ $this->check( self::$categories );
+ }
+
+ /**
+ * Test basic lists
+ */
+ public function testLists() {
+ $this->check( self::$allpages );
+ $this->check( self::$alllinks );
+ $this->check( self::$alltransclusions );
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // $this->check( self::$allcategories );
+ $this->check( self::$backlinks );
+ $this->check( self::$embeddedin );
+ $this->check( self::$categorymembers );
+ }
+
+ /**
+ * Test basic lists
+ */
+ public function testAllTogether() {
+
+ // All props together
+ $this->check( $this->merge(
+ self::$links,
+ self::$templates,
+ self::$categories
+ ) );
+
+ // All lists together
+ $this->check( $this->merge(
+ self::$allpages,
+ self::$alllinks,
+ self::$alltransclusions,
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // self::$allcategories,
+ self::$backlinks,
+ self::$embeddedin,
+ self::$categorymembers
+ ) );
+
+ // All props+lists together
+ $this->check( $this->merge(
+ self::$links,
+ self::$templates,
+ self::$categories,
+ self::$allpages,
+ self::$alllinks,
+ self::$alltransclusions,
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // self::$allcategories,
+ self::$backlinks,
+ self::$embeddedin,
+ self::$categorymembers
+ ) );
+ }
+
+ /**
+ * Test basic lists
+ */
+ public function testGenerator() {
+ // generator=allpages
+ $this->check( self::$generatorAllpages );
+ // generator=allpages & list=allpages
+ $this->check( $this->merge(
+ self::$generatorAllpages,
+ self::$allpages ) );
+ // generator=links
+ $this->check( self::$generatorLinks );
+ // generator=links & prop=links
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropLinks ) );
+ // generator=links & prop=templates
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropTemplates ) );
+ // generator=links & prop=links|templates
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropLinks,
+ self::$generatorLinksPropTemplates ) );
+ // generator=links & prop=links|templates & list=allpages|...
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropLinks,
+ self::$generatorLinksPropTemplates,
+ self::$allpages,
+ self::$alllinks,
+ self::$alltransclusions,
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // self::$allcategories,
+ self::$backlinks,
+ self::$embeddedin,
+ self::$categorymembers ) );
+ }
+
+ /**
+ * Test bug 51821
+ */
+ public function testGeneratorRedirects() {
+ $this->editPage( 'AQBT-Target', 'test' );
+ $this->editPage( 'AQBT-Redir', '#REDIRECT [[AQBT-Target]]' );
+ $this->check( array(
+ array( 'generator' => 'backlinks', 'gbltitle' => 'AQBT-Target', 'redirects' => '1' ),
+ array(
+ 'redirects' => array(
+ array(
+ 'from' => 'AQBT-Redir',
+ 'to' => 'AQBT-Target',
+ )
+ ),
+ 'pages' => array(
+ '6' => array(
+ 'pageid' => 6,
+ 'ns' => 0,
+ 'title' => 'AQBT-Target',
+ )
+ ),
+ )
+ ) );
+ }
+
+ /**
+ * Recursively merges the expected values in the $item into the $all
+ */
+ private function mergeExpected( &$all, $item ) {
+ foreach ( $item as $k => $v ) {
+ if ( array_key_exists( $k, $all ) ) {
+ if ( is_array( $all[$k] ) ) {
+ $this->mergeExpected( $all[$k], $v );
+ } else {
+ $this->assertEquals( $all[$k], $v );
+ }
+ } else {
+ $all[$k] = $v;
+ }
+ }
+ }
+
+ /**
+ * Recursively compare arrays, ignoring mismatches in numeric key and pageids.
+ * @param $expected array expected values
+ * @param $result array returned values
+ */
+ private function assertQueryResults( $expected, $result ) {
+ reset( $expected );
+ reset( $result );
+ while ( true ) {
+ $e = each( $expected );
+ $r = each( $result );
+ // If either of the arrays is shorter, abort. If both are done, success.
+ $this->assertEquals( (bool)$e, (bool)$r );
+ if ( !$e ) {
+ break; // done
+ }
+ // continue only if keys are identical or both keys are numeric
+ $this->assertTrue( $e['key'] === $r['key'] || ( is_numeric( $e['key'] ) && is_numeric( $r['key'] ) ) );
+ // don't compare pageids
+ if ( $e['key'] !== 'pageid' ) {
+ // If values are arrays, compare recursively, otherwise compare with ===
+ if ( is_array( $e['value'] ) && is_array( $r['value'] ) ) {
+ $this->assertQueryResults( $e['value'], $r['value'] );
+ } else {
+ $this->assertEquals( $e['value'], $r['value'] );
+ }
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php b/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php
new file mode 100644
index 00000000..4d5ddbae
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+require_once 'ApiQueryContinueTestBase.php';
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiQueryContinue2Test extends ApiQueryContinueTestBase {
+ /**
+ * Create a set of pages. These must not change, otherwise the tests might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ function addDBData() {
+ try {
+ $this->editPage( 'AQCT73462-A', '**AQCT73462-A** [[AQCT73462-B]] [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-B', '[[AQCT73462-A]] **AQCT73462-B** [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-C', '[[AQCT73462-A]] [[AQCT73462-B]] **AQCT73462-C**' );
+ $this->editPage( 'AQCT73462-A', '**AQCT73462-A** [[AQCT73462-B]] [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-B', '[[AQCT73462-A]] **AQCT73462-B** [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-C', '[[AQCT73462-A]] [[AQCT73462-B]] **AQCT73462-C**' );
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ /**
+ * @medium
+ */
+ public function testA() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p, $gDir ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT73462-',
+ 'prop' => 'links',
+ 'gaplimit' => "$g",
+ 'pllimit' => "$p",
+ 'gapdir' => $gDir ? "ascending" : "descending",
+ );
+ };
+ // generator + 1 prop + 1 list
+ $data = $this->query( $mk( 99, 99, true ), 1, 'g1p', false );
+ $this->checkC( $data, $mk( 1, 1, true ), 6, 'g1p-11t' );
+ $this->checkC( $data, $mk( 2, 2, true ), 3, 'g1p-22t' );
+ $this->checkC( $data, $mk( 1, 1, false ), 6, 'g1p-11f' );
+ $this->checkC( $data, $mk( 2, 2, false ), 3, 'g1p-22f' );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTest.php b/tests/phpunit/includes/api/query/ApiQueryContinueTest.php
new file mode 100644
index 00000000..f494e9ca
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryContinueTest.php
@@ -0,0 +1,313 @@
+<?php
+/**
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+require_once 'ApiQueryContinueTestBase.php';
+
+/**
+ * These tests validate the new continue functionality of the api query module by
+ * doing multiple requests with varying parameters, merging the results, and checking
+ * that the result matches the full data received in one no-limits call.
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiQueryContinueTest extends ApiQueryContinueTestBase {
+ /**
+ * Create a set of pages. These must not change, otherwise the tests might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ function addDBData() {
+ try {
+ $this->editPage( 'Template:AQCT-T1', '**Template:AQCT-T1**' );
+ $this->editPage( 'Template:AQCT-T2', '**Template:AQCT-T2**' );
+ $this->editPage( 'Template:AQCT-T3', '**Template:AQCT-T3**' );
+ $this->editPage( 'Template:AQCT-T4', '**Template:AQCT-T4**' );
+ $this->editPage( 'Template:AQCT-T5', '**Template:AQCT-T5**' );
+
+ $this->editPage( 'AQCT-1', '**AQCT-1** {{AQCT-T2}} {{AQCT-T3}} {{AQCT-T4}} {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-2', '[[AQCT-1]] **AQCT-2** {{AQCT-T3}} {{AQCT-T4}} {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-3', '[[AQCT-1]] [[AQCT-2]] **AQCT-3** {{AQCT-T4}} {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-4', '[[AQCT-1]] [[AQCT-2]] [[AQCT-3]] **AQCT-4** {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-5', '[[AQCT-1]] [[AQCT-2]] [[AQCT-3]] [[AQCT-4]] **AQCT-5**' );
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ /**
+ * Test smart continue - list=allpages
+ * @medium
+ */
+ public function test1List() {
+ $this->mVerbose = false;
+ $mk = function ( $l ) {
+ return array(
+ 'list' => 'allpages',
+ 'apprefix' => 'AQCT-',
+ 'aplimit' => "$l",
+ );
+ };
+ $data = $this->query( $mk( 99 ), 1, '1L', false );
+
+ // 1 list
+ $this->checkC( $data, $mk( 1 ), 5, '1L-1' );
+ $this->checkC( $data, $mk( 2 ), 3, '1L-2' );
+ $this->checkC( $data, $mk( 3 ), 2, '1L-3' );
+ $this->checkC( $data, $mk( 4 ), 2, '1L-4' );
+ $this->checkC( $data, $mk( 5 ), 1, '1L-5' );
+ }
+
+ /**
+ * Test smart continue - list=allpages|alltransclusions
+ * @medium
+ */
+ public function test2Lists() {
+ $this->mVerbose = false;
+ $mk = function ( $l1, $l2 ) {
+ return array(
+ 'list' => 'allpages|alltransclusions',
+ 'apprefix' => 'AQCT-',
+ 'atprefix' => 'AQCT-',
+ 'atunique' => '',
+ 'aplimit' => "$l1",
+ 'atlimit' => "$l2",
+ );
+ };
+ // 2 lists
+ $data = $this->query( $mk( 99, 99 ), 1, '2L', false );
+ $this->checkC( $data, $mk( 1, 1 ), 5, '2L-11' );
+ $this->checkC( $data, $mk( 2, 2 ), 3, '2L-22' );
+ $this->checkC( $data, $mk( 3, 3 ), 2, '2L-33' );
+ $this->checkC( $data, $mk( 4, 4 ), 2, '2L-44' );
+ $this->checkC( $data, $mk( 5, 5 ), 1, '2L-55' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links
+ * @medium
+ */
+ public function testGen1Prop() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links',
+ 'pllimit' => "$p",
+ );
+ };
+ // generator + 1 prop
+ $data = $this->query( $mk( 99, 99 ), 1, 'G1P', false );
+ $this->checkC( $data, $mk( 1, 1 ), 11, 'G1P-11' );
+ $this->checkC( $data, $mk( 2, 2 ), 6, 'G1P-22' );
+ $this->checkC( $data, $mk( 3, 3 ), 4, 'G1P-33' );
+ $this->checkC( $data, $mk( 4, 4 ), 3, 'G1P-44' );
+ $this->checkC( $data, $mk( 5, 5 ), 2, 'G1P-55' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links|templates
+ * @medium
+ */
+ public function testGen2Prop() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p1, $p2 ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links|templates',
+ 'pllimit' => "$p1",
+ 'tllimit' => "$p2",
+ );
+ };
+ // generator + 2 props
+ $data = $this->query( $mk( 99, 99, 99 ), 1, 'G2P', false );
+ $this->checkC( $data, $mk( 1, 1, 1 ), 16, 'G2P-111' );
+ $this->checkC( $data, $mk( 2, 2, 2 ), 9, 'G2P-222' );
+ $this->checkC( $data, $mk( 3, 3, 3 ), 6, 'G2P-333' );
+ $this->checkC( $data, $mk( 4, 4, 4 ), 4, 'G2P-444' );
+ $this->checkC( $data, $mk( 5, 5, 5 ), 2, 'G2P-555' );
+ $this->checkC( $data, $mk( 5, 1, 1 ), 10, 'G2P-511' );
+ $this->checkC( $data, $mk( 4, 2, 2 ), 7, 'G2P-422' );
+ $this->checkC( $data, $mk( 2, 3, 3 ), 7, 'G2P-233' );
+ $this->checkC( $data, $mk( 2, 4, 4 ), 5, 'G2P-244' );
+ $this->checkC( $data, $mk( 1, 5, 5 ), 5, 'G2P-155' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links, list=alltransclusions
+ * @medium
+ */
+ public function testGen1Prop1List() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p, $l ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links',
+ 'pllimit' => "$p",
+ 'list' => 'alltransclusions',
+ 'atprefix' => 'AQCT-',
+ 'atunique' => '',
+ 'atlimit' => "$l",
+ );
+ };
+ // generator + 1 prop + 1 list
+ $data = $this->query( $mk( 99, 99, 99 ), 1, 'G1P1L', false );
+ $this->checkC( $data, $mk( 1, 1, 1 ), 11, 'G1P1L-111' );
+ $this->checkC( $data, $mk( 2, 2, 2 ), 6, 'G1P1L-222' );
+ $this->checkC( $data, $mk( 3, 3, 3 ), 4, 'G1P1L-333' );
+ $this->checkC( $data, $mk( 4, 4, 4 ), 3, 'G1P1L-444' );
+ $this->checkC( $data, $mk( 5, 5, 5 ), 2, 'G1P1L-555' );
+ $this->checkC( $data, $mk( 5, 5, 1 ), 4, 'G1P1L-551' );
+ $this->checkC( $data, $mk( 5, 5, 2 ), 2, 'G1P1L-552' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links|templates,
+ * list=alllinks|alltransclusions, meta=siteinfo
+ * @medium
+ */
+ public function testGen2Prop2List1Meta() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p1, $p2, $l1, $l2 ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links|templates',
+ 'pllimit' => "$p1",
+ 'tllimit' => "$p2",
+ 'list' => 'alllinks|alltransclusions',
+ 'alprefix' => 'AQCT-',
+ 'alunique' => '',
+ 'allimit' => "$l1",
+ 'atprefix' => 'AQCT-',
+ 'atunique' => '',
+ 'atlimit' => "$l2",
+ 'meta' => 'siteinfo',
+ 'siprop' => 'namespaces',
+ );
+ };
+ // generator + 1 prop + 1 list
+ $data = $this->query( $mk( 99, 99, 99, 99, 99 ), 1, 'G2P2L1M', false );
+ $this->checkC( $data, $mk( 1, 1, 1, 1, 1 ), 16, 'G2P2L1M-11111' );
+ $this->checkC( $data, $mk( 2, 2, 2, 2, 2 ), 9, 'G2P2L1M-22222' );
+ $this->checkC( $data, $mk( 3, 3, 3, 3, 3 ), 6, 'G2P2L1M-33333' );
+ $this->checkC( $data, $mk( 4, 4, 4, 4, 4 ), 4, 'G2P2L1M-44444' );
+ $this->checkC( $data, $mk( 5, 5, 5, 5, 5 ), 2, 'G2P2L1M-55555' );
+ $this->checkC( $data, $mk( 5, 5, 5, 1, 1 ), 4, 'G2P2L1M-55511' );
+ $this->checkC( $data, $mk( 5, 5, 5, 2, 2 ), 2, 'G2P2L1M-55522' );
+ $this->checkC( $data, $mk( 5, 1, 1, 5, 5 ), 10, 'G2P2L1M-51155' );
+ $this->checkC( $data, $mk( 5, 2, 2, 5, 5 ), 5, 'G2P2L1M-52255' );
+ }
+
+ /**
+ * Test smart continue - generator=templates, prop=templates
+ * @medium
+ */
+ public function testSameGenAndProp() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $gDir, $p, $pDir ) {
+ return array(
+ 'titles' => 'AQCT-1',
+ 'generator' => 'templates',
+ 'gtllimit' => "$g",
+ 'gtldir' => $gDir ? 'ascending' : 'descending',
+ 'prop' => 'templates',
+ 'tllimit' => "$p",
+ 'tldir' => $pDir ? 'ascending' : 'descending',
+ );
+ };
+ // generator + 1 prop
+ $data = $this->query( $mk( 99, true, 99, true ), 1, 'G=P', false );
+
+ $this->checkC( $data, $mk( 1, true, 1, true ), 4, 'G=P-1t1t' );
+ $this->checkC( $data, $mk( 2, true, 2, true ), 2, 'G=P-2t2t' );
+ $this->checkC( $data, $mk( 3, true, 3, true ), 2, 'G=P-3t3t' );
+ $this->checkC( $data, $mk( 1, true, 3, true ), 4, 'G=P-1t3t' );
+ $this->checkC( $data, $mk( 3, true, 1, true ), 2, 'G=P-3t1t' );
+
+ $this->checkC( $data, $mk( 1, true, 1, false ), 4, 'G=P-1t1f' );
+ $this->checkC( $data, $mk( 2, true, 2, false ), 2, 'G=P-2t2f' );
+ $this->checkC( $data, $mk( 3, true, 3, false ), 2, 'G=P-3t3f' );
+ $this->checkC( $data, $mk( 1, true, 3, false ), 4, 'G=P-1t3f' );
+ $this->checkC( $data, $mk( 3, true, 1, false ), 2, 'G=P-3t1f' );
+
+ $this->checkC( $data, $mk( 1, false, 1, true ), 4, 'G=P-1f1t' );
+ $this->checkC( $data, $mk( 2, false, 2, true ), 2, 'G=P-2f2t' );
+ $this->checkC( $data, $mk( 3, false, 3, true ), 2, 'G=P-3f3t' );
+ $this->checkC( $data, $mk( 1, false, 3, true ), 4, 'G=P-1f3t' );
+ $this->checkC( $data, $mk( 3, false, 1, true ), 2, 'G=P-3f1t' );
+
+ $this->checkC( $data, $mk( 1, false, 1, false ), 4, 'G=P-1f1f' );
+ $this->checkC( $data, $mk( 2, false, 2, false ), 2, 'G=P-2f2f' );
+ $this->checkC( $data, $mk( 3, false, 3, false ), 2, 'G=P-3f3f' );
+ $this->checkC( $data, $mk( 1, false, 3, false ), 4, 'G=P-1f3f' );
+ $this->checkC( $data, $mk( 3, false, 1, false ), 2, 'G=P-3f1f' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, list=allpages
+ * @medium
+ */
+ public function testSameGenList() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $gDir, $l, $pDir ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'gapdir' => $gDir ? 'ascending' : 'descending',
+ 'list' => 'allpages',
+ 'apprefix' => 'AQCT-',
+ 'aplimit' => "$l",
+ 'apdir' => $pDir ? 'ascending' : 'descending',
+ );
+ };
+ // generator + 1 list
+ $data = $this->query( $mk( 99, true, 99, true ), 1, 'G=L', false );
+
+ $this->checkC( $data, $mk( 1, true, 1, true ), 5, 'G=L-1t1t' );
+ $this->checkC( $data, $mk( 2, true, 2, true ), 3, 'G=L-2t2t' );
+ $this->checkC( $data, $mk( 3, true, 3, true ), 2, 'G=L-3t3t' );
+ $this->checkC( $data, $mk( 1, true, 3, true ), 5, 'G=L-1t3t' );
+ $this->checkC( $data, $mk( 3, true, 1, true ), 5, 'G=L-3t1t' );
+ $this->checkC( $data, $mk( 1, true, 1, false ), 5, 'G=L-1t1f' );
+ $this->checkC( $data, $mk( 2, true, 2, false ), 3, 'G=L-2t2f' );
+ $this->checkC( $data, $mk( 3, true, 3, false ), 2, 'G=L-3t3f' );
+ $this->checkC( $data, $mk( 1, true, 3, false ), 5, 'G=L-1t3f' );
+ $this->checkC( $data, $mk( 3, true, 1, false ), 5, 'G=L-3t1f' );
+ $this->checkC( $data, $mk( 1, false, 1, true ), 5, 'G=L-1f1t' );
+ $this->checkC( $data, $mk( 2, false, 2, true ), 3, 'G=L-2f2t' );
+ $this->checkC( $data, $mk( 3, false, 3, true ), 2, 'G=L-3f3t' );
+ $this->checkC( $data, $mk( 1, false, 3, true ), 5, 'G=L-1f3t' );
+ $this->checkC( $data, $mk( 3, false, 1, true ), 5, 'G=L-3f1t' );
+ $this->checkC( $data, $mk( 1, false, 1, false ), 5, 'G=L-1f1f' );
+ $this->checkC( $data, $mk( 2, false, 2, false ), 3, 'G=L-2f2f' );
+ $this->checkC( $data, $mk( 3, false, 3, false ), 2, 'G=L-3f3f' );
+ $this->checkC( $data, $mk( 1, false, 3, false ), 5, 'G=L-1f3f' );
+ $this->checkC( $data, $mk( 3, false, 1, false ), 5, 'G=L-3f1f' );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php
new file mode 100644
index 00000000..fbb1e640
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ *
+ *
+ * Created on Jan 1, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+require_once 'ApiQueryTestBase.php';
+
+abstract class ApiQueryContinueTestBase extends ApiQueryTestBase {
+
+ /**
+ * Enable to print in-depth debugging info during the test run
+ */
+ protected $mVerbose = false;
+
+ /**
+ * Run query() and compare against expected values
+ */
+ protected function checkC( $expected, $params, $expectedCount, $id, $continue = true ) {
+ $result = $this->query( $params, $expectedCount, $id, $continue );
+ $this->assertResult( $expected, $result, $id );
+ }
+
+ /**
+ * Run query in a loop until no more values are available
+ * @param array $params api parameters
+ * @param int $expectedCount max number of iterations
+ * @param string $id unit test id
+ * @param boolean $useContinue true to use smart continue
+ * @return mixed: merged results data array
+ * @throws Exception
+ */
+ protected function query( $params, $expectedCount, $id, $useContinue = true ) {
+ if ( isset( $params['action'] ) ) {
+ $this->assertEquals( 'query', $params['action'], 'Invalid query action' );
+ } else {
+ $params['action'] = 'query';
+ }
+ if ( $useContinue && !isset( $params['continue'] ) ) {
+ $params['continue'] = '';
+ }
+ $count = 0;
+ $result = array();
+ $continue = array();
+ do {
+ $request = array_merge( $params, $continue );
+ uksort( $request, function ( $a, $b ) {
+ // put 'continue' params at the end - lazy method
+ $a = strpos( $a, 'continue' ) !== false ? 'zzz ' . $a : $a;
+ $b = strpos( $b, 'continue' ) !== false ? 'zzz ' . $b : $b;
+
+ return strcmp( $a, $b );
+ } );
+ $reqStr = http_build_query( $request );
+ //$reqStr = str_replace( '&', ' & ', $reqStr );
+ $this->assertLessThan( $expectedCount, $count, "$id more data: $reqStr" );
+ if ( $this->mVerbose ) {
+ print "$id (#$count): $reqStr\n";
+ }
+ try {
+ $data = $this->doApiRequest( $request );
+ } catch ( Exception $e ) {
+ throw new Exception( "$id on $count", 0, $e );
+ }
+ $data = $data[0];
+ if ( isset( $data['warnings'] ) ) {
+ $warnings = json_encode( $data['warnings'] );
+ $this->fail( "$id Warnings on #$count in $reqStr\n$warnings" );
+ }
+ $this->assertArrayHasKey( 'query', $data, "$id no 'query' on #$count in $reqStr" );
+ if ( isset( $data['continue'] ) ) {
+ $continue = $data['continue'];
+ unset( $data['continue'] );
+ } else {
+ $continue = array();
+ }
+ if ( $this->mVerbose ) {
+ $this->printResult( $data );
+ }
+ $this->mergeResult( $result, $data );
+ $count++;
+ if ( empty( $continue ) ) {
+ // $this->assertEquals( $expectedCount, $count, "$id finished early" );
+ if ( $expectedCount > $count ) {
+ print "***** $id Finished early in $count turns. $expectedCount was expected\n";
+ }
+
+ return $result;
+ } elseif ( !$useContinue ) {
+ $this->assertFalse( 'Non-smart query must be requested all at once' );
+ }
+ } while ( true );
+ }
+
+ private function printResult( $data ) {
+ $q = $data['query'];
+ $print = array();
+ if ( isset( $q['pages'] ) ) {
+ foreach ( $q['pages'] as $p ) {
+ $m = $p['title'];
+ if ( isset( $p['links'] ) ) {
+ $m .= '/[' . implode( ',', array_map(
+ function ( $v ) {
+ return $v['title'];
+ },
+ $p['links'] ) ) . ']';
+ }
+ if ( isset( $p['categories'] ) ) {
+ $m .= '/(' . implode( ',', array_map(
+ function ( $v ) {
+ return str_replace( 'Category:', '', $v['title'] );
+ },
+ $p['categories'] ) ) . ')';
+ }
+ $print[] = $m;
+ }
+ }
+ if ( isset( $q['allcategories'] ) ) {
+ $print[] = '*Cats/(' . implode( ',', array_map(
+ function ( $v ) {
+ return $v['*'];
+ },
+ $q['allcategories'] ) ) . ')';
+ }
+ self::GetItems( $q, 'allpages', 'Pages', $print );
+ self::GetItems( $q, 'alllinks', 'Links', $print );
+ self::GetItems( $q, 'alltransclusions', 'Trnscl', $print );
+ print ' ' . implode( ' ', $print ) . "\n";
+ }
+
+ private static function GetItems( $q, $moduleName, $name, &$print ) {
+ if ( isset( $q[$moduleName] ) ) {
+ $print[] = "*$name/[" . implode( ',',
+ array_map( function ( $v ) {
+ return $v['title'];
+ },
+ $q[$moduleName] ) ) . ']';
+ }
+ }
+
+ /**
+ * Recursively merge the new result returned from the query to the previous results.
+ * @param mixed $results
+ * @param mixed $newResult
+ * @param bool $numericIds If true, treat keys as ids to be merged instead of appending
+ */
+ protected function mergeResult( &$results, $newResult, $numericIds = false ) {
+ $this->assertEquals( is_array( $results ), is_array( $newResult ), 'Type of result and data do not match' );
+ if ( !is_array( $results ) ) {
+ $this->assertEquals( $results, $newResult, 'Repeated result must be the same as before' );
+ } else {
+ $sort = null;
+ foreach ( $newResult as $key => $value ) {
+ if ( !$numericIds && $sort === null ) {
+ if ( !is_array( $value ) ) {
+ $sort = false;
+ } elseif ( array_key_exists( 'title', $value ) ) {
+ $sort = function ( $a, $b ) {
+ return strcmp( $a['title'], $b['title'] );
+ };
+ } else {
+ $sort = false;
+ }
+ }
+ $keyExists = array_key_exists( $key, $results );
+ if ( is_numeric( $key ) ) {
+ if ( $numericIds ) {
+ if ( !$keyExists ) {
+ $results[$key] = $value;
+ } else {
+ $this->mergeResult( $results[$key], $value );
+ }
+ } else {
+ $results[] = $value;
+ }
+ } elseif ( !$keyExists ) {
+ $results[$key] = $value;
+ } else {
+ $this->mergeResult( $results[$key], $value, $key === 'pages' );
+ }
+ }
+ if ( $numericIds ) {
+ ksort( $results, SORT_NUMERIC );
+ } elseif ( $sort !== null && $sort !== false ) {
+ uasort( $results, $sort );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php
new file mode 100644
index 00000000..1bca2256
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiQueryRevisionsTest extends ApiTestCase {
+
+ /**
+ * @group medium
+ */
+ public function testContentComesWithContentModelAndFormat() {
+ $pageName = 'Help:' . __METHOD__;
+ $title = Title::newFromText( $pageName );
+ $page = WikiPage::factory( $title );
+ $page->doEdit( 'Some text', 'inserting content' );
+
+ $apiResult = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'prop' => 'revisions',
+ 'titles' => $pageName,
+ 'rvprop' => 'content',
+ ) );
+ $this->assertArrayHasKey( 'query', $apiResult[0] );
+ $this->assertArrayHasKey( 'pages', $apiResult[0]['query'] );
+ foreach ( $apiResult[0]['query']['pages'] as $page ) {
+ $this->assertArrayHasKey( 'revisions', $page );
+ foreach ( $page['revisions'] as $revision ) {
+ $this->assertArrayHasKey( 'contentformat', $revision,
+ 'contentformat should be included when asking content so client knows how to interpret it'
+ );
+ $this->assertArrayHasKey( 'contentmodel', $revision,
+ 'contentmodel should be included when asking content so client knows how to interpret it'
+ );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryTest.php b/tests/phpunit/includes/api/query/ApiQueryTest.php
new file mode 100644
index 00000000..f5645555
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryTest.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiQueryTest extends ApiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ public function testTitlesGetNormalized() {
+
+ global $wgMetaNamespace;
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => 'Project:articleA|article_B' ) );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'normalized', $data[0]['query'] );
+
+ // Forge a normalized title
+ $to = Title::newFromText( $wgMetaNamespace . ':ArticleA' );
+
+ $this->assertEquals(
+ array(
+ 'from' => 'Project:articleA',
+ 'to' => $to->getPrefixedText(),
+ ),
+ $data[0]['query']['normalized'][0]
+ );
+
+ $this->assertEquals(
+ array(
+ 'from' => 'article_B',
+ 'to' => 'Article B'
+ ),
+ $data[0]['query']['normalized'][1]
+ );
+ }
+
+ public function testTitlesAreRejectedIfInvalid() {
+ $title = false;
+ while ( !$title || Title::newFromText( $title )->exists() ) {
+ $title = md5( mt_rand( 0, 10000 ) + rand( 0, 999000 ) );
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => $title . '|Talk:' ) );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'pages', $data[0]['query'] );
+ $this->assertEquals( 2, count( $data[0]['query']['pages'] ) );
+
+ $this->assertArrayHasKey( -2, $data[0]['query']['pages'] );
+ $this->assertArrayHasKey( -1, $data[0]['query']['pages'] );
+
+ $this->assertArrayHasKey( 'missing', $data[0]['query']['pages'][-2] );
+ $this->assertArrayHasKey( 'invalid', $data[0]['query']['pages'][-1] );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryTestBase.php b/tests/phpunit/includes/api/query/ApiQueryTestBase.php
new file mode 100644
index 00000000..8ee8ea96
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryTestBase.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ *
+ *
+ * Created on Feb 10, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/** This class has some common functionality for testing query module
+ */
+abstract class ApiQueryTestBase extends ApiTestCase {
+
+ const PARAM_ASSERT = <<<STR
+Each parameter must be an array of two elements,
+first - an array of params to the API call,
+and the second array - expected results as returned by the API
+STR;
+
+ /**
+ * Merges all requests parameter + expected values into one
+ * @param ... list of arrays, each of which contains exactly two
+ * @return array
+ */
+ protected function merge( /*...*/ ) {
+ $request = array();
+ $expected = array();
+ foreach ( func_get_args() as $v ) {
+ list( $req, $exp ) = $this->validateRequestExpectedPair( $v );
+ $request = array_merge_recursive( $request, $req );
+ $this->mergeExpected( $expected, $exp );
+ }
+
+ return array( $request, $expected );
+ }
+
+ /**
+ * Check that the parameter is a valid two element array,
+ * with the first element being API request and the second - expected result
+ */
+ private function validateRequestExpectedPair( $v ) {
+ $this->assertType( 'array', $v, self::PARAM_ASSERT );
+ $this->assertEquals( 2, count( $v ), self::PARAM_ASSERT );
+ $this->assertArrayHasKey( 0, $v, self::PARAM_ASSERT );
+ $this->assertArrayHasKey( 1, $v, self::PARAM_ASSERT );
+ $this->assertType( 'array', $v[0], self::PARAM_ASSERT );
+ $this->assertType( 'array', $v[1], self::PARAM_ASSERT );
+
+ return $v;
+ }
+
+ /**
+ * Recursively merges the expected values in the $item into the $all
+ */
+ private function mergeExpected( &$all, $item ) {
+ foreach ( $item as $k => $v ) {
+ if ( array_key_exists( $k, $all ) ) {
+ if ( is_array( $all[$k] ) ) {
+ $this->mergeExpected( $all[$k], $v );
+ } else {
+ $this->assertEquals( $all[$k], $v );
+ }
+ } else {
+ $all[$k] = $v;
+ }
+ }
+ }
+
+ /**
+ * Checks that the request's result matches the expected results.
+ * @param $values array is a two element array( request, expected_results )
+ * @throws Exception
+ */
+ protected function check( $values ) {
+ list( $req, $exp ) = $this->validateRequestExpectedPair( $values );
+ if ( !array_key_exists( 'action', $req ) ) {
+ $req['action'] = 'query';
+ }
+ foreach ( $req as &$val ) {
+ if ( is_array( $val ) ) {
+ $val = implode( '|', array_unique( $val ) );
+ }
+ }
+ $result = $this->doApiRequest( $req );
+ $this->assertResult( array( 'query' => $exp ), $result[0], $req );
+ }
+
+ protected function assertResult( $exp, $result, $message = '' ) {
+ try {
+ $this->assertResultRecursive( $exp, $result );
+ } catch ( Exception $e ) {
+ if ( is_array( $message ) ) {
+ $message = http_build_query( $message );
+ }
+ print "\nRequest: $message\n";
+ print "\nExpected:\n";
+ print_r( $exp );
+ print "\nResult:\n";
+ print_r( $result );
+ throw $e; // rethrow it
+ }
+ }
+
+ /**
+ * Recursively compare arrays, ignoring mismatches in numeric key and pageids.
+ * @param $expected array expected values
+ * @param $result array returned values
+ */
+ private function assertResultRecursive( $expected, $result ) {
+ reset( $expected );
+ reset( $result );
+ while ( true ) {
+ $e = each( $expected );
+ $r = each( $result );
+ // If either of the arrays is shorter, abort. If both are done, success.
+ $this->assertEquals( (bool)$e, (bool)$r );
+ if ( !$e ) {
+ break; // done
+ }
+ // continue only if keys are identical or both keys are numeric
+ $this->assertTrue( $e['key'] === $r['key'] || ( is_numeric( $e['key'] ) && is_numeric( $r['key'] ) ) );
+ // don't compare pageids
+ if ( $e['key'] !== 'pageid' ) {
+ // If values are arrays, compare recursively, otherwise compare with ===
+ if ( is_array( $e['value'] ) && is_array( $r['value'] ) ) {
+ $this->assertResultRecursive( $e['value'], $r['value'] );
+ } else {
+ $this->assertEquals( $e['value'], $r['value'] );
+ }
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/words.txt b/tests/phpunit/includes/api/words.txt
new file mode 100644
index 00000000..7ce23ee3
--- /dev/null
+++ b/tests/phpunit/includes/api/words.txt
@@ -0,0 +1,1000 @@
+Andaquian
+Anoplanthus
+Araquaju
+Astrophyton
+Avarish
+Batonga
+Bdellidae
+Betoyan
+Bismarck
+Britishness
+Carmen
+Chatillon
+Clement
+Coryphaena
+Croton
+Cyrillianism
+Dagomba
+Decimus
+Dichorisandra
+Duculinae
+Empusa
+Escallonia
+Fathometer
+Fon
+Fundulinae
+Gadswoons
+Gederathite
+Gemini
+Gerbera
+Gregarinida
+Gyracanthus
+Halopsychidae
+Hasidim
+Hemerobius
+Ichthyosauridae
+Iscariot
+Jeames
+Jesuitry
+Jovian
+Judaization
+Katie
+Ladin
+Langhian
+Lapithaean
+Lisette
+Macrochira
+Malaxis
+Malvastrum
+Maranhao
+Marxian
+Maurist
+Metrosideros
+Micky
+Microsporon
+Odacidae
+Ophiuchid
+Osmorhiza
+Paguma
+Palesman
+Papayaceae
+Pastinaca
+Philoxenian
+Pleurostigma
+Rarotongan
+Rhodoraceae
+Rong
+Saho
+Sanyakoan
+Sardanapalian
+Sauropoda
+Sedentaria
+Shambu
+Shukulumbwe
+Solonian
+Spaniardization
+Spirochaetaceae
+Stomatopoda
+Stratiotes
+Taiwanhemp
+Titanically
+Venetianed
+Victrola
+Yuman
+abatis
+abaton
+abjoint
+acanthoma
+acari
+acceptance
+actinography
+acuteness
+addiment
+adelite
+adelomorphic
+adelphogamy
+adipocele
+aelurophobia
+affined
+aflaunt
+agathokakological
+aischrolatreia
+alarmedly
+alebench
+aleurone
+allelotropic
+allerion
+alloplastic
+allowable
+alternacy
+alternariose
+altricial
+ambitionist
+amendment
+amiableness
+amicableness
+ammo
+amortizable
+anchorate
+anemometrically
+angelocracy
+angelological
+anodal
+anomalure
+antedate
+antiagglutinin
+antirationalist
+antiscorbutic
+antisplasher
+antithesize
+antiunionist
+antoecian
+apolegamic
+appropriation
+archididascalian
+archival
+arteriophlebotomy
+articulable
+asseveration
+assignation
+atelo
+atrienses
+atrophy
+atterminement
+atypic
+automower
+aveloz
+awrist
+azteca
+bairnteam
+balsamweed
+bannerman
+beardy
+becry
+beek
+beggarwise
+bescab
+bestness
+bethel
+bewildering
+bibliophilism
+bitterblain
+blakeberyed
+boccarella
+bocedization
+boobyalla
+bourbon
+bowbent
+bowerbird
+brachygnathous
+brail
+branchiferous
+brelaw
+brew
+brideweed
+bridgeable
+brombenzamide
+buddler
+burbankian
+burr
+buskin
+cacochymical
+calefactory
+caliper
+canaliculus
+candidature
+canellaceous
+canniness
+canning
+cantilene
+carbonatation
+carthamic
+caseum
+caudated
+causationist
+ceruleite
+chalder
+chalta
+charmel
+chekan
+chillness
+chirogymnast
+chirpling
+chlorinous
+cholanthrene
+chondroblast
+chromatography
+chromophilous
+chronical
+cicatrice
+cinchonine
+city
+clubbing
+coastal
+coaxially
+coercible
+coeternity
+coff
+coinventor
+collyba
+combinator
+complanation
+comprehensibility
+conchuela
+congenital
+context
+contranatural
+corallum
+cordately
+cornupete
+corolliferous
+coroneted
+corticosterone
+coseat
+cottage
+crocetin
+crossleted
+crottels
+curvedness
+cycadeous
+cyclism
+cylindrically
+cynanche
+cyrtoceratitic
+cystospasm
+danceress
+dancette
+dawny
+daydreamy
+debar
+decarburization
+decorousness
+decrepitness
+delirious
+deozonizer
+dermatosis
+desma
+deutencephalic
+diacetate
+diarthrodial
+diathermy
+dicolic
+dimastigate
+dimidiation
+dipetto
+disavowable
+disintrench
+disman
+dismay
+disorder
+disoxygenation
+dithionous
+dogman
+dragonfly
+dramatical
+drawspan
+drubbly
+drunk
+duskly
+ecderonic
+ectocuniform
+ectocyst
+ehrwaldite
+electrocute
+elemicin
+embracing
+emotionality
+enactment
+enamor
+enclave
+endameba
+endochylous
+endocrinologist
+endolymph
+endothecal
+entasia
+epigeous
+episcopicide
+epitrichial
+erminee
+erraticalness
+eruptivity
+erythrocytoschisis
+esperance
+estuous
+eucrystalline
+eugeny
+evacuant
+everbloomer
+evocation
+exarchateship
+exasperate
+excorticate
+excrementary
+exile
+expandedly
+exponency
+expressionist
+expulsion
+extemporary
+extollation
+extortive
+extrabulbar
+extraprostatic
+facticide
+fairer
+fakery
+fasibitikite
+fatiscent
+fearless
+febrifuge
+ferie
+fibrousness
+fingered
+fisheye
+flagpole
+flagrantness
+fleche
+fluidism
+folliculin
+footbreadth
+forceps
+forecontrive
+forthbring
+foveated
+fuchsin
+fungicidal
+funori
+gamelang
+gametically
+garvanzo
+gasoliner
+gastrophile
+germproof
+gerontism
+gigantical
+glaciology
+godmotherhood
+gooseherd
+gordunite
+gove
+gracilis
+greathead
+grieveship
+guidable
+gyromancy
+gyrostat
+habitus
+hailweed
+handhole
+hangalai
+haznadar
+heliced
+hemihypertrophy
+hemimorphic
+hemistrumectomy
+heptavalent
+heptite
+herbalist
+herpetology
+hesperid
+hexacarbon
+hieromnemon
+hobbyless
+holodactylic
+homoeoarchy
+hopperings
+hospitable
+houseboat
+huh
+huntedly
+hydroponics
+hydrosomal
+hyperdactylia
+hyperperistalsis
+hypogeocarpous
+ideogram
+idiopathical
+illegitimate
+imambarah
+impotently
+improvise
+impuberal
+inaccurately
+incarnant
+inchoation
+incliner
+incredulous
+indiscriminateness
+indulgenced
+inebriation
+inexpressiveness
+infibulate
+inflectedness
+iniome
+ink
+inquietly
+insaturable
+insinuative
+instiller
+institutive
+insultproof
+interactionist
+intercensal
+interpenetrable
+intertranspicuous
+intrinsicality
+inwards
+iridiocyte
+iridoparalysis
+irreportable
+isoprene
+isosmotic
+izard
+jacuaru
+jaculative
+jerkined
+joe
+joyous
+julienne
+justicehood
+kali
+kalidium
+katha
+kathal
+keelage
+keratomycosis
+khaki
+khedival
+kinkily
+knife
+kolo
+kraken
+kwarta
+labba
+labber
+laboress
+lacunar
+latch
+lauric
+lawter
+lectotype
+leeches
+legible
+lepidosteoid
+leucobasalt
+leverer
+libellate
+limnimeter
+lithography
+lithotypic
+locomotor
+logarithmetically
+logistician
+lyncine
+lysogenesis
+machan
+macromyelon
+maharana
+mandibulate
+manganapatite
+marchpane
+mas
+masochistic
+mastaba
+matching
+meditatively
+megalopolitan
+melaniline
+mentum
+mercaptides
+mestome
+metasomatism
+meterless
+micronuclear
+micropetalous
+microreaction
+microsporophore
+mileway
+milliarium
+millisecond
+misbind
+miscollocation
+misreader
+modernicide
+modification
+modulant
+monkfish
+monoamino
+monocarbide
+monographical
+morphinomaniac
+mullein
+munge
+mutilate
+mycophagist
+myelosarcoma
+myospasm
+myriadly
+nagaika
+naphthionate
+natant
+naviculaeform
+nayward
+neallotype
+necrophilia
+nectared
+neigher
+neogamous
+neurodynia
+neurorthopteran
+nidation
+nieceship
+nitrobacteria
+nitrosification
+nogheaded
+nonassertive
+noneuphonious
+nonextant
+nonincrease
+nonintermittent
+nonmetallic
+nonprehensile
+nonremunerative
+nonsocial
+nonvesting
+noontime
+noreaster
+nounal
+nub
+nucleoplasm
+nullisome
+numero
+numerous
+oblongatal
+observe
+obtusilingual
+obvert
+occipitoatlantal
+oceanside
+ochlophobist
+odontiasis
+opalescence
+opticon
+oraculousness
+orarium
+organically
+orthopedically
+ostosis
+overadvance
+overbuilt
+overdiscouragement
+overdoer
+overhardy
+overjocular
+overmagnify
+overofficered
+overpotent
+overprizer
+overrunner
+overshrink
+oversimply
+oversplash
+ovology
+oxskin
+oxychloride
+oxygenant
+ozokerite
+pactional
+palaeoanthropography
+palaeographical
+palaeopsychology
+palliasse
+palpebral
+pandaric
+pantelegraph
+papicolist
+papulate
+parakinetic
+parasitism
+parochialic
+parochialize
+passionlike
+patch
+paucidentate
+pawnbrokeress
+pecite
+pecky
+pedipulation
+pellitory
+perfilograph
+periblast
+perigemmal
+periost
+periplus
+perishable
+periwig
+permansive
+persistingly
+persymmetrical
+phantom
+phasmatrope
+philocaly
+philogyny
+philosophister
+philotherianism
+phorology
+phototrophic
+phrator
+phratral
+phthisipneumony
+physogastry
+phytologic
+phytoptid
+pianograph
+picqueter
+piculet
+pigeoner
+pimaric
+pinesap
+pist
+planometer
+platano
+playful
+plea
+pleuropneumonic
+plowwoman
+plump
+pluviographical
+pneumocele
+podophthalmate
+polyad
+polythalamian
+poppyhead
+portamento
+portmanteau
+portraitlike
+possible
+potassamide
+powderer
+praepubis
+preanesthetic
+prebarbaric
+predealer
+predomination
+prefactory
+preirrigational
+prelector
+presbytership
+presecure
+preservable
+prespecialist
+preventionism
+prewound
+princely
+priorship
+proannexationist
+proanthropos
+probeable
+probouleutic
+profitless
+proplasma
+prosectorial
+protecting
+protochemistry
+protosulphate
+pseudoataxia
+psilology
+psychoneurotic
+pterygial
+publicist
+purgation
+purplishness
+putatively
+pyracene
+pyrenomycete
+pyromancy
+pyrophone
+quadroon
+quailhead
+qualifier
+quaternal
+rabblelike
+rambunctious
+rapidness
+ratably
+rationalism
+razor
+reannoy
+recultivation
+regulable
+reimplant
+reimposition
+reimprison
+reinjure
+reinspiration
+reintroduce
+remantle
+reprehensibility
+reptant
+require
+resteal
+restful
+returnability
+revisableness
+rewash
+rewhirl
+reyield
+rhizotomy
+rhodamine
+rigwiddie
+rimester
+ripper
+rippet
+rockish
+rockwards
+rollicky
+roosters
+rooted
+rosal
+rozum
+saccharated
+sagamore
+sagy
+salesmanship
+salivous
+sallet
+salta
+saprostomous
+satiation
+sauropsid
+sawarra
+sawback
+scabish
+scabrate
+scampavia
+scientificophilosophical
+scirrosity
+scoliometer
+scolopendrelloid
+secantly
+seignioral
+semibull
+semic
+seminarianism
+semiped
+semiprivate
+semispherical
+semispontaneous
+seneschal
+septendecimal
+serotherapist
+servation
+sesquisulphuret
+severish
+sextipartite
+sextubercular
+shipyard
+shuckpen
+siderosis
+silex
+sillyhow
+silverbelly
+silverbelly
+simulacrum
+sisham
+sixte
+skeiner
+skiapod
+slopped
+slubby
+smalts
+sockmaker
+solute
+somethingness
+somnify
+southwester
+spathilla
+spectrochemical
+sphagnology
+spinales
+spiriting
+spirling
+spirochetemia
+spreadboard
+spurflower
+squawdom
+squeezing
+staircase
+staker
+stamphead
+statolith
+stekan
+stellulate
+stinker
+stomodaea
+streamingly
+strikingness
+strouthocamelian
+stuprum
+subacutely
+subboreal
+subcontractor
+subendorsement
+subprofitable
+subserviate
+subsneer
+subungual
+sucuruju
+sugan
+sulphocarbolate
+summerwood
+superficialist
+superinference
+superregenerative
+supplicate
+suspendible
+synchronizer
+syntectic
+tachyglossate
+tailless
+taintment
+takingly
+taletelling
+tarpon
+tasteful
+taxeater
+taxy
+teache
+teachless
+teg
+tegmen
+teletyper
+temperable
+ten
+tenent
+teskere
+testes
+thallogen
+thapsia
+thewness
+thickety
+thiobacteria
+thorniness
+throwing
+thyroprivic
+tinnitus
+tocalote
+tolerationist
+tonalamatl
+torvous
+totality
+tottering
+toug
+tracheopathia
+tragedical
+translucent
+trifoveolate
+trilaurin
+trophoplasmatic
+trunkless
+turbanless
+turnpiker
+twangle
+twitterboned
+ultraornate
+umbilication
+unabatingly
+unabjured
+unadequateness
+unaffectedness
+unarriving
+unassorted
+unattacked
+unbenumbed
+unboasted
+unburning
+uncensorious
+uncongested
+uncontemnedly
+uncontemporary
+uncrook
+uncrystallizability
+uncurb
+uncustomariness
+underbillow
+undercanopy
+underestimation
+underhanging
+underpetticoated
+underpropped
+undersole
+understocking
+underworld
+undevout
+undisappointing
+undistinctive
+unfiscal
+unfluted
+unfreckled
+ungentilize
+unglobe
+unhelped
+unhomogeneously
+unifoliate
+uninflammable
+uninterrogated
+unisonal
+unkindled
+unlikeableness
+unlisty
+unlocked
+unmoving
+unmultipliable
+unnestled
+unnoticed
+unobservable
+unobviated
+unoffensively
+unofficerlike
+unpoetic
+unpractically
+unquestionableness
+unrehearsed
+unrevised
+unrhetorical
+unsadden
+unsaluting
+unscriptural
+unseeking
+unshowed
+unsolicitous
+unsprouted
+unsubjective
+unsubsidized
+unsymbolic
+untenant
+unterrified
+untranquil
+untraversed
+untrusty
+untying
+unwillful
+unwinding
+upspring
+uptwist
+urachovesical
+uropygial
+vagabondism
+varicoid
+varletess
+vasal
+ventrocaudal
+verisimilitude
+vermigerous
+vibrometer
+viminal
+virus
+vocationalism
+voguey
+vulnerability
+waggle
+wamblingly
+warmus
+waxer
+waying
+wedgeable
+wellmaker
+whomever
+wigged
+witchlike
+wokas
+woodrowel
+woodsman
+woolding
+xanthelasmic
+xiphosternum
+yachtman
+yachtsmanlike
+yelp
+zoophytal \ No newline at end of file
diff --git a/tests/phpunit/includes/cache/GenderCacheTest.php b/tests/phpunit/includes/cache/GenderCacheTest.php
new file mode 100644
index 00000000..ce2db5d7
--- /dev/null
+++ b/tests/phpunit/includes/cache/GenderCacheTest.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @group Database
+ * @group Cache
+ */
+class GenderCacheTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ global $wgDefaultUserOptions;
+ parent::setUp();
+ //ensure the correct default gender
+ $wgDefaultUserOptions['gender'] = 'unknown';
+ }
+
+ function addDBData() {
+ $user = User::newFromName( 'UTMale' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTMalePassword' );
+ }
+ //ensure the right gender
+ $user->setOption( 'gender', 'male' );
+ $user->saveSettings();
+
+ $user = User::newFromName( 'UTFemale' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTFemalePassword' );
+ }
+ //ensure the right gender
+ $user->setOption( 'gender', 'female' );
+ $user->saveSettings();
+
+ $user = User::newFromName( 'UTDefaultGender' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTDefaultGenderPassword' );
+ }
+ //ensure the default gender
+ $user->setOption( 'gender', null );
+ $user->saveSettings();
+ }
+
+ /**
+ * test usernames
+ *
+ * @dataProvider provideUserGenders
+ * @covers GenderCache::getGenderOf
+ */
+ public function testUserName( $username, $expectedGender ) {
+ $genderCache = GenderCache::singleton();
+ $gender = $genderCache->getGenderOf( $username );
+ $this->assertEquals( $gender, $expectedGender, "GenderCache normal" );
+ }
+
+ /**
+ * genderCache should work with user objects, too
+ *
+ * @dataProvider provideUserGenders
+ * @covers GenderCache::getGenderOf
+ */
+ public function testUserObjects( $username, $expectedGender ) {
+ $genderCache = GenderCache::singleton();
+ $user = User::newFromName( $username );
+ $gender = $genderCache->getGenderOf( $user );
+ $this->assertEquals( $gender, $expectedGender, "GenderCache normal" );
+ }
+
+ public static function provideUserGenders() {
+ return array(
+ array( 'UTMale', 'male' ),
+ array( 'UTFemale', 'female' ),
+ array( 'UTDefaultGender', 'unknown' ),
+ array( 'UTNotExist', 'unknown' ),
+ //some not valid user
+ array( '127.0.0.1', 'unknown' ),
+ array( 'user@test', 'unknown' ),
+ );
+ }
+
+ /**
+ * test strip of subpages to avoid unnecessary queries
+ * against the never existing username
+ *
+ * @dataProvider provideStripSubpages
+ * @covers GenderCache::getGenderOf
+ */
+ public function testStripSubpages( $pageWithSubpage, $expectedGender ) {
+ $genderCache = GenderCache::singleton();
+ $gender = $genderCache->getGenderOf( $pageWithSubpage );
+ $this->assertEquals( $gender, $expectedGender, "GenderCache must strip of subpages" );
+ }
+
+ public static function provideStripSubpages() {
+ return array(
+ array( 'UTMale/subpage', 'male' ),
+ array( 'UTFemale/subpage', 'female' ),
+ array( 'UTDefaultGender/subpage', 'unknown' ),
+ array( 'UTNotExist/subpage', 'unknown' ),
+ array( '127.0.0.1/subpage', 'unknown' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php
new file mode 100644
index 00000000..803acf73
--- /dev/null
+++ b/tests/phpunit/includes/cache/MessageCacheTest.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @group Database
+ * @group Cache
+ * @covers MessageCache
+ */
+class MessageCacheTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->configureLanguages();
+ MessageCache::singleton()->enable();
+ }
+
+ /**
+ * Helper function -- setup site language for testing
+ */
+ protected function configureLanguages() {
+ // for the test, we need the content language to be anything but English,
+ // let's choose e.g. German (de)
+ $langCode = 'de';
+ $langObj = Language::factory( $langCode );
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => $langCode,
+ 'wgLang' => $langObj,
+ 'wgContLang' => $langObj,
+ ) );
+ }
+
+ function addDBData() {
+ $this->configureLanguages();
+
+ // Set up messages and fallbacks ab -> ru -> de
+ $this->makePage( 'FallbackLanguageTest-Full', 'ab' );
+ $this->makePage( 'FallbackLanguageTest-Full', 'ru' );
+ $this->makePage( 'FallbackLanguageTest-Full', 'de' );
+
+ // Fallbacks where ab does not exist
+ $this->makePage( 'FallbackLanguageTest-Partial', 'ru' );
+ $this->makePage( 'FallbackLanguageTest-Partial', 'de' );
+
+ // Fallback to the content language
+ $this->makePage( 'FallbackLanguageTest-ContLang', 'de' );
+
+ // Add customizations for an existing message.
+ $this->makePage( 'sunday', 'ru' );
+
+ // Full key tests -- always want russian
+ $this->makePage( 'MessageCacheTest-FullKeyTest', 'ab' );
+ $this->makePage( 'MessageCacheTest-FullKeyTest', 'ru' );
+
+ // In content language -- get base if no derivative
+ $this->makePage( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none', false );
+ }
+
+ /**
+ * Helper function for addDBData -- adds a simple page to the database
+ *
+ * @param string $title Title of page to be created
+ * @param string $lang Language and content of the created page
+ * @param string|null $content Content of the created page, or null for a generic string
+ * @param bool $createSubPage Set to false if a root page should be created
+ */
+ protected function makePage( $title, $lang, $content = null, $createSubPage = true ) {
+ global $wgContLang;
+
+ if ( $content === null ) {
+ $content = $lang;
+ }
+ if ( $lang !== $wgContLang->getCode() || $createSubPage ) {
+ $title = "$title/$lang";
+ }
+
+ $title = Title::newFromText( $title, NS_MEDIAWIKI );
+ $wikiPage = new WikiPage( $title );
+ $contentHandler = ContentHandler::makeContent( $content, $title );
+ $wikiPage->doEditContent( $contentHandler, "$lang translation test case" );
+ }
+
+ /**
+ * Test message fallbacks, bug #1495
+ *
+ * @dataProvider provideMessagesForFallback
+ */
+ public function testMessageFallbacks( $message, $lang, $expectedContent ) {
+ $result = MessageCache::singleton()->get( $message, true, $lang );
+ $this->assertEquals( $expectedContent, $result, "Message fallback failed." );
+ }
+
+ function provideMessagesForFallback() {
+ return array(
+ array( 'FallbackLanguageTest-Full', 'ab', 'ab' ),
+ array( 'FallbackLanguageTest-Partial', 'ab', 'ru' ),
+ array( 'FallbackLanguageTest-ContLang', 'ab', 'de' ),
+ array( 'FallbackLanguageTest-None', 'ab', false ),
+
+ // Existing message with customizations on the fallbacks
+ array( 'sunday', 'ab', 'амҽыш' ),
+
+ // bug 46579
+ array( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' ),
+ // UI language different from content language should only use de/none as last option
+ array( 'FallbackLanguageTest-NoDervContLang', 'fit', 'de/none' ),
+ );
+ }
+
+ /**
+ * There's a fallback case where the message key is given as fully qualified -- this
+ * should ignore the passed $lang and use the language from the key
+ *
+ * @dataProvider provideMessagesForFullKeys
+ */
+ public function testFullKeyBehaviour( $message, $lang, $expectedContent ) {
+ $result = MessageCache::singleton()->get( $message, true, $lang, true );
+ $this->assertEquals( $expectedContent, $result, "Full key message fallback failed." );
+ }
+
+ function provideMessagesForFullKeys() {
+ return array(
+ array( 'MessageCacheTest-FullKeyTest/ru', 'ru', 'ru' ),
+ array( 'MessageCacheTest-FullKeyTest/ru', 'ab', 'ru' ),
+ array( 'MessageCacheTest-FullKeyTest/ru/foo', 'ru', false ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php
new file mode 100644
index 00000000..d3793d83
--- /dev/null
+++ b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php
@@ -0,0 +1,237 @@
+<?php
+
+/**
+ * Test for ProcessCacheLRU class.
+ *
+ * Note that it uses the ProcessCacheLRUTestable class which extends some
+ * properties and methods visibility. That class is defined at the end of the
+ * file containing this class.
+ *
+ * @group Cache
+ */
+class ProcessCacheLRUTest extends MediaWikiTestCase {
+
+ /**
+ * Helper to verify emptiness of a cache object.
+ * Compare against an array so we get the cache content difference.
+ */
+ function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) {
+ $this->assertAttributeEquals( array(), 'cache', $cache, $msg );
+ }
+
+ /**
+ * Helper to fill a cache object passed by reference
+ */
+ function fillCache( &$cache, $numEntries ) {
+ // Fill cache with three values
+ for ( $i = 1; $i <= $numEntries; $i++ ) {
+ $cache->set( "cache-key-$i", "prop-$i", "value-$i" );
+ }
+ }
+
+ /**
+ * Generates an array of what would be expected in cache for a given cache
+ * size and a number of entries filled in sequentially
+ */
+ function getExpectedCache( $cacheMaxEntries, $entryToFill ) {
+ $expected = array();
+
+ if ( $entryToFill === 0 ) {
+ # The cache is empty!
+ return array();
+ } elseif ( $entryToFill <= $cacheMaxEntries ) {
+ # Cache is not fully filled
+ $firstKey = 1;
+ } else {
+ # Cache overflowed
+ $firstKey = 1 + $entryToFill - $cacheMaxEntries;
+ }
+
+ $lastKey = $entryToFill;
+
+ for ( $i = $firstKey; $i <= $lastKey; $i++ ) {
+ $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" );
+ }
+
+ return $expected;
+ }
+
+ /**
+ * Highlight diff between assertEquals and assertNotSame
+ */
+ public function testPhpUnitArrayEquality() {
+ $one = array( 'A' => 1, 'B' => 2 );
+ $two = array( 'B' => 2, 'A' => 1 );
+ $this->assertEquals( $one, $two ); // ==
+ $this->assertNotSame( $one, $two ); // ===
+ }
+
+ /**
+ * @dataProvider provideInvalidConstructorArg
+ * @expectedException MWException
+ */
+ public function testConstructorGivenInvalidValue( $maxSize ) {
+ new ProcessCacheLRUTestable( $maxSize );
+ }
+
+ /**
+ * Value which are forbidden by the constructor
+ */
+ public static function provideInvalidConstructorArg() {
+ return array(
+ array( null ),
+ array( array() ),
+ array( new stdClass() ),
+ array( 0 ),
+ array( '5' ),
+ array( -1 ),
+ );
+ }
+
+ public function testAddAndGetAKey() {
+ $oneCache = new ProcessCacheLRUTestable( 1 );
+ $this->assertCacheEmpty( $oneCache );
+
+ // First set just one value
+ $oneCache->set( 'cache-key', 'prop1', 'value1' );
+ $this->assertEquals( 1, $oneCache->getEntriesCount() );
+ $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) );
+ $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) );
+ }
+
+ public function testDeleteOldKey() {
+ $oneCache = new ProcessCacheLRUTestable( 1 );
+ $this->assertCacheEmpty( $oneCache );
+
+ $oneCache->set( 'cache-key', 'prop1', 'value1' );
+ $oneCache->set( 'cache-key', 'prop1', 'value2' );
+ $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) );
+ }
+
+ /**
+ * This test that we properly overflow when filling a cache with
+ * a sequence of always different cache-keys. Meant to verify we correclty
+ * delete the older key.
+ *
+ * @dataProvider provideCacheFilling
+ * @param $cacheMaxEntries Maximum entry the created cache will hold
+ * @param $entryToFill Number of entries to insert in the created cache.
+ */
+ public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) {
+ $cache = new ProcessCacheLRUTestable( $cacheMaxEntries );
+ $this->fillCache( $cache, $entryToFill );
+
+ $this->assertSame(
+ $this->getExpectedCache( $cacheMaxEntries, $entryToFill ),
+ $cache->getCache(),
+ "Filling a $cacheMaxEntries entries cache with $entryToFill entries"
+ );
+ }
+
+ /**
+ * Provider for testFillingCache
+ */
+ public static function provideCacheFilling() {
+ // ($cacheMaxEntries, $entryToFill, $msg='')
+ return array(
+ array( 1, 0 ),
+ array( 1, 1 ),
+ array( 1, 2 ), # overflow
+ array( 5, 33 ), # overflow
+ );
+ }
+
+ /**
+ * Create a cache with only one remaining entry then update
+ * the first inserted entry. Should bump it to the top.
+ */
+ public function testReplaceExistingKeyShouldBumpEntryToTop() {
+ $maxEntries = 3;
+
+ $cache = new ProcessCacheLRUTestable( $maxEntries );
+ // Fill cache leaving just one remaining slot
+ $this->fillCache( $cache, $maxEntries - 1 );
+
+ // Set an existing cache key
+ $cache->set( "cache-key-1", "prop-1", "new-value-for-1" );
+
+ $this->assertSame(
+ array(
+ 'cache-key-2' => array( 'prop-2' => 'value-2' ),
+ 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ),
+ ),
+ $cache->getCache()
+ );
+ }
+
+ public function testRecentlyAccessedKeyStickIn() {
+ $cache = new ProcessCacheLRUTestable( 2 );
+ $cache->set( 'first', 'prop1', 'value1' );
+ $cache->set( 'second', 'prop2', 'value2' );
+
+ // Get first
+ $cache->get( 'first', 'prop1' );
+ // Cache a third value, should invalidate the least used one
+ $cache->set( 'third', 'prop3', 'value3' );
+
+ $this->assertFalse( $cache->has( 'second', 'prop2' ) );
+ }
+
+ /**
+ * This first create a full cache then update the value for the 2nd
+ * filled entry.
+ * Given a cache having 1,2,3 as key, updating 2 should bump 2 to
+ * the top of the queue with the new value: 1,3,2* (* = updated).
+ */
+ public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() {
+ $maxEntries = 3;
+
+ $cache = new ProcessCacheLRUTestable( $maxEntries );
+ $this->fillCache( $cache, $maxEntries );
+
+ // Set an existing cache key
+ $cache->set( "cache-key-2", "prop-2", "new-value-for-2" );
+ $this->assertSame(
+ array(
+ 'cache-key-1' => array( 'prop-1' => 'value-1' ),
+ 'cache-key-3' => array( 'prop-3' => 'value-3' ),
+ 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ),
+ ),
+ $cache->getCache()
+ );
+ $this->assertEquals( 'new-value-for-2',
+ $cache->get( 'cache-key-2', 'prop-2' )
+ );
+ }
+
+ public function testBumpExistingKeyToTop() {
+ $cache = new ProcessCacheLRUTestable( 3 );
+ $this->fillCache( $cache, 3 );
+
+ // Set the very first cache key to a new value
+ $cache->set( "cache-key-1", "prop-1", "new value for 1" );
+ $this->assertEquals(
+ array(
+ 'cache-key-2' => array( 'prop-2' => 'value-2' ),
+ 'cache-key-3' => array( 'prop-3' => 'value-3' ),
+ 'cache-key-1' => array( 'prop-1' => 'new value for 1' ),
+ ),
+ $cache->getCache()
+ );
+ }
+}
+
+/**
+ * Overrides some ProcessCacheLRU methods and properties accessibility.
+ */
+class ProcessCacheLRUTestable extends ProcessCacheLRU {
+ public $cache = array();
+
+ public function getCache() {
+ return $this->cache;
+ }
+
+ public function getEntriesCount() {
+ return count( $this->cache );
+ }
+}
diff --git a/tests/phpunit/includes/content/ContentHandlerTest.php b/tests/phpunit/includes/content/ContentHandlerTest.php
new file mode 100644
index 00000000..aedf594d
--- /dev/null
+++ b/tests/phpunit/includes/content/ContentHandlerTest.php
@@ -0,0 +1,451 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ *
+ * @note Declare that we are using the database, because otherwise we'll fail in the "databaseless" test run.
+ * This is because the LinkHolderArray used by the parser needs database access.
+ *
+ */
+class ContentHandlerTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ global $wgContLang;
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgExtraNamespaces' => array(
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ ),
+ // The below tests assume that namespaces not mentioned here (Help, User, MediaWiki, ..)
+ // default to CONTENT_MODEL_WIKITEXT.
+ 'wgNamespaceContentModels' => array(
+ 12312 => 'testing',
+ ),
+ 'wgContentHandlers' => array(
+ CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler',
+ CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler',
+ CONTENT_MODEL_CSS => 'CssContentHandler',
+ CONTENT_MODEL_TEXT => 'TextContentHandler',
+ 'testing' => 'DummyContentHandlerForTesting',
+ ),
+ ) );
+
+ // Reset namespace cache
+ MWNamespace::getCanonicalNamespaces( true );
+ $wgContLang->resetNamespaces();
+ }
+
+ public function tearDown() {
+ global $wgContLang;
+
+ // Reset namespace cache
+ MWNamespace::getCanonicalNamespaces( true );
+ $wgContLang->resetNamespaces();
+
+ parent::tearDown();
+ }
+
+ public static function dataGetDefaultModelFor() {
+ return array(
+ array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ),
+ array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ),
+ array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetDefaultModelFor
+ * @covers ContentHandler::getDefaultModelFor
+ */
+ public function testGetDefaultModelFor( $title, $expectedModelId ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedModelId, ContentHandler::getDefaultModelFor( $title ) );
+ }
+
+ /**
+ * @dataProvider dataGetDefaultModelFor
+ * @covers ContentHandler::getForTitle
+ */
+ public function testGetForTitle( $title, $expectedContentModel ) {
+ $title = Title::newFromText( $title );
+ $handler = ContentHandler::getForTitle( $title );
+ $this->assertEquals( $expectedContentModel, $handler->getModelID() );
+ }
+
+ public static function dataGetLocalizedName() {
+ return array(
+ array( null, null ),
+ array( "xyzzy", null ),
+
+ // XXX: depends on content language
+ array( CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetLocalizedName
+ * @covers ContentHandler::getLocalizedName
+ */
+ public function testGetLocalizedName( $id, $expected ) {
+ $name = ContentHandler::getLocalizedName( $id );
+
+ if ( $expected ) {
+ $this->assertNotNull( $name, "no name found for content model $id" );
+ $this->assertTrue( preg_match( $expected, $name ) > 0,
+ "content model name for #$id did not match pattern $expected"
+ );
+ } else {
+ $this->assertEquals( $id, $name, "localization of unknown model $id should have "
+ . "fallen back to use the model id directly."
+ );
+ }
+ }
+
+ public static function dataGetPageLanguage() {
+ global $wgLanguageCode;
+
+ return array(
+ array( "Main", $wgLanguageCode ),
+ array( "Dummy:Foo", $wgLanguageCode ),
+ array( "MediaWiki:common.js", 'en' ),
+ array( "User:Foo/common.js", 'en' ),
+ array( "MediaWiki:common.css", 'en' ),
+ array( "User:Foo/common.css", 'en' ),
+ array( "User:Foo", $wgLanguageCode ),
+
+ array( CONTENT_MODEL_JAVASCRIPT, 'javascript' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetPageLanguage
+ * @covers ContentHandler::getPageLanguage
+ */
+ public function testGetPageLanguage( $title, $expected ) {
+ if ( is_string( $title ) ) {
+ $title = Title::newFromText( $title );
+ }
+
+ $expected = wfGetLangObj( $expected );
+
+ $handler = ContentHandler::getForTitle( $title );
+ $lang = $handler->getPageLanguage( $title );
+
+ $this->assertEquals( $expected->getCode(), $lang->getCode() );
+ }
+
+ public static function dataGetContentText_Null() {
+ return array(
+ array( 'fail' ),
+ array( 'serialize' ),
+ array( 'ignore' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetContentText_Null
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_Null( $contentHandlerTextFallback ) {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback );
+
+ $content = null;
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertEquals( '', $text );
+ }
+
+ public static function dataGetContentText_TextContent() {
+ return array(
+ array( 'fail' ),
+ array( 'serialize' ),
+ array( 'ignore' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetContentText_TextContent
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_TextContent( $contentHandlerTextFallback ) {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback );
+
+ $content = new WikitextContent( "hello world" );
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertEquals( $content->getNativeData(), $text );
+ }
+
+ /**
+ * ContentHandler::getContentText should have thrown an exception for non-text Content object
+ * @expectedException MWException
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_NonTextContent_fail() {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', 'fail' );
+
+ $content = new DummyContentForTesting( "hello world" );
+
+ ContentHandler::getContentText( $content );
+ }
+
+ /**
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_NonTextContent_serialize() {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', 'serialize' );
+
+ $content = new DummyContentForTesting( "hello world" );
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertEquals( $content->serialize(), $text );
+ }
+
+ /**
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_NonTextContent_ignore() {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', 'ignore' );
+
+ $content = new DummyContentForTesting( "hello world" );
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertNull( $text );
+ }
+
+ /*
+ public static function makeContent( $text, Title $title, $modelId = null, $format = null ) {}
+ */
+
+ public static function dataMakeContent() {
+ return array(
+ array( 'hallo', 'Help:Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ),
+ array( 'hallo', 'MediaWiki:Test.js', null, null, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ),
+ array( serialize( 'hallo' ), 'Dummy:Test', null, null, "testing", 'hallo', false ),
+
+ array( 'hallo', 'Help:Test', null, CONTENT_FORMAT_WIKITEXT, CONTENT_MODEL_WIKITEXT, 'hallo', false ),
+ array( 'hallo', 'MediaWiki:Test.js', null, CONTENT_FORMAT_JAVASCRIPT, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ),
+ array( serialize( 'hallo' ), 'Dummy:Test', null, "testing", "testing", 'hallo', false ),
+
+ array( 'hallo', 'Help:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ),
+ array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ),
+ array( serialize( 'hallo' ), 'Dummy:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, serialize( 'hallo' ), false ),
+
+ array( 'hallo', 'Help:Test', CONTENT_MODEL_WIKITEXT, "testing", null, null, true ),
+ array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, "testing", null, null, true ),
+ array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, "testing", null, null, true ),
+ );
+ }
+
+ /**
+ * @dataProvider dataMakeContent
+ * @covers ContentHandler::makeContent
+ */
+ public function testMakeContent( $data, $title, $modelId, $format, $expectedModelId, $expectedNativeData, $shouldFail ) {
+ $title = Title::newFromText( $title );
+
+ try {
+ $content = ContentHandler::makeContent( $data, $title, $modelId, $format );
+
+ if ( $shouldFail ) {
+ $this->fail( "ContentHandler::makeContent should have failed!" );
+ }
+
+ $this->assertEquals( $expectedModelId, $content->getModel(), 'bad model id' );
+ $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' );
+ } catch ( MWException $ex ) {
+ if ( !$shouldFail ) {
+ $this->fail( "ContentHandler::makeContent failed unexpectedly: " . $ex->getMessage() );
+ } else {
+ // dummy, so we don't get the "test did not perform any assertions" message.
+ $this->assertTrue( true );
+ }
+ }
+ }
+
+ /*
+ public function testSupportsSections() {
+ $this->markTestIncomplete( "not yet implemented" );
+ }
+ */
+
+ /**
+ * @covers ContentHandler::runLegacyHooks
+ */
+ public function testRunLegacyHooks() {
+ Hooks::register( 'testRunLegacyHooks', __CLASS__ . '::dummyHookHandler' );
+
+ $content = new WikitextContent( 'test text' );
+ $ok = ContentHandler::runLegacyHooks( 'testRunLegacyHooks', array( 'foo', &$content, 'bar' ), false );
+
+ $this->assertTrue( $ok, "runLegacyHooks should have returned true" );
+ $this->assertEquals( "TEST TEXT", $content->getNativeData() );
+ }
+
+ public static function dummyHookHandler( $foo, &$text, $bar ) {
+ if ( $text === null || $text === false ) {
+ return false;
+ }
+
+ $text = strtoupper( $text );
+
+ return true;
+ }
+}
+
+class DummyContentHandlerForTesting extends ContentHandler {
+
+ public function __construct( $dataModel ) {
+ parent::__construct( $dataModel, array( "testing" ) );
+ }
+
+ /**
+ * Serializes Content object of the type supported by this ContentHandler.
+ *
+ * @param Content $content the Content object to serialize
+ * @param null $format the desired serialization format
+ * @return String serialized form of the content
+ */
+ public function serializeContent( Content $content, $format = null ) {
+ return $content->serialize();
+ }
+
+ /**
+ * Unserializes a Content object of the type supported by this ContentHandler.
+ *
+ * @param $blob String serialized form of the content
+ * @param null $format the format used for serialization
+ * @return Content the Content object created by deserializing $blob
+ */
+ public function unserializeContent( $blob, $format = null ) {
+ $d = unserialize( $blob );
+
+ return new DummyContentForTesting( $d );
+ }
+
+ /**
+ * Creates an empty Content object of the type supported by this ContentHandler.
+ *
+ */
+ public function makeEmptyContent() {
+ return new DummyContentForTesting( '' );
+ }
+}
+
+class DummyContentForTesting extends AbstractContent {
+
+ public function __construct( $data ) {
+ parent::__construct( "testing" );
+
+ $this->data = $data;
+ }
+
+ public function serialize( $format = null ) {
+ return serialize( $this->data );
+ }
+
+ /**
+ * @return String a string representing the content in a way useful for building a full text search index.
+ * If no useful representation exists, this method returns an empty string.
+ */
+ public function getTextForSearchIndex() {
+ return '';
+ }
+
+ /**
+ * @return String the wikitext to include when another page includes this content, or false if the content is not
+ * includable in a wikitext page.
+ */
+ public function getWikitextForTransclusion() {
+ return false;
+ }
+
+ /**
+ * Returns a textual representation of the content suitable for use in edit summaries and log messages.
+ *
+ * @param int $maxlength Maximum length of the summary text.
+ * @return string The summary text.
+ */
+ public function getTextForSummary( $maxlength = 250 ) {
+ return '';
+ }
+
+ /**
+ * Returns native represenation of the data. Interpretation depends on the data model used,
+ * as given by getDataModel().
+ *
+ * @return mixed the native representation of the content. Could be a string, a nested array
+ * structure, an object, a binary blob... anything, really.
+ */
+ public function getNativeData() {
+ return $this->data;
+ }
+
+ /**
+ * returns the content's nominal size in bogo-bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+ return strlen( $this->data );
+ }
+
+ /**
+ * Return a copy of this Content object. The following must be true for the object returned
+ * if $copy = $original->copy()
+ *
+ * * get_class($original) === get_class($copy)
+ * * $original->getModel() === $copy->getModel()
+ * * $original->equals( $copy )
+ *
+ * If and only if the Content object is imutable, the copy() method can and should
+ * return $this. That is, $copy === $original may be true, but only for imutable content
+ * objects.
+ *
+ * @return Content. A copy of this object.
+ */
+ public function copy() {
+ return $this;
+ }
+
+ /**
+ * Returns true if this content is countable as a "real" wiki page, provided
+ * that it's also in a countable location (e.g. a current revision in the main namespace).
+ *
+ * @param boolean $hasLinks if it is known whether this content contains links, provide this information here,
+ * to avoid redundant parsing to find out.
+ * @return boolean
+ */
+ public function isCountable( $hasLinks = null ) {
+ return false;
+ }
+
+ /**
+ * @param Title $title
+ * @param null $revId
+ * @param null|ParserOptions $options
+ * @param boolean $generateHtml whether to generate Html (default: true). If false,
+ * the result of calling getText() on the ParserOutput object returned by
+ * this method is undefined.
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+ return new ParserOutput( $this->getNativeData() );
+ }
+}
diff --git a/tests/phpunit/includes/content/CssContentTest.php b/tests/phpunit/includes/content/CssContentTest.php
new file mode 100644
index 00000000..bd6d41fe
--- /dev/null
+++ b/tests/phpunit/includes/content/CssContentTest.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class CssContentTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Anon user
+ $user = new User();
+ $user->setName( '127.0.0.1' );
+
+ $this->setMwGlobals( array(
+ 'wgUser' => $user,
+ 'wgTextModelsToParse' => array(
+ CONTENT_MODEL_CSS,
+ )
+ ) );
+ }
+
+ public function newContent( $text ) {
+ return new CssContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ 'MediaWiki:Test.css',
+ null,
+ "hello <world>\n",
+ "<pre class=\"mw-code mw-css\" dir=\"ltr\">\nhello &lt;world&gt;\n\n</pre>"
+ ),
+ array(
+ 'MediaWiki:Test.css',
+ null,
+ "/* hello [[world]] */\n",
+ "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n/* hello [[world]] */\n\n</pre>",
+ array(
+ 'Links' => array(
+ array( 'World' => 0 )
+ )
+ )
+ ),
+
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @covers CssContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() );
+ }
+
+ /**
+ * @covers CssContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new CssContent( 'hallo' ), null, false ),
+ array( new CssContent( 'hallo' ), new CssContent( 'hallo' ), true ),
+ array( new CssContent( 'hallo' ), new WikitextContent( 'hallo' ), false ),
+ array( new CssContent( 'hallo' ), new CssContent( 'HALLO' ), false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataEquals
+ * @covers CssContent::equals
+ */
+ public function testEquals( Content $a, Content $b = null, $equal = false ) {
+ $this->assertEquals( $equal, $a->equals( $b ) );
+ }
+}
diff --git a/tests/phpunit/includes/content/JavaScriptContentTest.php b/tests/phpunit/includes/content/JavaScriptContentTest.php
new file mode 100644
index 00000000..c8616ff0
--- /dev/null
+++ b/tests/phpunit/includes/content/JavaScriptContentTest.php
@@ -0,0 +1,287 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class JavaScriptContentTest extends TextContentTest {
+
+ public function newContent( $text ) {
+ return new JavaScriptContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ 'MediaWiki:Test.js',
+ null,
+ "hello <world>\n",
+ "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello &lt;world&gt;\n\n</pre>"
+ ),
+ array(
+ 'MediaWiki:Test.js',
+ null,
+ "hello(); // [[world]]\n",
+ "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello(); // [[world]]\n\n</pre>",
+ array(
+ 'Links' => array(
+ array( 'World' => 0 )
+ )
+ )
+ ),
+
+ // TODO: more...?
+ );
+ }
+
+ // XXX: Unused function
+ public static function dataGetSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ '0',
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '2',
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '8',
+ null
+ ),
+ );
+ }
+
+ // XXX: Unused function
+ public static function dataReplaceSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ '0',
+ 'No more',
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '',
+ 'No more',
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '2',
+ "== TEST ==\nmore fun",
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '8',
+ 'No more',
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ 'new',
+ 'No more',
+ 'New',
+ null
+ ),
+ );
+ }
+
+ /**
+ * @covers JavaScriptContent::addSectionHeader
+ */
+ public function testAddSectionHeader() {
+ $content = $this->newContent( 'hello world' );
+ $c = $content->addSectionHeader( 'test' );
+
+ $this->assertTrue( $content->equals( $c ) );
+ }
+
+ // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional.
+ public static function dataPreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ array( " Foo \n ",
+ " Foo",
+ ),
+ );
+ }
+
+ public static function dataPreloadTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ 'hello this is ~~~',
+ ),
+ array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+ 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+ ),
+ );
+ }
+
+ public static function dataGetRedirectTarget() {
+ return array(
+ array( '#REDIRECT [[Test]]',
+ null,
+ ),
+ array( '#REDIRECT Test',
+ null,
+ ),
+ array( '* #REDIRECT [[Test]]',
+ null,
+ ),
+ );
+ }
+
+ /**
+ * @todo Test needs database!
+ */
+ /*
+ public function getRedirectChain() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectArray( $text );
+ }
+ */
+
+ /**
+ * @todo Test needs database!
+ */
+ /*
+ public function getUltimateRedirectTarget() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectRecurse( $text );
+ }
+ */
+
+ public static function dataIsCountable() {
+ return array(
+ array( '',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo, bar',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo',
+ null,
+ 'link',
+ false
+ ),
+ array( 'Foo [[bar]]',
+ null,
+ 'link',
+ false
+ ),
+ array( 'Foo',
+ true,
+ 'link',
+ false
+ ),
+ array( 'Foo [[bar]]',
+ false,
+ 'link',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'any',
+ true
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'comma',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'link',
+ false
+ ),
+ );
+ }
+
+ public static function dataGetTextForSummary() {
+ return array(
+ array( "hello\nworld.",
+ 16,
+ 'hello world.',
+ ),
+ array( 'hello world.',
+ 8,
+ 'hello...',
+ ),
+ array( '[[hello world]].',
+ 8,
+ '[[hel...',
+ ),
+ );
+ }
+
+ /**
+ * @covers JavaScriptContent::matchMagicWord
+ */
+ public function testMatchMagicWord() {
+ $mw = MagicWord::get( "staticredirect" );
+
+ $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
+ $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word, since it's not wikitext" );
+ }
+
+ /**
+ * @covers JavaScriptContent::updateRedirect
+ */
+ public function testUpdateRedirect() {
+ $target = Title::newFromText( "testUpdateRedirect_target" );
+
+ $content = $this->newContent( "#REDIRECT [[Someplace]]" );
+ $newContent = $content->updateRedirect( $target );
+
+ $this->assertTrue( $content->equals( $newContent ), "content should be unchanged since it's not wikitext" );
+ }
+
+ /**
+ * @covers JavaScriptContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModel() );
+ }
+
+ /**
+ * @covers JavaScriptContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new JavaScriptContent( "hallo" ), null, false ),
+ array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "hallo" ), true ),
+ array( new JavaScriptContent( "hallo" ), new CssContent( "hallo" ), false ),
+ array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "HALLO" ), false ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/content/TextContentTest.php b/tests/phpunit/includes/content/TextContentTest.php
new file mode 100644
index 00000000..a1f099f3
--- /dev/null
+++ b/tests/phpunit/includes/content/TextContentTest.php
@@ -0,0 +1,458 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class TextContentTest extends MediaWikiLangTestCase {
+ protected $context;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Anon user
+ $user = new User();
+ $user->setName( '127.0.0.1' );
+
+ $this->setMwGlobals( array(
+ 'wgUser' => $user,
+ 'wgTextModelsToParse' => array(
+ CONTENT_MODEL_WIKITEXT,
+ CONTENT_MODEL_CSS,
+ CONTENT_MODEL_JAVASCRIPT,
+ ),
+ 'wgUseTidy' => false,
+ 'wgAlwaysUseTidy' => false,
+ ) );
+
+ $this->context = new RequestContext( new FauxRequest() );
+ $this->context->setTitle( Title::newFromText( 'Test' ) );
+ $this->context->setUser( $user );
+ }
+
+ public function newContent( $text ) {
+ return new TextContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ 'TextContentTest_testGetParserOutput',
+ CONTENT_MODEL_TEXT,
+ "hello ''world'' & [[stuff]]\n", "hello ''world'' &amp; [[stuff]]",
+ array(
+ 'Links' => array()
+ )
+ ),
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @dataProvider dataGetParserOutput
+ * @covers TextContent::getParserOutput
+ */
+ public function testGetParserOutput( $title, $model, $text, $expectedHtml, $expectedFields = null ) {
+ $title = Title::newFromText( $title );
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $po = $content->getParserOutput( $title );
+
+ $html = $po->getText();
+ $html = preg_replace( '#<!--.*?-->#sm', '', $html ); // strip comments
+
+ $this->assertEquals( $expectedHtml, trim( $html ) );
+
+ if ( $expectedFields ) {
+ foreach ( $expectedFields as $field => $exp ) {
+ $f = 'get' . ucfirst( $field );
+ $v = call_user_func( array( $po, $f ) );
+
+ if ( is_array( $exp ) ) {
+ $this->assertArrayEquals( $exp, $v );
+ } else {
+ $this->assertEquals( $exp, $v );
+ }
+ }
+ }
+
+ // TODO: assert more properties
+ }
+
+ public static function dataPreSaveTransform() {
+ return array(
+ array(
+ #0: no signature resolution
+ 'hello this is ~~~',
+ 'hello this is ~~~',
+ ),
+ array(
+ #1: rtrim
+ " Foo \n ",
+ ' Foo',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataPreSaveTransform
+ * @covers TextContent::preSaveTransform
+ */
+ public function testPreSaveTransform( $text, $expected ) {
+ global $wgContLang;
+
+ $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang );
+
+ $content = $this->newContent( $text );
+ $content = $content->preSaveTransform( $this->context->getTitle(), $this->context->getUser(), $options );
+
+ $this->assertEquals( $expected, $content->getNativeData() );
+ }
+
+ public static function dataPreloadTransform() {
+ return array(
+ array(
+ 'hello this is ~~~',
+ 'hello this is ~~~',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataPreloadTransform
+ * @covers TextContent::preloadTransform
+ */
+ public function testPreloadTransform( $text, $expected ) {
+ global $wgContLang;
+ $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang );
+
+ $content = $this->newContent( $text );
+ $content = $content->preloadTransform( $this->context->getTitle(), $options );
+
+ $this->assertEquals( $expected, $content->getNativeData() );
+ }
+
+ public static function dataGetRedirectTarget() {
+ return array(
+ array( '#REDIRECT [[Test]]',
+ null,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetRedirectTarget
+ * @covers TextContent::getRedirectTarget
+ */
+ public function testGetRedirectTarget( $text, $expected ) {
+ $content = $this->newContent( $text );
+ $t = $content->getRedirectTarget();
+
+ if ( is_null( $expected ) ) {
+ $this->assertNull( $t, "text should not have generated a redirect target: $text" );
+ } else {
+ $this->assertEquals( $expected, $t->getPrefixedText() );
+ }
+ }
+
+ /**
+ * @dataProvider dataGetRedirectTarget
+ * @covers TextContent::isRedirect
+ */
+ public function testIsRedirect( $text, $expected ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( !is_null( $expected ), $content->isRedirect() );
+ }
+
+ /**
+ * @todo Test needs database! Should be done by a test class in the Database group.
+ */
+ /*
+ public function getRedirectChain() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectArray( $text );
+ }
+ */
+
+ /**
+ * @todo Test needs database! Should be done by a test class in the Database group.
+ */
+ /*
+ public function getUltimateRedirectTarget() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectRecurse( $text );
+ }
+ */
+
+ public static function dataIsCountable() {
+ return array(
+ array( '',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo, bar',
+ null,
+ 'comma',
+ false
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataIsCountable
+ * @group Database
+ * @covers TextContent::isCountable
+ */
+ public function testIsCountable( $text, $hasLinks, $mode, $expected ) {
+ $this->setMwGlobals( 'wgArticleCountMethod', $mode );
+
+ $content = $this->newContent( $text );
+
+ $v = $content->isCountable( $hasLinks, $this->context->getTitle() );
+
+ $this->assertEquals( $expected, $v, 'isCountable() returned unexpected value ' . var_export( $v, true )
+ . ' instead of ' . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
+ }
+
+ public static function dataGetTextForSummary() {
+ return array(
+ array( "hello\nworld.",
+ 16,
+ 'hello world.',
+ ),
+ array( 'hello world.',
+ 8,
+ 'hello...',
+ ),
+ array( '[[hello world]].',
+ 8,
+ '[[hel...',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetTextForSummary
+ * @covers TextContent::getTextForSummary
+ */
+ public function testGetTextForSummary( $text, $maxlength, $expected ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) );
+ }
+
+ /**
+ * @covers TextContent::getTextForSearchIndex
+ */
+ public function testGetTextForSearchIndex() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getTextForSearchIndex() );
+ }
+
+ /**
+ * @covers TextContent::copy
+ */
+ public function testCopy() {
+ $content = $this->newContent( 'hello world.' );
+ $copy = $content->copy();
+
+ $this->assertTrue( $content->equals( $copy ), 'copy must be equal to original' );
+ $this->assertEquals( 'hello world.', $copy->getNativeData() );
+ }
+
+ /**
+ * @covers TextContent::getSize
+ */
+ public function testGetSize() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 12, $content->getSize() );
+ }
+
+ /**
+ * @covers TextContent::getNativeData
+ */
+ public function testGetNativeData() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getNativeData() );
+ }
+
+ /**
+ * @covers TextContent::getWikitextForTransclusion
+ */
+ public function testGetWikitextForTransclusion() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getWikitextForTransclusion() );
+ }
+
+ /**
+ * @covers TextContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_TEXT, $content->getModel() );
+ }
+
+ /**
+ * @covers TextContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_TEXT, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataIsEmpty() {
+ return array(
+ array( '', true ),
+ array( ' ', false ),
+ array( '0', false ),
+ array( 'hallo welt.', false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataIsEmpty
+ * @covers TextContent::isEmpty
+ */
+ public function testIsEmpty( $text, $empty ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( $empty, $content->isEmpty() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new TextContent( "hallo" ), null, false ),
+ array( new TextContent( "hallo" ), new TextContent( "hallo" ), true ),
+ array( new TextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ),
+ array( new TextContent( "hallo" ), new WikitextContent( "hallo" ), false ),
+ array( new TextContent( "hallo" ), new TextContent( "HALLO" ), false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataEquals
+ * @covers TextContent::equals
+ */
+ public function testEquals( Content $a, Content $b = null, $equal = false ) {
+ $this->assertEquals( $equal, $a->equals( $b ) );
+ }
+
+ public static function dataGetDeletionUpdates() {
+ return array(
+ array( "TextContentTest_testGetSecondaryDataUpdates_1",
+ CONTENT_MODEL_TEXT, "hello ''world''\n",
+ array()
+ ),
+ array( "TextContentTest_testGetSecondaryDataUpdates_2",
+ CONTENT_MODEL_TEXT, "hello [[world test 21344]]\n",
+ array()
+ ),
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @dataProvider dataGetDeletionUpdates
+ * @covers TextContent::getDeletionUpdates
+ */
+ public function testDeletionUpdates( $title, $model, $text, $expectedStuff ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $page = WikiPage::factory( $title );
+ $page->doEditContent( $content, '' );
+
+ $updates = $content->getDeletionUpdates( $page );
+
+ // make updates accessible by class name
+ foreach ( $updates as $update ) {
+ $class = get_class( $update );
+ $updates[$class] = $update;
+ }
+
+ if ( !$expectedStuff ) {
+ $this->assertTrue( true ); // make phpunit happy
+ return;
+ }
+
+ foreach ( $expectedStuff as $class => $fieldValues ) {
+ $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
+
+ $update = $updates[$class];
+
+ foreach ( $fieldValues as $field => $value ) {
+ $v = $update->$field; #if the field doesn't exist, just crash and burn
+ $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
+ }
+ }
+
+ $page->doDeleteArticle( '' );
+ }
+
+ public static function provideConvert() {
+ return array(
+ array( // #0
+ 'Hallo Welt',
+ CONTENT_MODEL_WIKITEXT,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ array( // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_WIKITEXT,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ array( // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_CSS,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ array( // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_JAVASCRIPT,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideConvert
+ * @covers TextContent::convert
+ */
+ public function testConvert( $text, $model, $lossy, $expectedNative ) {
+ $content = $this->newContent( $text );
+
+ $converted = $content->convert( $model, $lossy );
+
+ if ( $expectedNative === false ) {
+ $this->assertFalse( $converted, "conversion to $model was expected to fail!" );
+ } else {
+ $this->assertInstanceOf( 'Content', $converted );
+ $this->assertEquals( $expectedNative, $converted->getNativeData() );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/content/WikitextContentHandlerTest.php b/tests/phpunit/includes/content/WikitextContentHandlerTest.php
new file mode 100644
index 00000000..75a72784
--- /dev/null
+++ b/tests/phpunit/includes/content/WikitextContentHandlerTest.php
@@ -0,0 +1,227 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class WikitextContentHandlerTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var ContentHandler
+ */
+ var $handler;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
+ }
+
+ /**
+ * @covers WikitextContentHandler::serializeContent
+ */
+ public function testSerializeContent() {
+ $content = new WikitextContent( 'hello world' );
+
+ $this->assertEquals( 'hello world', $this->handler->serializeContent( $content ) );
+ $this->assertEquals( 'hello world', $this->handler->serializeContent( $content, CONTENT_FORMAT_WIKITEXT ) );
+
+ try {
+ $this->handler->serializeContent( $content, 'dummy/foo' );
+ $this->fail( "serializeContent() should have failed on unknown format" );
+ } catch ( MWException $e ) {
+ // ok, as expected
+ }
+ }
+
+ /**
+ * @covers WikitextContentHandler::unserializeContent
+ */
+ public function testUnserializeContent() {
+ $content = $this->handler->unserializeContent( 'hello world' );
+ $this->assertEquals( 'hello world', $content->getNativeData() );
+
+ $content = $this->handler->unserializeContent( 'hello world', CONTENT_FORMAT_WIKITEXT );
+ $this->assertEquals( 'hello world', $content->getNativeData() );
+
+ try {
+ $this->handler->unserializeContent( 'hello world', 'dummy/foo' );
+ $this->fail( "unserializeContent() should have failed on unknown format" );
+ } catch ( MWException $e ) {
+ // ok, as expected
+ }
+ }
+
+ /**
+ * @covers WikitextContentHandler::makeEmptyContent
+ */
+ public function testMakeEmptyContent() {
+ $content = $this->handler->makeEmptyContent();
+
+ $this->assertTrue( $content->isEmpty() );
+ $this->assertEquals( '', $content->getNativeData() );
+ }
+
+ public static function dataIsSupportedFormat() {
+ return array(
+ array( null, true ),
+ array( CONTENT_FORMAT_WIKITEXT, true ),
+ array( 99887766, false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideMakeRedirectContent
+ * @param Title|string $title Title object or string for Title::newFromText()
+ * @param string $expected Serialized form of the content object built
+ * @covers WikitextContentHandler::makeRedirectContent
+ */
+ public function testMakeRedirectContent( $title, $expected ) {
+ global $wgContLang;
+ $wgContLang->resetNamespaces();
+
+ if ( is_string( $title ) ) {
+ $title = Title::newFromText( $title );
+ }
+ $content = $this->handler->makeRedirectContent( $title );
+ $this->assertEquals( $expected, $content->serialize() );
+ }
+
+ public static function provideMakeRedirectContent() {
+ return array(
+ array( 'Hello', '#REDIRECT [[Hello]]' ),
+ array( 'Template:Hello', '#REDIRECT [[Template:Hello]]' ),
+ array( 'Hello#section', '#REDIRECT [[Hello#section]]' ),
+ array( 'user:john_doe#section', '#REDIRECT [[User:John doe#section]]' ),
+ array( 'MEDIAWIKI:FOOBAR', '#REDIRECT [[MediaWiki:FOOBAR]]' ),
+ array( 'Category:Foo', '#REDIRECT [[:Category:Foo]]' ),
+ array( Title::makeTitle( NS_MAIN, 'en:Foo' ), '#REDIRECT [[en:Foo]]' ),
+ array( Title::makeTitle( NS_MAIN, 'Foo', '', 'en' ), '#REDIRECT [[:en:Foo]]' ),
+ array( Title::makeTitle( NS_MAIN, 'Bar', 'fragment', 'google' ), '#REDIRECT [[google:Bar#fragment]]' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataIsSupportedFormat
+ * @covers WikitextContentHandler::isSupportedFormat
+ */
+ public function testIsSupportedFormat( $format, $supported ) {
+ $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) );
+ }
+
+ public static function dataMerge3() {
+ return array(
+ array(
+ "first paragraph
+
+ second paragraph\n",
+
+ "FIRST paragraph
+
+ second paragraph\n",
+
+ "first paragraph
+
+ SECOND paragraph\n",
+
+ "FIRST paragraph
+
+ SECOND paragraph\n",
+ ),
+
+ array( "first paragraph
+ second paragraph\n",
+
+ "Bla bla\n",
+
+ "Blubberdibla\n",
+
+ false,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataMerge3
+ * @covers WikitextContentHandler::merge3
+ */
+ public function testMerge3( $old, $mine, $yours, $expected ) {
+ $this->checkHasDiff3();
+
+ // test merge
+ $oldContent = new WikitextContent( $old );
+ $myContent = new WikitextContent( $mine );
+ $yourContent = new WikitextContent( $yours );
+
+ $merged = $this->handler->merge3( $oldContent, $myContent, $yourContent );
+
+ $this->assertEquals( $expected, $merged ? $merged->getNativeData() : $merged );
+ }
+
+ public static function dataGetAutosummary() {
+ return array(
+ array(
+ 'Hello there, world!',
+ '#REDIRECT [[Foo]]',
+ 0,
+ '/^Redirected page .*Foo/'
+ ),
+
+ array(
+ null,
+ 'Hello world!',
+ EDIT_NEW,
+ '/^Created page .*Hello/'
+ ),
+
+ array(
+ 'Hello there, world!',
+ '',
+ 0,
+ '/^Blanked/'
+ ),
+
+ array(
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
+ labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et
+ ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
+ 'Hello world!',
+ 0,
+ '/^Replaced .*Hello/'
+ ),
+
+ array(
+ 'foo',
+ 'bar',
+ 0,
+ '/^$/'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetAutosummary
+ * @covers WikitextContentHandler::getAutosummary
+ */
+ public function testGetAutosummary( $old, $new, $flags, $expected ) {
+ $oldContent = is_null( $old ) ? null : new WikitextContent( $old );
+ $newContent = is_null( $new ) ? null : new WikitextContent( $new );
+
+ $summary = $this->handler->getAutosummary( $oldContent, $newContent, $flags );
+
+ $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" );
+ }
+
+ /**
+ * @todo Text case requires database, should be done by a test class in the Database group
+ */
+ /*
+ public function testGetAutoDeleteReason( Title $title, &$hasHistory ) {}
+ */
+
+ /**
+ * @todo Text case requires database, should be done by a test class in the Database group
+ */
+ /*
+ public function testGetUndoContent( Revision $current, Revision $undo, Revision $undoafter = null ) {}
+ */
+}
diff --git a/tests/phpunit/includes/content/WikitextContentTest.php b/tests/phpunit/includes/content/WikitextContentTest.php
new file mode 100644
index 00000000..9f20073d
--- /dev/null
+++ b/tests/phpunit/includes/content/WikitextContentTest.php
@@ -0,0 +1,404 @@
+<?php
+
+/**
+ * @group ContentHandler
+ *
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class WikitextContentTest extends TextContentTest {
+ static $sections = "Intro
+
+== stuff ==
+hello world
+
+== test ==
+just a test
+
+== foo ==
+more stuff
+";
+
+ public function newContent( $text ) {
+ return new WikitextContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ "WikitextContentTest_testGetParserOutput",
+ CONTENT_MODEL_WIKITEXT,
+ "hello ''world''\n",
+ "<p>hello <i>world</i>\n</p>"
+ ),
+ // TODO: more...?
+ );
+ }
+
+ public static function dataGetSecondaryDataUpdates() {
+ return array(
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_1",
+ CONTENT_MODEL_WIKITEXT, "hello ''world''\n",
+ array(
+ 'LinksUpdate' => array(
+ 'mRecursive' => true,
+ 'mLinks' => array()
+ )
+ )
+ ),
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_2",
+ CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n",
+ array(
+ 'LinksUpdate' => array(
+ 'mRecursive' => true,
+ 'mLinks' => array(
+ array( 'World_test_21344' => 0 )
+ )
+ )
+ )
+ ),
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @dataProvider dataGetSecondaryDataUpdates
+ * @group Database
+ * @covers WikitextContent::getSecondaryDataUpdates
+ */
+ public function testGetSecondaryDataUpdates( $title, $model, $text, $expectedStuff ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $page = WikiPage::factory( $title );
+ $page->doEditContent( $content, '' );
+
+ $updates = $content->getSecondaryDataUpdates( $title );
+
+ // make updates accessible by class name
+ foreach ( $updates as $update ) {
+ $class = get_class( $update );
+ $updates[$class] = $update;
+ }
+
+ foreach ( $expectedStuff as $class => $fieldValues ) {
+ $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
+
+ $update = $updates[$class];
+
+ foreach ( $fieldValues as $field => $value ) {
+ $v = $update->$field; #if the field doesn't exist, just crash and burn
+ $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
+ }
+ }
+
+ $page->doDeleteArticle( '' );
+ }
+
+ public static function dataGetSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ "0",
+ "Intro"
+ ),
+ array( WikitextContentTest::$sections,
+ "2",
+ "== test ==
+just a test"
+ ),
+ array( WikitextContentTest::$sections,
+ "8",
+ false
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetSection
+ * @covers WikitextContent::getSection
+ */
+ public function testGetSection( $text, $sectionId, $expectedText ) {
+ $content = $this->newContent( $text );
+
+ $sectionContent = $content->getSection( $sectionId );
+ if ( is_object( $sectionContent ) ) {
+ $sectionText = $sectionContent->getNativeData();
+ } else {
+ $sectionText = $sectionContent;
+ }
+
+ $this->assertEquals( $expectedText, $sectionText );
+ }
+
+ public static function dataReplaceSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ "0",
+ "No more",
+ null,
+ trim( preg_replace( '/^Intro/sm', 'No more', WikitextContentTest::$sections ) )
+ ),
+ array( WikitextContentTest::$sections,
+ "",
+ "No more",
+ null,
+ "No more"
+ ),
+ array( WikitextContentTest::$sections,
+ "2",
+ "== TEST ==\nmore fun",
+ null,
+ trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikitextContentTest::$sections ) )
+ ),
+ array( WikitextContentTest::$sections,
+ "8",
+ "No more",
+ null,
+ WikitextContentTest::$sections
+ ),
+ array( WikitextContentTest::$sections,
+ "new",
+ "No more",
+ "New",
+ trim( WikitextContentTest::$sections ) . "\n\n\n== New ==\n\nNo more"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikitextContent::replaceSection
+ */
+ public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) {
+ $content = $this->newContent( $text );
+ $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle );
+
+ $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() );
+ }
+
+ /**
+ * @covers WikitextContent::addSectionHeader
+ */
+ public function testAddSectionHeader() {
+ $content = $this->newContent( 'hello world' );
+ $content = $content->addSectionHeader( 'test' );
+
+ $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() );
+ }
+
+ public static function dataPreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ array( // rtrim
+ " Foo \n ",
+ " Foo",
+ ),
+ );
+ }
+
+ public static function dataPreloadTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is ~~~",
+ ),
+ array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+ 'hello \'\'this\'\' is bar',
+ ),
+ );
+ }
+
+ public static function dataGetRedirectTarget() {
+ return array(
+ array( '#REDIRECT [[Test]]',
+ 'Test',
+ ),
+ array( '#REDIRECT Test',
+ null,
+ ),
+ array( '* #REDIRECT [[Test]]',
+ null,
+ ),
+ );
+ }
+
+ public static function dataGetTextForSummary() {
+ return array(
+ array( "hello\nworld.",
+ 16,
+ 'hello world.',
+ ),
+ array( 'hello world.',
+ 8,
+ 'hello...',
+ ),
+ array( '[[hello world]].',
+ 8,
+ 'hel...',
+ ),
+ );
+ }
+
+ /**
+ * @todo Test needs database! Should be done by a test class in the Database group.
+ */
+ /*
+ public function getRedirectChain() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectArray( $text );
+ }
+ */
+
+ /**
+ * @todo Test needs database! Should be done by a test class in the Database group.
+ */
+ /*
+ public function getUltimateRedirectTarget() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectRecurse( $text );
+ }
+ */
+
+ public static function dataIsCountable() {
+ return array(
+ array( '',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo, bar',
+ null,
+ 'comma',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'link',
+ false
+ ),
+ array( 'Foo [[bar]]',
+ null,
+ 'link',
+ true
+ ),
+ array( 'Foo',
+ true,
+ 'link',
+ true
+ ),
+ array( 'Foo [[bar]]',
+ false,
+ 'link',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'any',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'comma',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'link',
+ false
+ ),
+ );
+ }
+
+ /**
+ * @covers WikitextContent::matchMagicWord
+ */
+ public function testMatchMagicWord() {
+ $mw = MagicWord::get( "staticredirect" );
+
+ $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
+ $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" );
+
+ $content = $this->newContent( "#REDIRECT [[FOO]]" );
+ $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word" );
+ }
+
+ /**
+ * @covers WikitextContent::updateRedirect
+ */
+ public function testUpdateRedirect() {
+ $target = Title::newFromText( "testUpdateRedirect_target" );
+
+ // test with non-redirect page
+ $content = $this->newContent( "hello world." );
+ $newContent = $content->updateRedirect( $target );
+
+ $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" );
+
+ // test with actual redirect
+ $content = $this->newContent( "#REDIRECT [[Someplace]]" );
+ $newContent = $content->updateRedirect( $target );
+
+ $this->assertFalse( $content->equals( $newContent ), "content should have changed" );
+ $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" );
+
+ $this->assertEquals( $target->getFullText(), $newContent->getRedirectTarget()->getFullText() );
+ }
+
+ /**
+ * @covers WikitextContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() );
+ }
+
+ /**
+ * @covers WikitextContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new WikitextContent( "hallo" ), null, false ),
+ array( new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ),
+ array( new WikitextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ),
+ array( new WikitextContent( "hallo" ), new TextContent( "hallo" ), false ),
+ array( new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ),
+ );
+ }
+
+ public static function dataGetDeletionUpdates() {
+ return array(
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_1",
+ CONTENT_MODEL_WIKITEXT, "hello ''world''\n",
+ array( 'LinksDeletionUpdate' => array() )
+ ),
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_2",
+ CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n",
+ array( 'LinksDeletionUpdate' => array() )
+ ),
+ // @todo more...?
+ );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php b/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
new file mode 100644
index 00000000..ba63c091
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ * Holds tests for DatabaseMysqlBase MediaWiki class.
+ *
+ * @section LICENSE
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Antoine Musso
+ * @author Bryan Davis
+ * @copyright © 2013 Antoine Musso
+ * @copyright © 2013 Bryan Davis
+ * @copyright © 2013 Wikimedia Foundation Inc.
+ */
+
+/**
+ * Fake class around abstract class so we can call concrete methods.
+ */
+class FakeDatabaseMysqlBase extends DatabaseMysqlBase {
+ // From DatabaseBase
+ protected function closeConnection() {}
+ protected function doQuery( $sql ) {}
+
+ // From DatabaseMysql
+ protected function mysqlConnect( $realServer ) {}
+ protected function mysqlFreeResult( $res ) {}
+ protected function mysqlFetchObject( $res ) {}
+ protected function mysqlFetchArray( $res ) {}
+ protected function mysqlNumRows( $res ) {}
+ protected function mysqlNumFields( $res ) {}
+ protected function mysqlFieldName( $res, $n ) {}
+ protected function mysqlDataSeek( $res, $row ) {}
+ protected function mysqlError( $conn = null ) {}
+ protected function mysqlFetchField( $res, $n ) {}
+ protected function mysqlPing() {}
+
+ // From interface DatabaseType
+ function insertId() {}
+ function lastErrno() {}
+ function affectedRows() {}
+ function getServerVersion() {}
+}
+
+class DatabaseMysqlBaseTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideDiapers
+ * @covers DatabaseMysqlBase::addIdentifierQuotes
+ */
+ public function testAddIdentifierQuotes( $expected, $in ) {
+ $db = new FakeDatabaseMysqlBase();
+ $quoted = $db->addIdentifierQuotes( $in );
+ $this->assertEquals($expected, $quoted);
+ }
+
+
+ /**
+ * Feeds testAddIdentifierQuotes
+ *
+ * Named per bug 20281 convention.
+ */
+ function provideDiapers() {
+ return array(
+ // Format: expected, input
+ array( '``', '' ),
+
+ // Yeah I really hate loosely typed PHP idiocies nowadays
+ array( '``', null ),
+
+ // Dear codereviewer, guess what addIdentifierQuotes()
+ // will return with thoses:
+ array( '``', false ),
+ array( '`1`', true ),
+
+ // We never know what could happen
+ array( '`0`', 0 ),
+ array( '`1`', 1 ),
+
+ // Whatchout! Should probably use something more meaningful
+ array( "`'`", "'" ), # single quote
+ array( '`"`', '"' ), # double quote
+ array( '````', '`' ), # backtick
+ array( '`’`', '’' ), # apostrophe (look at your encyclopedia)
+
+ // sneaky NUL bytes are lurking everywhere
+ array( '``', "\0" ),
+ array( '`xyzzy`', "\0x\0y\0z\0z\0y\0" ),
+
+ // unicode chars
+ array(
+ self::createUnicodeString( '`\u0001a\uFFFFb`' ),
+ self::createUnicodeString( '\u0001a\uFFFFb' )
+ ),
+ array(
+ self::createUnicodeString( '`\u0001\uFFFF`' ),
+ self::createUnicodeString( '\u0001\u0000\uFFFF\u0000' )
+ ),
+ array( '`☃`', '☃' ),
+ array( '`メインページ`', 'メインページ' ),
+ array( '`Басты_бет`', 'Басты_бет' ),
+
+ // Real world:
+ array( '`Alix`', 'Alix' ), # while( ! $recovered ) { sleep(); }
+ array( '`Backtick: ```', 'Backtick: `' ),
+ array( '`This is a test`', 'This is a test' ),
+ );
+ }
+
+ private static function createUnicodeString($str) {
+ return json_decode( '"' . $str . '"' );
+ }
+
+ function getMockForViews() {
+ $db = $this->getMockBuilder( 'DatabaseMysql' )
+ ->disableOriginalConstructor()
+ ->setMethods( array( 'fetchRow', 'query' ) )
+ ->getMock();
+
+ $db->expects( $this->any() )
+ ->method( 'query' )
+ ->with( $this->anything() )
+ ->will(
+ $this->returnValue( null )
+ );
+
+ $db->expects( $this->any() )
+ ->method( 'fetchRow' )
+ ->with( $this->anything() )
+ ->will( $this->onConsecutiveCalls(
+ array( 'Tables_in_' => 'view1' ),
+ array( 'Tables_in_' => 'view2' ),
+ array( 'Tables_in_' => 'myview' ),
+ false # no more rows
+ ));
+ return $db;
+ }
+ /**
+ * @covers DatabaseMysqlBase::listViews
+ */
+ function testListviews() {
+ $db = $this->getMockForViews();
+
+ // The first call populate an internal cache of views
+ $this->assertEquals( array( 'view1', 'view2', 'myview'),
+ $db->listViews() );
+ $this->assertEquals( array( 'view1', 'view2', 'myview'),
+ $db->listViews() );
+
+ // Prefix filtering
+ $this->assertEquals( array( 'view1', 'view2' ),
+ $db->listViews( 'view' ) );
+ $this->assertEquals( array( 'myview' ),
+ $db->listViews( 'my' ) );
+ $this->assertEquals( array(),
+ $db->listViews( 'UNUSED_PREFIX' ) );
+ $this->assertEquals( array( 'view1', 'view2', 'myview'),
+ $db->listViews( '' ) );
+ }
+
+ /**
+ * @covers DatabaseMysqlBase::isView
+ * @dataProvider provideViewExistanceChecks
+ */
+ function testIsView( $isView, $viewName ) {
+ $db = $this->getMockForViews();
+
+ switch( $isView ) {
+ case true:
+ $this->assertTrue( $db->isView( $viewName ),
+ "$viewName should be considered a view" );
+ break;
+
+ case false:
+ $this->assertFalse( $db->isView( $viewName ),
+ "$viewName has not been defined as a view" );
+ break;
+ }
+
+ }
+
+ function provideViewExistanceChecks() {
+ return array(
+ // format: whether it is a view, view name
+ array( true, 'view1' ),
+ array( true, 'view2' ),
+ array( true, 'myview' ),
+
+ array( false, 'user' ),
+
+ array( false, 'view10' ),
+ array( false, 'my' ),
+ array( false, 'OH_MY_GOD' ), # they killed kenny!
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/db/DatabaseSQLTest.php b/tests/phpunit/includes/db/DatabaseSQLTest.php
new file mode 100644
index 00000000..bdd567e7
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseSQLTest.php
@@ -0,0 +1,721 @@
+<?php
+
+/**
+ * Test the abstract database layer
+ * This is a non DBMS depending test.
+ */
+class DatabaseSQLTest extends MediaWikiTestCase {
+
+ /**
+ * @var DatabaseTestHelper
+ */
+ private $database;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->database = new DatabaseTestHelper( __CLASS__ );
+ }
+
+ protected function assertLastSql( $sqlText ) {
+ $this->assertEquals(
+ $this->database->getLastSqls(),
+ $sqlText
+ );
+ }
+
+ /**
+ * @dataProvider provideSelect
+ * @covers DatabaseBase::select
+ */
+ public function testSelect( $sql, $sqlText ) {
+ $this->database->select(
+ $sql['tables'],
+ $sql['fields'],
+ isset( $sql['conds'] ) ? $sql['conds'] : array(),
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : array(),
+ isset( $sql['join_conds'] ) ? $sql['join_conds'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideSelect() {
+ return array(
+ array(
+ array(
+ 'tables' => 'table',
+ 'fields' => array( 'field', 'alias' => 'field2' ),
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "SELECT field,field2 AS alias " .
+ "FROM table " .
+ "WHERE alias = 'text'"
+ ),
+ array(
+ array(
+ 'tables' => 'table',
+ 'fields' => array( 'field', 'alias' => 'field2' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ),
+ ),
+ "SELECT field,field2 AS alias " .
+ "FROM table " .
+ "WHERE alias = 'text' " .
+ "ORDER BY field " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table', 't2' => 'table2' ),
+ 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ),
+ 'join_conds' => array( 't2' => array(
+ 'LEFT JOIN', 'tid = t2.id'
+ ) ),
+ ),
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "ORDER BY field " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table', 't2' => 'table2' ),
+ 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ),
+ 'join_conds' => array( 't2' => array(
+ 'LEFT JOIN', 'tid = t2.id'
+ ) ),
+ ),
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "GROUP BY field HAVING COUNT(*) > 1 " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table', 't2' => 'table2' ),
+ 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'GROUP BY' => array( 'field', 'field2' ), 'HAVING' => array( 'COUNT(*) > 1', 'field' => 1 ) ),
+ 'join_conds' => array( 't2' => array(
+ 'LEFT JOIN', 'tid = t2.id'
+ ) ),
+ ),
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table' ),
+ 'fields' => array( 'alias' => 'field' ),
+ 'conds' => array( 'alias' => array( 1, 2, 3, 4 ) ),
+ ),
+ "SELECT field AS alias " .
+ "FROM table " .
+ "WHERE alias IN ('1','2','3','4')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUpdate
+ * @covers DatabaseBase::update
+ */
+ public function testUpdate( $sql, $sqlText ) {
+ $this->database->update(
+ $sql['table'],
+ $sql['values'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideUpdate() {
+ return array(
+ array(
+ array(
+ 'table' => 'table',
+ 'values' => array( 'field' => 'text', 'field2' => 'text2' ),
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "UPDATE table " .
+ "SET field = 'text'" .
+ ",field2 = 'text2' " .
+ "WHERE alias = 'text'"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'values' => array( 'field = other', 'field2' => 'text2' ),
+ 'conds' => array( 'id' => '1' ),
+ ),
+ "UPDATE table " .
+ "SET field = other" .
+ ",field2 = 'text2' " .
+ "WHERE id = '1'"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'values' => array( 'field = other', 'field2' => 'text2' ),
+ 'conds' => '*',
+ ),
+ "UPDATE table " .
+ "SET field = other" .
+ ",field2 = 'text2'"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDelete
+ * @covers DatabaseBase::delete
+ */
+ public function testDelete( $sql, $sqlText ) {
+ $this->database->delete(
+ $sql['table'],
+ $sql['conds'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideDelete() {
+ return array(
+ array(
+ array(
+ 'table' => 'table',
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "DELETE FROM table " .
+ "WHERE alias = 'text'"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'conds' => '*',
+ ),
+ "DELETE FROM table"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUpsert
+ * @covers DatabaseBase::upsert
+ */
+ public function testUpsert( $sql, $sqlText ) {
+ $this->database->upsert(
+ $sql['table'],
+ $sql['rows'],
+ $sql['uniqueIndexes'],
+ $sql['set'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideUpsert() {
+ return array(
+ array(
+ array(
+ 'table' => 'upsert_table',
+ 'rows' => array( 'field' => 'text', 'field2' => 'text2' ),
+ 'uniqueIndexes' => array( 'field' ),
+ 'set' => array( 'field' => 'set' ),
+ ),
+ "BEGIN; " .
+ "UPDATE upsert_table " .
+ "SET field = 'set' " .
+ "WHERE ((field = 'text')); " .
+ "INSERT IGNORE INTO upsert_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2'); " .
+ "COMMIT"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDeleteJoin
+ * @covers DatabaseBase::deleteJoin
+ */
+ public function testDeleteJoin( $sql, $sqlText ) {
+ $this->database->deleteJoin(
+ $sql['delTable'],
+ $sql['joinTable'],
+ $sql['delVar'],
+ $sql['joinVar'],
+ $sql['conds'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideDeleteJoin() {
+ return array(
+ array(
+ array(
+ 'delTable' => 'table',
+ 'joinTable' => 'table_join',
+ 'delVar' => 'field',
+ 'joinVar' => 'field_join',
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "DELETE FROM table " .
+ "WHERE field IN (" .
+ "SELECT field_join FROM table_join WHERE alias = 'text'" .
+ ")"
+ ),
+ array(
+ array(
+ 'delTable' => 'table',
+ 'joinTable' => 'table_join',
+ 'delVar' => 'field',
+ 'joinVar' => 'field_join',
+ 'conds' => '*',
+ ),
+ "DELETE FROM table " .
+ "WHERE field IN (" .
+ "SELECT field_join FROM table_join " .
+ ")"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInsert
+ * @covers DatabaseBase::insert
+ */
+ public function testInsert( $sql, $sqlText ) {
+ $this->database->insert(
+ $sql['table'],
+ $sql['rows'],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideInsert() {
+ return array(
+ array(
+ array(
+ 'table' => 'table',
+ 'rows' => array( 'field' => 'text', 'field2' => 2 ),
+ ),
+ "INSERT INTO table " .
+ "(field,field2) " .
+ "VALUES ('text','2')"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'rows' => array( 'field' => 'text', 'field2' => 2 ),
+ 'options' => 'IGNORE',
+ ),
+ "INSERT IGNORE INTO table " .
+ "(field,field2) " .
+ "VALUES ('text','2')"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'rows' => array(
+ array( 'field' => 'text', 'field2' => 2 ),
+ array( 'field' => 'multi', 'field2' => 3 ),
+ ),
+ 'options' => 'IGNORE',
+ ),
+ "INSERT IGNORE INTO table " .
+ "(field,field2) " .
+ "VALUES " .
+ "('text','2')," .
+ "('multi','3')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInsertSelect
+ * @covers DatabaseBase::insertSelect
+ */
+ public function testInsertSelect( $sql, $sqlText ) {
+ $this->database->insertSelect(
+ $sql['destTable'],
+ $sql['srcTable'],
+ $sql['varMap'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : array(),
+ isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideInsertSelect() {
+ return array(
+ array(
+ array(
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ),
+ 'conds' => '*',
+ ),
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table"
+ ),
+ array(
+ array(
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ),
+ 'conds' => array( 'field' => 2 ),
+ ),
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table " .
+ "WHERE field = '2'"
+ ),
+ array(
+ array(
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ),
+ 'conds' => array( 'field' => 2 ),
+ 'insertOptions' => 'IGNORE',
+ 'selectOptions' => array( 'ORDER BY' => 'field' ),
+ ),
+ "INSERT IGNORE INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table " .
+ "WHERE field = '2' " .
+ "ORDER BY field"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideReplace
+ * @covers DatabaseBase::replace
+ */
+ public function testReplace( $sql, $sqlText ) {
+ $this->database->replace(
+ $sql['table'],
+ $sql['uniqueIndexes'],
+ $sql['rows'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideReplace() {
+ return array(
+ array(
+ array(
+ 'table' => 'replace_table',
+ 'uniqueIndexes' => array( 'field' ),
+ 'rows' => array( 'field' => 'text', 'field2' => 'text2' ),
+ ),
+ "DELETE FROM replace_table " .
+ "WHERE ( field='text' ); " .
+ "INSERT INTO replace_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array( array( 'md_module', 'md_skin' ) ),
+ 'rows' => array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ),
+ ),
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' AND md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array( array( 'md_module', 'md_skin' ) ),
+ 'rows' => array(
+ array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ), array(
+ 'md_module' => 'module2',
+ 'md_skin' => 'skin2',
+ 'md_deps' => 'deps2',
+ ),
+ ),
+ ),
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' AND md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps'); " .
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module2' AND md_skin='skin2' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module2','skin2','deps2')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array( 'md_module', 'md_skin' ),
+ 'rows' => array(
+ array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ), array(
+ 'md_module' => 'module2',
+ 'md_skin' => 'skin2',
+ 'md_deps' => 'deps2',
+ ),
+ ),
+ ),
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' ) OR ( md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps'); " .
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module2' ) OR ( md_skin='skin2' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module2','skin2','deps2')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array(),
+ 'rows' => array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ),
+ ),
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideNativeReplace
+ * @covers DatabaseBase::nativeReplace
+ */
+ public function testNativeReplace( $sql, $sqlText ) {
+ $this->database->nativeReplace(
+ $sql['table'],
+ $sql['rows'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideNativeReplace() {
+ return array(
+ array(
+ array(
+ 'table' => 'replace_table',
+ 'rows' => array( 'field' => 'text', 'field2' => 'text2' ),
+ ),
+ "REPLACE INTO replace_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideConditional
+ * @covers DatabaseBase::conditional
+ */
+ public function testConditional( $sql, $sqlText ) {
+ $this->assertEquals( trim( $this->database->conditional(
+ $sql['conds'],
+ $sql['true'],
+ $sql['false']
+ ) ), $sqlText );
+ }
+
+ public static function provideConditional() {
+ return array(
+ array(
+ array(
+ 'conds' => array( 'field' => 'text' ),
+ 'true' => 1,
+ 'false' => 'NULL',
+ ),
+ "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
+ ),
+ array(
+ array(
+ 'conds' => array( 'field' => 'text', 'field2' => 'anothertext' ),
+ 'true' => 1,
+ 'false' => 'NULL',
+ ),
+ "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
+ ),
+ array(
+ array(
+ 'conds' => 'field=1',
+ 'true' => 1,
+ 'false' => 'NULL',
+ ),
+ "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideBuildConcat
+ * @covers DatabaseBase::buildConcat
+ */
+ public function testBuildConcat( $stringList, $sqlText ) {
+ $this->assertEquals( trim( $this->database->buildConcat(
+ $stringList
+ ) ), $sqlText );
+ }
+
+ public static function provideBuildConcat() {
+ return array(
+ array(
+ array( 'field', 'field2' ),
+ "CONCAT(field,field2)"
+ ),
+ array(
+ array( "'test'", 'field2' ),
+ "CONCAT('test',field2)"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideBuildLike
+ * @covers DatabaseBase::buildLike
+ */
+ public function testBuildLike( $array, $sqlText ) {
+ $this->assertEquals( trim( $this->database->buildLike(
+ $array
+ ) ), $sqlText );
+ }
+
+ public static function provideBuildLike() {
+ return array(
+ array(
+ 'text',
+ "LIKE 'text'"
+ ),
+ array(
+ array( 'text', new LikeMatch( '%' ) ),
+ "LIKE 'text%'"
+ ),
+ array(
+ array( 'text', new LikeMatch( '%' ), 'text2' ),
+ "LIKE 'text%text2'"
+ ),
+ array(
+ array( 'text', new LikeMatch( '_' ) ),
+ "LIKE 'text_'"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUnionQueries
+ * @covers DatabaseBase::unionQueries
+ */
+ public function testUnionQueries( $sql, $sqlText ) {
+ $this->assertEquals( trim( $this->database->unionQueries(
+ $sql['sqls'],
+ $sql['all']
+ ) ), $sqlText );
+ }
+
+ public static function provideUnionQueries() {
+ return array(
+ array(
+ array(
+ 'sqls' => array( 'RAW SQL', 'RAW2SQL' ),
+ 'all' => true,
+ ),
+ "(RAW SQL) UNION ALL (RAW2SQL)"
+ ),
+ array(
+ array(
+ 'sqls' => array( 'RAW SQL', 'RAW2SQL' ),
+ 'all' => false,
+ ),
+ "(RAW SQL) UNION (RAW2SQL)"
+ ),
+ array(
+ array(
+ 'sqls' => array( 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ),
+ 'all' => false,
+ ),
+ "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
+ ),
+ );
+ }
+
+ /**
+ * @covers DatabaseBase::commit
+ */
+ public function testTransactionCommit() {
+ $this->database->begin( __METHOD__ );
+ $this->database->commit( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; COMMIT' );
+ }
+
+ /**
+ * @covers DatabaseBase::rollback
+ */
+ public function testTransactionRollback() {
+ $this->database->begin( __METHOD__ );
+ $this->database->rollback( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+ }
+
+ /**
+ * @covers DatabaseBase::dropTable
+ */
+ public function testDropTable() {
+ $this->database->setExistingTables( array( 'table' ) );
+ $this->database->dropTable( 'table', __METHOD__ );
+ $this->assertLastSql( 'DROP TABLE table' );
+ }
+
+ /**
+ * @covers DatabaseBase::dropTable
+ */
+ public function testDropNonExistingTable() {
+ $this->assertFalse(
+ $this->database->dropTable( 'non_existing', __METHOD__ )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php
new file mode 100644
index 00000000..70ee9465
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php
@@ -0,0 +1,421 @@
+<?php
+
+class MockDatabaseSqlite extends DatabaseSqliteStandalone {
+ var $lastQuery;
+
+ function __construct() {
+ parent::__construct( ':memory:' );
+ }
+
+ function query( $sql, $fname = '', $tempIgnore = false ) {
+ $this->lastQuery = $sql;
+
+ return true;
+ }
+
+ /**
+ * Override parent visibility to public
+ */
+ public function replaceVars( $s ) {
+ return parent::replaceVars( $s );
+ }
+}
+
+/**
+ * @group sqlite
+ * @group Database
+ * @group medium
+ */
+class DatabaseSqliteTest extends MediaWikiTestCase {
+
+ /**
+ * @var MockDatabaseSqlite
+ */
+ var $db;
+
+ protected function setUp() {
+ parent::setUp();
+
+ if ( !Sqlite::isPresent() ) {
+ $this->markTestSkipped( 'No SQLite support detected' );
+ }
+ $this->db = new MockDatabaseSqlite();
+ if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) {
+ $this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" );
+ }
+ }
+
+ private function replaceVars( $sql ) {
+ // normalize spacing to hide implementation details
+ return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) );
+ }
+
+ private function assertResultIs( $expected, $res ) {
+ $this->assertNotNull( $res );
+ $i = 0;
+ foreach ( $res as $row ) {
+ foreach ( $expected[$i] as $key => $value ) {
+ $this->assertTrue( isset( $row->$key ) );
+ $this->assertEquals( $value, $row->$key );
+ }
+ $i++;
+ }
+ $this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' );
+ }
+
+ public static function provideAddQuotes() {
+ return array(
+ array( // #0: empty
+ '', "''"
+ ),
+ array( // #1: simple
+ 'foo bar', "'foo bar'"
+ ),
+ array( // #2: including quote
+ 'foo\'bar', "'foo''bar'"
+ ),
+ array( // #3: including \0 (must be represented as hex, per https://bugs.php.net/bug.php?id=63419)
+ "x\0y",
+ "x'780079'",
+ ),
+ array( // #4: blob object (must be represented as hex)
+ new Blob( "hello" ),
+ "x'68656c6c6f'",
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideAddQuotes()
+ * @covers DatabaseSqlite::addQuotes
+ */
+ public function testAddQuotes( $value, $expected ) {
+ // check quoting
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $this->assertEquals( $expected, $db->addQuotes( $value ), 'string not quoted as expected' );
+
+ // ok, quoting works as expected, now try a round trip.
+ $re = $db->query( 'select ' . $db->addQuotes( $value ) );
+
+ $this->assertTrue( $re !== false, 'query failed' );
+
+ if ( $row = $re->fetchRow() ) {
+ if ( $value instanceof Blob ) {
+ $value = $value->fetch();
+ }
+
+ $this->assertEquals( $value, $row[0], 'string mangled by the database' );
+ } else {
+ $this->fail( 'query returned no result' );
+ }
+ }
+
+ /**
+ * @covers DatabaseSqlite::replaceVars
+ */
+ 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 foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
+ $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY NOT NULL AUTO_INCREMENT );" )
+ );
+ $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
+ $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY AUTO_INCREMENT NOT NULL );" )
+ );
+
+ $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" )
+ );
+ }
+
+ /**
+ * @covers DatabaseSqlite::tableName
+ */
+ 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' ) );
+ }
+
+ /**
+ * @covers DatabaseSqlite::duplicateTableStructure
+ */
+ public function testDuplicateTableStructure() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->query( 'CREATE TABLE foo(foo, barfoo)' );
+
+ $db->duplicateTableStructure( 'foo', 'bar' );
+ $this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)',
+ $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ),
+ 'Normal table duplication'
+ );
+
+ $db->duplicateTableStructure( 'foo', 'baz', true );
+ $this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)',
+ $db->selectField( 'sqlite_temp_master', 'sql', array( 'name' => 'baz' ) ),
+ 'Creation of temporary duplicate'
+ );
+ $this->assertEquals( 0,
+ $db->selectField( 'sqlite_master', 'COUNT(*)', array( 'name' => 'baz' ) ),
+ 'Create a temporary duplicate only'
+ );
+ }
+
+ /**
+ * @covers DatabaseSqlite::duplicateTableStructure
+ */
+ public function testDuplicateTableStructureVirtual() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ if ( $db->getFulltextSearchModule() != 'FTS3' ) {
+ $this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' );
+ }
+ $db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' );
+
+ $db->duplicateTableStructure( 'foo', 'bar' );
+ $this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)',
+ $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ),
+ 'Duplication of virtual tables'
+ );
+
+ $db->duplicateTableStructure( 'foo', 'baz', true );
+ $this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)',
+ $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'baz' ) ),
+ "Can't create temporary virtual tables, should fall back to non-temporary duplication"
+ );
+ }
+
+ /**
+ * @covers DatabaseSqlite::deleteJoin
+ */
+ public function testDeleteJoin() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->query( 'CREATE TABLE a (a_1)', __METHOD__ );
+ $db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ );
+ $db->insert( 'a', array(
+ array( 'a_1' => 1 ),
+ array( 'a_1' => 2 ),
+ array( 'a_1' => 3 ),
+ ),
+ __METHOD__
+ );
+ $db->insert( 'b', array(
+ array( 'b_1' => 2, 'b_2' => 'a' ),
+ array( 'b_1' => 3, 'b_2' => 'b' ),
+ ),
+ __METHOD__
+ );
+ $db->deleteJoin( 'a', 'b', 'a_1', 'b_1', array( 'b_2' => 'a' ), __METHOD__ );
+ $res = $db->query( "SELECT * FROM a", __METHOD__ );
+ $this->assertResultIs( array(
+ array( 'a_1' => 1 ),
+ array( 'a_1' => 3 ),
+ ),
+ $res
+ );
+ }
+
+ public function testEntireSchema() {
+ global $IP;
+
+ $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" );
+ if ( $result !== true ) {
+ $this->fail( $result );
+ }
+ $this->assertTrue( true ); // avoid test being marked as incomplete due to lack of assertions
+ }
+
+ /**
+ * Runs upgrades of older databases and compares results with current schema
+ * @todo Currently only checks list of tables
+ */
+ public function testUpgrades() {
+ global $IP, $wgVersion, $wgProfileToDatabase;
+
+ // Versions tested
+ $versions = array(
+ //'1.13', disabled for now, was totally screwed up
+ // SQLite wasn't included in 1.14
+ '1.15',
+ '1.16',
+ '1.17',
+ '1.18',
+ );
+
+ // Mismatches for these columns we can safely ignore
+ $ignoredColumns = array(
+ 'user_newtalk.user_last_timestamp', // r84185
+ );
+
+ $currentDB = new DatabaseSqliteStandalone( ':memory:' );
+ $currentDB->sourceFile( "$IP/maintenance/tables.sql" );
+ if ( $wgProfileToDatabase ) {
+ $currentDB->sourceFile( "$IP/maintenance/sqlite/archives/patch-profiling.sql" );
+ }
+ $currentTables = $this->getTables( $currentDB );
+ sort( $currentTables );
+
+ foreach ( $versions as $version ) {
+ $versions = "upgrading from $version to $wgVersion";
+ $db = $this->prepareDB( $version );
+ $tables = $this->getTables( $db );
+ $this->assertEquals( $currentTables, $tables, "Different tables $versions" );
+ foreach ( $tables as $table ) {
+ $currentCols = $this->getColumns( $currentDB, $table );
+ $cols = $this->getColumns( $db, $table );
+ $this->assertEquals(
+ array_keys( $currentCols ),
+ array_keys( $cols ),
+ "Mismatching columns for table \"$table\" $versions"
+ );
+ foreach ( $currentCols as $name => $column ) {
+ $fullName = "$table.$name";
+ $this->assertEquals(
+ (bool)$column->pk,
+ (bool)$cols[$name]->pk,
+ "PRIMARY KEY status does not match for column $fullName $versions"
+ );
+ if ( !in_array( $fullName, $ignoredColumns ) ) {
+ $this->assertEquals(
+ (bool)$column->notnull,
+ (bool)$cols[$name]->notnull,
+ "NOT NULL status does not match for column $fullName $versions"
+ );
+ $this->assertEquals(
+ $column->dflt_value,
+ $cols[$name]->dflt_value,
+ "Default values does not match for column $fullName $versions"
+ );
+ }
+ }
+ $currentIndexes = $this->getIndexes( $currentDB, $table );
+ $indexes = $this->getIndexes( $db, $table );
+ $this->assertEquals(
+ array_keys( $currentIndexes ),
+ array_keys( $indexes ),
+ "mismatching indexes for table \"$table\" $versions"
+ );
+ }
+ $db->close();
+ }
+ }
+
+ /**
+ * @covers DatabaseSqlite::insertId
+ */
+ public function testInsertIdType() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+
+ $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
+ $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Database creation" );
+
+ $insertion = $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ );
+ $this->assertTrue( $insertion, "Insertion worked" );
+
+ $this->assertInternalType( 'integer', $db->insertId(), "Actual typecheck" );
+ $this->assertTrue( $db->close(), "closing database" );
+ }
+
+ private function prepareDB( $version ) {
+ static $maint = null;
+ if ( $maint === null ) {
+ $maint = new FakeMaintenance();
+ $maint->loadParamsAndArgs( null, array( 'quiet' => 1 ) );
+ }
+
+ global $IP;
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->sourceFile( "$IP/tests/phpunit/data/db/sqlite/tables-$version.sql" );
+ $updater = DatabaseUpdater::newForDB( $db, false, $maint );
+ $updater->doUpdates( array( 'core' ) );
+
+ return $db;
+ }
+
+ private function getTables( $db ) {
+ $list = array_flip( $db->listTables() );
+ $excluded = array(
+ 'external_user', // removed from core in 1.22
+ 'math', // moved out of core in 1.18
+ 'trackbacks', // removed from core in 1.19
+ 'searchindex',
+ 'searchindex_content',
+ 'searchindex_segments',
+ 'searchindex_segdir',
+ // FTS4 ready!!1
+ 'searchindex_docsize',
+ 'searchindex_stat',
+ );
+ foreach ( $excluded as $t ) {
+ unset( $list[$t] );
+ }
+ $list = array_flip( $list );
+ sort( $list );
+
+ return $list;
+ }
+
+ private function getColumns( $db, $table ) {
+ $cols = array();
+ $res = $db->query( "PRAGMA table_info($table)" );
+ $this->assertNotNull( $res );
+ foreach ( $res as $col ) {
+ $cols[$col->name] = $col;
+ }
+ ksort( $cols );
+
+ return $cols;
+ }
+
+ private function getIndexes( $db, $table ) {
+ $indexes = array();
+ $res = $db->query( "PRAGMA index_list($table)" );
+ $this->assertNotNull( $res );
+ foreach ( $res as $index ) {
+ $res2 = $db->query( "PRAGMA index_info({$index->name})" );
+ $this->assertNotNull( $res2 );
+ $index->columns = array();
+ foreach ( $res2 as $col ) {
+ $index->columns[] = $col;
+ }
+ $indexes[$index->name] = $index;
+ }
+ ksort( $indexes );
+
+ return $indexes;
+ }
+
+ public function testCaseInsensitiveLike() {
+ // TODO: Test this for all databases
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $res = $db->query( 'SELECT "a" LIKE "A" AS a' );
+ $row = $res->fetchRow();
+ $this->assertFalse( (bool)$row['a'] );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseTest.php b/tests/phpunit/includes/db/DatabaseTest.php
new file mode 100644
index 00000000..301fc990
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseTest.php
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * @group Database
+ * @group DatabaseBase
+ */
+class DatabaseTest extends MediaWikiTestCase {
+ /**
+ * @var DatabaseBase
+ */
+ var $db;
+ var $functionTest = false;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->db = wfGetDB( DB_MASTER );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ if ( $this->functionTest ) {
+ $this->dropFunctions();
+ $this->functionTest = false;
+ }
+ }
+ /**
+ * @covers DatabaseBase::dropTable
+ */
+ public function testAddQuotesNull() {
+ $check = "NULL";
+ if ( $this->db->getType() === 'sqlite' || $this->db->getType() === 'oracle' ) {
+ $check = "''";
+ }
+ $this->assertEquals( $check, $this->db->addQuotes( null ) );
+ }
+
+ public function testAddQuotesInt() {
+ # returning just "1234" should be ok too, though...
+ # maybe
+ $this->assertEquals(
+ "'1234'",
+ $this->db->addQuotes( 1234 ) );
+ }
+
+ public function testAddQuotesFloat() {
+ # returning just "1234.5678" would be ok too, though
+ $this->assertEquals(
+ "'1234.5678'",
+ $this->db->addQuotes( 1234.5678 ) );
+ }
+
+ public function testAddQuotesString() {
+ $this->assertEquals(
+ "'string'",
+ $this->db->addQuotes( 'string' ) );
+ }
+
+ public 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" ) );
+ }
+
+ private function getSharedTableName( $table, $database, $prefix, $format = 'quoted' ) {
+ global $wgSharedDB, $wgSharedTables, $wgSharedPrefix;
+
+ $oldName = $wgSharedDB;
+ $oldTables = $wgSharedTables;
+ $oldPrefix = $wgSharedPrefix;
+
+ $wgSharedDB = $database;
+ $wgSharedTables = array( $table );
+ $wgSharedPrefix = $prefix;
+
+ $ret = $this->db->tableName( $table, $format );
+
+ $wgSharedDB = $oldName;
+ $wgSharedTables = $oldTables;
+ $wgSharedPrefix = $oldPrefix;
+
+ return $ret;
+ }
+
+ private function prefixAndQuote( $table, $database = null, $prefix = null, $format = 'quoted' ) {
+ if ( $this->db->getType() === 'sqlite' || $format !== 'quoted' ) {
+ $quote = '';
+ } elseif ( $this->db->getType() === 'mysql' ) {
+ $quote = '`';
+ } elseif ( $this->db->getType() === 'oracle' ) {
+ $quote = '/*Q*/';
+ } else {
+ $quote = '"';
+ }
+
+ if ( $database !== null ) {
+ if ( $this->db->getType() === 'oracle' ) {
+ $database = $quote . $database . '.';
+ } else {
+ $database = $quote . $database . $quote . '.';
+ }
+ }
+
+ if ( $prefix === null ) {
+ $prefix = $this->dbPrefix();
+ }
+
+ if ( $this->db->getType() === 'oracle' ) {
+ return strtoupper($database . $quote . $prefix . $table);
+ } else {
+ return $database . $quote . $prefix . $table . $quote;
+ }
+ }
+
+ public function testTableNameLocal() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename' ),
+ $this->db->tableName( 'tablename' )
+ );
+ }
+
+ public function testTableNameRawLocal() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', null, null, 'raw' ),
+ $this->db->tableName( 'tablename', 'raw' )
+ );
+ }
+
+ public function testTableNameShared() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_' ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_' )
+ );
+
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', null ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', null )
+ );
+ }
+
+ public function testTableNameRawShared() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_', 'raw' ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_', 'raw' )
+ );
+
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', null, 'raw' ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', null, 'raw' )
+ );
+ }
+
+ public function testTableNameForeign() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'databasename', '' ),
+ $this->db->tableName( 'databasename.tablename' )
+ );
+ }
+
+ public function testTableNameRawForeign() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'databasename', '', 'raw' ),
+ $this->db->tableName( 'databasename.tablename', 'raw' )
+ );
+ }
+
+ public function testFillPreparedEmpty() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT * FROM interwiki', array() );
+ $this->assertEquals(
+ "SELECT * FROM interwiki",
+ $sql );
+ }
+
+ public 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 );
+ }
+
+ public 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 );
+ }
+
+ public 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 );
+ }
+
+ public function testStoredFunctions() {
+ if ( !in_array( wfGetDB( DB_MASTER )->getType(), array( 'mysql', 'postgres' ) ) ) {
+ $this->markTestSkipped( 'MySQL or Postgres required' );
+ }
+ global $IP;
+ $this->dropFunctions();
+ $this->functionTest = true;
+ $this->assertTrue( $this->db->sourceFile( "$IP/tests/phpunit/data/db/{$this->db->getType()}/functions.sql" ) );
+ $res = $this->db->query( 'SELECT mw_test_function() AS test', __METHOD__ );
+ $this->assertEquals( 42, $res->fetchObject()->test );
+ }
+
+ private function dropFunctions() {
+ $this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function'
+ . ( $this->db->getType() == 'postgres' ? '()' : '' )
+ );
+ }
+
+ public function testUnknownTableCorruptsResults() {
+ $res = $this->db->select( 'page', '*', array( 'page_id' => 1 ) );
+ $this->assertFalse( $this->db->tableExists( 'foobarbaz' ) );
+ $this->assertInternalType( 'int', $res->numRows() );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php
new file mode 100644
index 00000000..790f273c
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseTestHelper.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * Helper for testing the methods from the DatabaseBase class
+ * @since 1.22
+ */
+class DatabaseTestHelper extends DatabaseBase {
+
+ /**
+ * __CLASS__ of the test suite,
+ * used to determine, if the function name is passed every time to query()
+ */
+ protected $testName = array();
+
+ /**
+ * Array of lastSqls passed to query(),
+ * This is an array since some methods in DatabaseBase can do more than one
+ * query. Cleared when calling getLastSqls().
+ */
+ protected $lastSqls = array();
+
+ /**
+ * Array of tables to be considered as existing by tableExist()
+ * Use setExistingTables() to alter.
+ */
+ protected $tablesExists;
+
+ public function __construct( $testName ) {
+ $this->testName = $testName;
+ }
+
+ /**
+ * Returns SQL queries grouped by '; '
+ * Clear the list of queries that have been done so far.
+ */
+ public function getLastSqls() {
+ $lastSqls = implode( '; ', $this->lastSqls );
+ $this->lastSqls = array();
+
+ return $lastSqls;
+ }
+
+ public function setExistingTables( $tablesExists ) {
+ $this->tablesExists = (array)$tablesExists;
+ }
+
+ protected function addSql( $sql ) {
+ // clean up spaces before and after some words and the whole string
+ $this->lastSqls[] = trim( preg_replace(
+ '/\s{2,}(?=FROM|WHERE|GROUP BY|ORDER BY|LIMIT)|(?<=SELECT|INSERT|UPDATE)\s{2,}/',
+ ' ', $sql
+ ) );
+ }
+
+ protected function checkFunctionName( $fname ) {
+ if ( substr( $fname, 0, strlen( $this->testName ) ) !== $this->testName ) {
+ throw new MWException( 'function name does not start with test class. ' .
+ $fname . ' vs. ' . $this->testName . '. ' .
+ 'Please provide __METHOD__ to database methods.' );
+ }
+ }
+
+ function strencode( $s ) {
+ // Choose apos to avoid handling of escaping double quotes in quoted text
+ return str_replace( "'", "\'", $s );
+ }
+
+ public function addIdentifierQuotes( $s ) {
+ // no escaping to avoid handling of double quotes in quoted text
+ return $s;
+ }
+
+ public function query( $sql, $fname = '', $tempIgnore = false ) {
+ $this->checkFunctionName( $fname );
+ $this->addSql( $sql );
+
+ return parent::query( $sql, $fname, $tempIgnore );
+ }
+
+ public function tableExists( $table, $fname = __METHOD__ ) {
+ $this->checkFunctionName( $fname );
+
+ return in_array( $table, (array)$this->tablesExists );
+ }
+
+ // Redeclare parent method to make it public
+ public function nativeReplace( $table, $rows, $fname ) {
+ return parent::nativeReplace( $table, $rows, $fname );
+ }
+
+ function getType() {
+ return 'test';
+ }
+
+ function open( $server, $user, $password, $dbName ) {
+ return false;
+ }
+
+ function fetchObject( $res ) {
+ return false;
+ }
+
+ function fetchRow( $res ) {
+ return false;
+ }
+
+ function numRows( $res ) {
+ return -1;
+ }
+
+ function numFields( $res ) {
+ return -1;
+ }
+
+ function fieldName( $res, $n ) {
+ return 'test';
+ }
+
+ function insertId() {
+ return -1;
+ }
+
+ function dataSeek( $res, $row ) {
+ /* nop */
+ }
+
+ function lastErrno() {
+ return -1;
+ }
+
+ function lastError() {
+ return 'test';
+ }
+
+ function fieldInfo( $table, $field ) {
+ return false;
+ }
+
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+ return false;
+ }
+
+ function affectedRows() {
+ return -1;
+ }
+
+ function getSoftwareLink() {
+ return 'test';
+ }
+
+ function getServerVersion() {
+ return 'test';
+ }
+
+ function getServerInfo() {
+ return 'test';
+ }
+
+ protected function closeConnection() {
+ return false;
+ }
+
+ protected function doQuery( $sql ) {
+ return array();
+ }
+}
diff --git a/tests/phpunit/includes/db/ORMRowTest.php b/tests/phpunit/includes/db/ORMRowTest.php
new file mode 100644
index 00000000..27d4d0e8
--- /dev/null
+++ b/tests/phpunit/includes/db/ORMRowTest.php
@@ -0,0 +1,226 @@
+<?php
+
+/**
+ * Abstract class to construct tests for ORMRow deriving classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.20
+ *
+ * @ingroup Test
+ *
+ * @group ORM
+ *
+ * The database group has as a side effect that temporal database tables are created. This makes
+ * it possible to test without poisoning a production database.
+ * @group Database
+ *
+ * Some of the tests takes more time, and needs therefor longer time before they can be aborted
+ * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases
+ * that hold the first tests in a pending state awaiting access to the database.
+ * @group medium
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class ORMRowTest extends \MediaWikiTestCase {
+
+ /**
+ * @since 1.20
+ * @return string
+ */
+ abstract protected function getRowClass();
+
+ /**
+ * @since 1.20
+ * @return IORMTable
+ */
+ abstract protected function getTableInstance();
+
+ /**
+ * @since 1.20
+ * @return array
+ */
+ abstract public function constructorTestProvider();
+
+ /**
+ * @since 1.20
+ * @param IORMRow $row
+ * @param array $data
+ */
+ protected function verifyFields( IORMRow $row, array $data ) {
+ foreach ( array_keys( $data ) as $fieldName ) {
+ $this->assertEquals( $data[$fieldName], $row->getField( $fieldName ) );
+ }
+ }
+
+ /**
+ * @since 1.20
+ * @param array $data
+ * @param boolean $loadDefaults
+ * @return IORMRow
+ */
+ protected function getRowInstance( array $data, $loadDefaults ) {
+ $class = $this->getRowClass();
+
+ return new $class( $this->getTableInstance(), $data, $loadDefaults );
+ }
+
+ /**
+ * @since 1.20
+ * @return array
+ */
+ protected function getMockValues() {
+ return array(
+ 'id' => 1,
+ 'str' => 'foobar4645645',
+ 'int' => 42,
+ 'float' => 4.2,
+ 'bool' => true,
+ 'array' => array( 42, 'foobar' ),
+ 'blob' => new stdClass()
+ );
+ }
+
+ /**
+ * @since 1.20
+ * @return array
+ */
+ protected function getMockFields() {
+ $mockValues = $this->getMockValues();
+ $mockFields = array();
+
+ foreach ( $this->getTableInstance()->getFields() as $name => $type ) {
+ if ( $name !== 'id' ) {
+ $mockFields[$name] = $mockValues[$type];
+ }
+ }
+
+ return $mockFields;
+ }
+
+ /**
+ * @since 1.20
+ * @return array of IORMRow
+ */
+ public function instanceProvider() {
+ $instances = array();
+
+ foreach ( $this->constructorTestProvider() as $arguments ) {
+ $instances[] = array( call_user_func_array( array( $this, 'getRowInstance' ), $arguments ) );
+ }
+
+ return $instances;
+ }
+
+ /**
+ * @dataProvider constructorTestProvider
+ */
+ public function testConstructor( array $data, $loadDefaults ) {
+ $this->verifyFields( $this->getRowInstance( $data, $loadDefaults ), $data );
+ }
+
+ /**
+ * @dataProvider constructorTestProvider
+ */
+ public function testSaveAndRemove( array $data, $loadDefaults ) {
+ $item = $this->getRowInstance( $data, $loadDefaults );
+
+ $this->assertTrue( $item->save() );
+
+ $this->assertTrue( $item->hasIdField() );
+ $this->assertTrue( is_integer( $item->getId() ) );
+
+ $id = $item->getId();
+
+ $this->assertTrue( $item->save() );
+
+ $this->assertEquals( $id, $item->getId() );
+
+ $this->verifyFields( $item, $data );
+
+ $this->assertTrue( $item->remove() );
+
+ $this->assertFalse( $item->hasIdField() );
+
+ $this->assertTrue( $item->save() );
+
+ $this->verifyFields( $item, $data );
+
+ $this->assertTrue( $item->remove() );
+
+ $this->assertFalse( $item->hasIdField() );
+
+ $this->verifyFields( $item, $data );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ */
+ public function testSetField( IORMRow $item ) {
+ foreach ( $this->getMockFields() as $name => $value ) {
+ $item->setField( $name, $value );
+ $this->assertEquals( $value, $item->getField( $name ) );
+ }
+ }
+
+ /**
+ * @since 1.20
+ * @param array $expected
+ * @param IORMRow $item
+ */
+ protected function assertFieldValues( array $expected, IORMRow $item ) {
+ foreach ( $expected as $name => $type ) {
+ if ( $name !== 'id' ) {
+ $this->assertEquals( $expected[$name], $item->getField( $name ) );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ */
+ public function testSetFields( IORMRow $item ) {
+ $originalValues = $item->getFields();
+
+ $item->setFields( array(), false );
+
+ foreach ( $item->getTable()->getFields() as $name => $type ) {
+ $originalHas = array_key_exists( $name, $originalValues );
+ $newHas = $item->hasField( $name );
+
+ $this->assertEquals( $originalHas, $newHas );
+
+ if ( $originalHas && $newHas ) {
+ $this->assertEquals( $originalValues[$name], $item->getField( $name ) );
+ }
+ }
+
+ $mockFields = $this->getMockFields();
+
+ $item->setFields( $mockFields, false );
+
+ $this->assertFieldValues( $originalValues, $item );
+
+ $item->setFields( $mockFields, true );
+
+ $this->assertFieldValues( $mockFields, $item );
+ }
+
+ // TODO: test all of the methods!
+
+}
diff --git a/tests/phpunit/includes/db/ORMTableTest.php b/tests/phpunit/includes/db/ORMTableTest.php
new file mode 100644
index 00000000..e583d1bc
--- /dev/null
+++ b/tests/phpunit/includes/db/ORMTableTest.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Abstract class to construct tests for ORMTable deriving classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ *
+ * @ingroup Test
+ *
+ * @group ORM
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author Daniel Kinzler
+ */
+class ORMTableTest extends MediaWikiTestCase {
+
+ /**
+ * @since 1.21
+ * @return string
+ */
+ protected function getTableClass() {
+ return 'PageORMTableForTesting';
+ }
+
+ /**
+ * @since 1.21
+ * @return IORMTable
+ */
+ public function getTable() {
+ $class = $this->getTableClass();
+
+ return $class::singleton();
+ }
+
+ /**
+ * @since 1.21
+ * @return string
+ */
+ public function getRowClass() {
+ return $this->getTable()->getRowClass();
+ }
+
+ /**
+ * @since 1.21
+ */
+ public function testSingleton() {
+ $class = $this->getTableClass();
+
+ $this->assertInstanceOf( $class, $class::singleton() );
+ $this->assertTrue( $class::singleton() === $class::singleton() );
+ }
+
+ /**
+ * @since 1.21
+ */
+ public function testIgnoreErrorsOverride() {
+ $table = $this->getTable();
+
+ $db = $table->getReadDbConnection();
+ $db->ignoreErrors( true );
+
+ try {
+ $table->rawSelect( "this is invalid" );
+ $this->fail( "An invalid query should trigger a DBQueryError even if ignoreErrors is enabled." );
+ } catch ( DBQueryError $ex ) {
+ $this->assertTrue( true, "just making phpunit happy" );
+ }
+
+ $db->ignoreErrors( false );
+ }
+}
+
+/**
+ * Dummy ORM table for testing, reading Title objects from the page table.
+ *
+ * @since 1.21
+ */
+
+class PageORMTableForTesting extends ORMTable {
+
+ /**
+ * @see ORMTable::getName
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'page';
+ }
+
+ /**
+ * @see ORMTable::getRowClass
+ *
+ * @return string
+ */
+ public function getRowClass() {
+ return 'Title';
+ }
+
+ /**
+ * @see ORMTable::newRow
+ *
+ * @return IORMRow
+ */
+ public function newRow( array $data, $loadDefaults = false ) {
+ return Title::makeTitle( $data['namespace'], $data['title'] );
+ }
+
+ /**
+ * @see ORMTable::getFields
+ *
+ * @return array
+ */
+ public function getFields() {
+ return array(
+ 'id' => 'int',
+ 'namespace' => 'int',
+ 'title' => 'str',
+ );
+ }
+
+ /**
+ * @see ORMTable::getFieldPrefix
+ *
+ * @return string
+ */
+ protected function getFieldPrefix() {
+ return 'page_';
+ }
+}
diff --git a/tests/phpunit/includes/db/TestORMRowTest.php b/tests/phpunit/includes/db/TestORMRowTest.php
new file mode 100644
index 00000000..f65642b8
--- /dev/null
+++ b/tests/phpunit/includes/db/TestORMRowTest.php
@@ -0,0 +1,215 @@
+<?php
+
+/**
+ * Tests for the TestORMRow class.
+ * TestORMRow is a dummy class to be able to test the abstract ORMRow class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.20
+ *
+ * @ingroup Test
+ *
+ * @group ORM
+ *
+ * The database group has as a side effect that temporal database tables are created. This makes
+ * it possible to test without poisoning a production database.
+ * @group Database
+ *
+ * Some of the tests takes more time, and needs therefor longer time before they can be aborted
+ * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases
+ * that hold the first tests in a pending state awaiting access to the database.
+ * @group medium
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+require_once __DIR__ . "/ORMRowTest.php";
+
+class TestORMRowTest extends ORMRowTest {
+
+ /**
+ * @since 1.20
+ * @return string
+ */
+ protected function getRowClass() {
+ return 'TestORMRow';
+ }
+
+ /**
+ * @since 1.20
+ * @return IORMTable
+ */
+ protected function getTableInstance() {
+ return TestORMTable::singleton();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $isSqlite = $GLOBALS['wgDBtype'] === 'sqlite';
+ $isPostgres = $GLOBALS['wgDBtype'] === 'postgres';
+
+ $idField = $isSqlite ? 'INTEGER' : 'INT unsigned';
+ $primaryKey = $isSqlite ? 'PRIMARY KEY AUTOINCREMENT' : 'auto_increment PRIMARY KEY';
+
+ if ( $isPostgres ) {
+ $dbw->query(
+ 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . "(
+ test_id serial PRIMARY KEY,
+ test_name TEXT NOT NULL DEFAULT '',
+ test_age INTEGER NOT NULL DEFAULT 0,
+ test_height REAL NOT NULL DEFAULT 0,
+ test_awesome INTEGER NOT NULL DEFAULT 0,
+ test_stuff BYTEA,
+ test_moarstuff BYTEA,
+ test_time TIMESTAMPTZ
+ );",
+ __METHOD__
+ );
+ } else {
+ $dbw->query(
+ 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . '(
+ test_id ' . $idField . ' NOT NULL ' . $primaryKey . ',
+ test_name VARCHAR(255) NOT NULL,
+ test_age TINYINT unsigned NOT NULL,
+ test_height FLOAT NOT NULL,
+ test_awesome TINYINT unsigned NOT NULL,
+ test_stuff BLOB NOT NULL,
+ test_moarstuff BLOB NOT NULL,
+ test_time varbinary(14) NOT NULL
+ );',
+ __METHOD__
+ );
+ }
+ }
+
+ protected function tearDown() {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->dropTable( 'orm_test', __METHOD__ );
+
+ parent::tearDown();
+ }
+
+ public function constructorTestProvider() {
+ $dbw = wfGetDB( DB_MASTER );
+ return array(
+ array(
+ array(
+ 'name' => 'Foobar',
+ 'time' => $dbw->timestamp( '20120101020202' ),
+ 'age' => 42,
+ 'height' => 9000.1,
+ 'awesome' => true,
+ 'stuff' => array( 13, 11, 7, 5, 3, 2 ),
+ 'moarstuff' => (object)array( 'foo' => 'bar', 'bar' => array( 4, 2 ), 'baz' => true )
+ ),
+ true
+ ),
+ );
+ }
+
+ /**
+ * @since 1.21
+ * @return array
+ */
+ protected function getMockValues() {
+ return array(
+ 'id' => 1,
+ 'str' => 'foobar4645645',
+ 'int' => 42,
+ 'float' => 4.2,
+ 'bool' => '',
+ 'array' => array( 42, 'foobar' ),
+ 'blob' => new stdClass()
+ );
+ }
+}
+
+class TestORMRow extends ORMRow {
+}
+
+class TestORMTable extends ORMTable {
+
+ /**
+ * Returns the name of the database table objects of this type are stored in.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'orm_test';
+ }
+
+ /**
+ * Returns the name of a IORMRow implementing class that
+ * represents single rows in this table.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function getRowClass() {
+ return 'TestORMRow';
+ }
+
+ /**
+ * Returns an array with the fields and their types this object contains.
+ * This corresponds directly to the fields in the database, without prefix.
+ *
+ * field name => type
+ *
+ * Allowed types:
+ * * id
+ * * str
+ * * int
+ * * float
+ * * bool
+ * * array
+ * * blob
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFields() {
+ return array(
+ 'id' => 'id',
+ 'name' => 'str',
+ 'age' => 'int',
+ 'height' => 'float',
+ 'awesome' => 'bool',
+ 'stuff' => 'array',
+ 'moarstuff' => 'blob',
+ 'time' => 'str', // TS_MW
+ );
+ }
+
+ /**
+ * Gets the db field prefix.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ protected function getFieldPrefix() {
+ return 'test_';
+ }
+}
diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php
new file mode 100644
index 00000000..6926b1c8
--- /dev/null
+++ b/tests/phpunit/includes/debug/MWDebugTest.php
@@ -0,0 +1,72 @@
+<?php
+
+class MWDebugTest extends MediaWikiTestCase {
+
+
+ protected function setUp() {
+ parent::setUp();
+ // Make sure MWDebug class is enabled
+ static $MWDebugEnabled = false;
+ if ( !$MWDebugEnabled ) {
+ MWDebug::init();
+ $MWDebugEnabled = true;
+ }
+ /** Clear log before each test */
+ MWDebug::clearLog();
+ wfSuppressWarnings();
+ }
+
+ protected function tearDown() {
+ wfRestoreWarnings();
+ parent::tearDown();
+ }
+
+ public function testAddLog() {
+ MWDebug::log( 'logging a string' );
+ $this->assertEquals(
+ array( array(
+ 'msg' => 'logging a string',
+ 'type' => 'log',
+ 'caller' => __METHOD__,
+ ) ),
+ MWDebug::getLog()
+ );
+ }
+
+ public function testAddWarning() {
+ MWDebug::warning( 'Warning message' );
+ $this->assertEquals(
+ array( array(
+ 'msg' => 'Warning message',
+ 'type' => 'warn',
+ 'caller' => 'MWDebugTest::testAddWarning',
+ ) ),
+ MWDebug::getLog()
+ );
+ }
+
+ public function testAvoidDuplicateDeprecations() {
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+
+ // assertCount() not available on WMF integration server
+ $this->assertEquals( 1,
+ count( MWDebug::getLog() ),
+ "Only one deprecated warning per function should be kept"
+ );
+ }
+
+ public function testAvoidNonConsecutivesDuplicateDeprecations() {
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+ MWDebug::warning( 'some warning' );
+ MWDebug::log( 'we could have logged something too' );
+ // Another deprecation
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+
+ // assertCount() not available on WMF integration server
+ $this->assertEquals( 3,
+ count( MWDebug::getLog() ),
+ "Only one deprecated warning per function should be kept"
+ );
+ }
+}
diff --git a/tests/phpunit/includes/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php
new file mode 100644
index 00000000..fcfa724f
--- /dev/null
+++ b/tests/phpunit/includes/filebackend/FileBackendTest.php
@@ -0,0 +1,2306 @@
+<?php
+
+/**
+ * @group FileRepo
+ * @group FileBackend
+ * @group medium
+ */
+class FileBackendTest extends MediaWikiTestCase {
+
+ /** @var FileBackend */
+ private $backend;
+ /** @var FileBackendMultiWrite */
+ private $multiBackend;
+ /** @var FSFileBackend */
+ public $singleBackend;
+ private $filesToPrune = array();
+ private static $backendToUse;
+
+ protected function setUp() {
+ global $wgFileBackends;
+ parent::setUp();
+ $uniqueId = time() . '-' . mt_rand();
+ $tmpPrefix = wfTempDir() . '/filebackend-unittest-' . $uniqueId;
+ if ( $this->getCliArg( 'use-filebackend=' ) ) {
+ if ( self::$backendToUse ) {
+ $this->singleBackend = self::$backendToUse;
+ } else {
+ $name = $this->getCliArg( 'use-filebackend=' );
+ $useConfig = array();
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] == $name ) {
+ $useConfig = $conf;
+ break;
+ }
+ }
+ $useConfig['name'] = 'localtesting'; // swap name
+ $useConfig['shardViaHashLevels'] = array( // test sharding
+ 'unittest-cont1' => array( 'levels' => 1, 'base' => 16, 'repeat' => 1 )
+ );
+ $class = $useConfig['class'];
+ self::$backendToUse = new $class( $useConfig );
+ $this->singleBackend = self::$backendToUse;
+ }
+ } else {
+ $this->singleBackend = new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'lockManager' => 'fsLockManager',
+ #'parallelize' => 'implicit',
+ 'wikiId' => wfWikiID() . $uniqueId,
+ 'containerPaths' => array(
+ 'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1",
+ 'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" )
+ ) );
+ }
+ $this->multiBackend = new FileBackendMultiWrite( array(
+ 'name' => 'localtesting',
+ 'lockManager' => 'fsLockManager',
+ 'parallelize' => 'implicit',
+ 'wikiId' => wfWikiId() . $uniqueId,
+ 'backends' => array(
+ array(
+ 'name' => 'localmultitesting1',
+ 'class' => 'FSFileBackend',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array(
+ 'unittest-cont1' => "{$tmpPrefix}-localtestingmulti1-cont1",
+ 'unittest-cont2' => "{$tmpPrefix}-localtestingmulti1-cont2" ),
+ 'isMultiMaster' => false
+ ),
+ array(
+ 'name' => 'localmultitesting2',
+ 'class' => 'FSFileBackend',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array(
+ 'unittest-cont1' => "{$tmpPrefix}-localtestingmulti2-cont1",
+ 'unittest-cont2' => "{$tmpPrefix}-localtestingmulti2-cont2" ),
+ 'isMultiMaster' => true
+ )
+ )
+ ) );
+ $this->filesToPrune = array();
+ }
+
+ private static function baseStorePath() {
+ return 'mwstore://localtesting';
+ }
+
+ private function backendClass() {
+ return get_class( $this->backend );
+ }
+
+ /**
+ * @dataProvider provider_testIsStoragePath
+ * @covers FileBackend::isStoragePath
+ */
+ public function testIsStoragePath( $path, $isStorePath ) {
+ $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
+ "FileBackend::isStoragePath on path '$path'" );
+ }
+
+ public static function provider_testIsStoragePath() {
+ return array(
+ array( 'mwstore://', true ),
+ array( 'mwstore://backend', true ),
+ array( 'mwstore://backend/container', true ),
+ array( 'mwstore://backend/container/', true ),
+ array( 'mwstore://backend/container/path', true ),
+ array( 'mwstore://backend//container/', true ),
+ array( 'mwstore://backend//container//', true ),
+ array( 'mwstore://backend//container//path', true ),
+ array( 'mwstore:///', true ),
+ array( 'mwstore:/', false ),
+ array( 'mwstore:', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testSplitStoragePath
+ * @covers FileBackend::splitStoragePath
+ */
+ public function testSplitStoragePath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
+ "FileBackend::splitStoragePath on path '$path'" );
+ }
+
+ public static function provider_testSplitStoragePath() {
+ return array(
+ array( 'mwstore://backend/container', array( 'backend', 'container', '' ) ),
+ array( 'mwstore://backend/container/', array( 'backend', 'container', '' ) ),
+ array( 'mwstore://backend/container/path', array( 'backend', 'container', 'path' ) ),
+ array( 'mwstore://backend/container//path', array( 'backend', 'container', '/path' ) ),
+ array( 'mwstore://backend//container/path', array( null, null, null ) ),
+ array( 'mwstore://backend//container//path', array( null, null, null ) ),
+ array( 'mwstore://', array( null, null, null ) ),
+ array( 'mwstore://backend', array( null, null, null ) ),
+ array( 'mwstore:///', array( null, null, null ) ),
+ array( 'mwstore:/', array( null, null, null ) ),
+ array( 'mwstore:', array( null, null, null ) )
+ );
+ }
+
+ /**
+ * @dataProvider provider_normalizeStoragePath
+ * @covers FileBackend::normalizeStoragePath
+ */
+ public function testNormalizeStoragePath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
+ "FileBackend::normalizeStoragePath on path '$path'" );
+ }
+
+ public static function provider_normalizeStoragePath() {
+ return array(
+ array( 'mwstore://backend/container', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container/', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container/path', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container//path', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container///path', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container///path//to///obj', 'mwstore://backend/container/path/to/obj' ),
+ array( 'mwstore://', null ),
+ array( 'mwstore://backend', null ),
+ array( 'mwstore://backend//container/path', null ),
+ array( 'mwstore://backend//container//path', null ),
+ array( 'mwstore:///', null ),
+ array( 'mwstore:/', null ),
+ array( 'mwstore:', null ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testParentStoragePath
+ * @covers FileBackend::parentStoragePath
+ */
+ public function testParentStoragePath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
+ "FileBackend::parentStoragePath on path '$path'" );
+ }
+
+ public static function provider_testParentStoragePath() {
+ return array(
+ array( 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ),
+ array( 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container/path', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container', null ),
+ array( 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ),
+ array( 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container/path/', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container/', null ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testExtensionFromPath
+ * @covers FileBackend::extensionFromPath
+ */
+ public function testExtensionFromPath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
+ "FileBackend::extensionFromPath on path '$path'" );
+ }
+
+ public static function provider_testExtensionFromPath() {
+ return array(
+ array( 'mwstore://backend/container/path.txt', 'txt' ),
+ array( 'mwstore://backend/container/path.svg.png', 'png' ),
+ array( 'mwstore://backend/container/path', '' ),
+ array( 'mwstore://backend/container/path.', '' ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testStore
+ */
+ public function testStore( $op ) {
+ $this->filesToPrune[] = $op['src'];
+
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestStore( $op );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestStore( $op );
+ $this->filesToPrune[] = $op['src']; # avoid file leaking
+ $this->tearDownFiles();
+ }
+
+ /**
+ * @covers FileBackend::doOperation
+ */
+ private function doTestStore( $op ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ file_put_contents( $source, "Unit test file" );
+
+ if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+ $this->backend->store( $op );
+ }
+
+ $status = $this->backend->doOperation( $op );
+
+ $this->assertGoodStatus( $status,
+ "Store from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Store from $source to $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Store from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( true, file_exists( $source ),
+ "Source file $source still exists ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists ($backendName)." );
+
+ $this->assertEquals( filesize( $source ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+
+ $props1 = FSFile::getPropsFromPath( $source );
+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( $props1, $props2,
+ "Source and destination have the same props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $dest ) );
+ }
+
+ public static function provider_testStore() {
+ $cases = array();
+
+ $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ $toPath = self::baseStorePath() . '/unittest-cont1/e/fun/obj1.txt';
+ $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath );
+ $cases[] = array(
+ $op, // operation
+ $tmpName, // source
+ $toPath, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $tmpName, // source
+ $toPath, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $tmpName, // source
+ $toPath, // dest
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testCopy
+ * @covers FileBackend::doOperation
+ */
+ public function testCopy( $op ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestCopy( $op );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestCopy( $op );
+ $this->tearDownFiles();
+ }
+
+ private function doTestCopy( $op ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ if ( isset( $op['ignoreMissingSource'] ) ) {
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest does not exist ($backendName)." );
+
+ return; // done
+ }
+
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+
+ if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+ $this->backend->copy( $op );
+ }
+
+ $status = $this->backend->doOperation( $op );
+
+ $this->assertGoodStatus( $status,
+ "Copy from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Copy from $source to $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source still exists ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists after copy ($backendName)." );
+
+ $this->assertEquals(
+ $this->backend->getFileSize( array( 'src' => $source ) ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( $props1, $props2,
+ "Source and destination have the same props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $source, $dest ) );
+ }
+
+ public static function provider_testCopy() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
+ $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+
+ $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest );
+ $cases[] = array(
+ $op, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
+ $dest, // dest
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testMove
+ * @covers FileBackend::doOperation
+ */
+ public function testMove( $op ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestMove( $op );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestMove( $op );
+ $this->tearDownFiles();
+ }
+
+ private function doTestMove( $op ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ if ( isset( $op['ignoreMissingSource'] ) ) {
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest does not exist ($backendName)." );
+
+ return; // done
+ }
+
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+
+ if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+ $this->backend->copy( $op );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Move from $source to $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not still exists ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists after move ($backendName)." );
+
+ $this->assertNotEquals(
+ $this->backend->getFileSize( array( 'src' => $source ) ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( false, $props1['fileExists'],
+ "Source file does not exist accourding to props ($backendName)." );
+ $this->assertEquals( true, $props2['fileExists'],
+ "Destination file exists accourding to props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $source, $dest ) );
+ }
+
+ public static function provider_testMove() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
+ $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+
+ $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest );
+ $cases[] = array(
+ $op, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
+ $dest, // dest
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testDelete
+ * @covers FileBackend::doOperation
+ */
+ public function testDelete( $op, $withSource, $okStatus ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDelete( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDelete( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+ }
+
+ private function doTestDelete( $op, $withSource, $okStatus ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+
+ if ( $withSource ) {
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Deletion of file at $source succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Deletion of file at $source succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Deletion of file at $source has proper 'success' field in Status ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Deletion of file at $source failed ($backendName)." );
+ }
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist after move ($backendName)." );
+
+ $this->assertFalse(
+ $this->backend->getFileSize( array( 'src' => $source ) ),
+ "Source file $source has correct size (false) ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
+ $this->assertFalse( $props1['fileExists'],
+ "Source file $source does not exist according to props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $source ) );
+ }
+
+ public static function provider_testDelete() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
+
+ $op = array( 'op' => 'delete', 'src' => $source );
+ $cases[] = array(
+ $op, // operation
+ true, // with source
+ true // succeeds
+ );
+
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ false // fails
+ );
+
+ $op['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ true // succeeds
+ );
+
+ $op['ignoreMissingSource'] = true;
+ $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt';
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ true // succeeds
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testDescribe
+ * @covers FileBackend::doOperation
+ */
+ public function testDescribe( $op, $withSource, $okStatus ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDescribe( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDescribe( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+ }
+
+ private function doTestDescribe( $op, $withSource, $okStatus ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+
+ if ( $withSource ) {
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Describe of file at $source succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Describe of file at $source succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Describe of file at $source has proper 'success' field in Status ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Describe of file at $source failed ($backendName)." );
+ }
+
+ $this->assertBackendPathsConsistent( array( $source ) );
+ }
+
+ public static function provider_testDescribe() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
+
+ $op = array( 'op' => 'describe', 'src' => $source,
+ 'headers' => array( 'X-Content-Length' => '91.3', 'Content-Old-Header' => '' ),
+ 'disposition' => 'inline' );
+ $cases[] = array(
+ $op, // operation
+ true, // with source
+ true // succeeds
+ );
+
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ false // fails
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testCreate
+ * @covers FileBackend::doOperation
+ */
+ public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
+ $this->tearDownFiles();
+ }
+
+ private function doTestCreate( $op, $alreadyExists, $okStatus, $newSize ) {
+ $backendName = $this->backendClass();
+
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ $oldText = 'blah...blah...waahwaah';
+ if ( $alreadyExists ) {
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $dest succeeded ($backendName)." );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Creation of file at $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of file at $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Creation of file at $dest has proper 'success' field in Status ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Creation of file at $dest failed ($backendName)." );
+ }
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists after creation ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( true, $props1['fileExists'],
+ "Destination file $dest exists according to props ($backendName)." );
+ if ( $okStatus ) { // file content is what we saved
+ $this->assertEquals( $newSize, $props1['size'],
+ "Destination file $dest has expected size according to props ($backendName)." );
+ $this->assertEquals( $newSize,
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+ } else { // file content is some other previous text
+ $this->assertEquals( strlen( $oldText ), $props1['size'],
+ "Destination file $dest has original size according to props ($backendName)." );
+ $this->assertEquals( strlen( $oldText ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has original size according to props ($backendName)." );
+ }
+
+ $this->assertBackendPathsConsistent( array( $dest ) );
+ }
+
+ /**
+ * @dataProvider provider_testCreate
+ */
+ public static function provider_testCreate() {
+ $cases = array();
+
+ $dest = self::baseStorePath() . '/unittest-cont2/a/myspacefile.txt';
+
+ $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest );
+ $cases[] = array(
+ $op, // operation
+ false, // no dest already exists
+ true, // succeeds
+ strlen( $op['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "\n";
+ $cases[] = array(
+ $op2, // operation
+ false, // no dest already exists
+ true, // succeeds
+ strlen( $op2['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "fsf\n waf 3kt";
+ $cases[] = array(
+ $op2, // operation
+ true, // dest already exists
+ false, // fails
+ strlen( $op2['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "egm'g gkpe gpqg eqwgwqg";
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ true, // dest already exists
+ true, // succeeds
+ strlen( $op2['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "39qjmg3-qg";
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ true, // dest already exists
+ false, // succeeds
+ strlen( $op2['content'] )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @covers FileBackend::doQuickOperations
+ */
+ public function testDoQuickOperations() {
+ $this->backend = $this->singleBackend;
+ $this->doTestDoQuickOperations();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->doTestDoQuickOperations();
+ $this->tearDownFiles();
+ }
+
+ private function doTestDoQuickOperations() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+ $files = array(
+ "$base/unittest-cont1/e/fileA.a",
+ "$base/unittest-cont1/e/fileB.a",
+ "$base/unittest-cont1/e/fileC.a"
+ );
+ $createOps = array();
+ $purgeOps = array();
+ foreach ( $files as $path ) {
+ $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
+ $this->assertGoodStatus( $status,
+ "Preparing $path succeeded without warnings ($backendName)." );
+ $createOps[] = array( 'op' => 'create', 'dst' => $path, 'content' => mt_rand( 0, 50000 ) );
+ $copyOps[] = array( 'op' => 'copy', 'src' => $path, 'dst' => "$path-2" );
+ $moveOps[] = array( 'op' => 'move', 'src' => "$path-2", 'dst' => "$path-3" );
+ $purgeOps[] = array( 'op' => 'delete', 'src' => $path );
+ $purgeOps[] = array( 'op' => 'delete', 'src' => "$path-3" );
+ }
+ $purgeOps[] = array( 'op' => 'null' );
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $createOps ),
+ "Creation of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => $file ) ),
+ "File $file exists." );
+ }
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $copyOps ),
+ "Quick copy of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => "$file-2" ) ),
+ "File $file-2 exists." );
+ }
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $moveOps ),
+ "Quick move of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => "$file-3" ) ),
+ "File $file-3 move in." );
+ $this->assertFalse( $this->backend->fileExists( array( 'src' => "$file-2" ) ),
+ "File $file-2 moved away." );
+ }
+
+ $this->assertGoodStatus(
+ $this->backend->quickCopy( array( 'src' => $files[0], 'dst' => $files[0] ) ),
+ "Copy of file {$files[0]} over itself succeeded ($backendName)." );
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => $files[0] ) ),
+ "File {$files[0]} still exists." );
+
+ $this->assertGoodStatus(
+ $this->backend->quickMove( array( 'src' => $files[0], 'dst' => $files[0] ) ),
+ "Move of file {$files[0]} over itself succeeded ($backendName)." );
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => $files[0] ) ),
+ "File {$files[0]} still exists." );
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $purgeOps ),
+ "Quick deletion of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertFalse( $this->backend->fileExists( array( 'src' => $file ) ),
+ "File $file purged." );
+ $this->assertFalse( $this->backend->fileExists( array( 'src' => "$file-3" ) ),
+ "File $file-3 purged." );
+ }
+ }
+
+ /**
+ * @dataProvider provider_testConcatenate
+ */
+ public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
+ $this->filesToPrune[] = $op['dst'];
+
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
+ $this->filesToPrune[] = $op['dst']; # avoid file leaking
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
+ $this->filesToPrune[] = $op['dst']; # avoid file leaking
+ $this->tearDownFiles();
+ }
+
+ private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
+ $backendName = $this->backendClass();
+
+ $expContent = '';
+ // Create sources
+ $ops = array();
+ foreach ( $srcs as $i => $source ) {
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $ops[] = array(
+ 'op' => 'create', // operation
+ 'dst' => $source, // source
+ 'content' => $srcsContent[$i]
+ );
+ $expContent .= $srcsContent[$i];
+ }
+ $status = $this->backend->doOperations( $ops );
+
+ $this->assertGoodStatus( $status,
+ "Creation of source files succeeded ($backendName)." );
+
+ $dest = $params['dst'];
+ if ( $alreadyExists ) {
+ $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false;
+ $this->assertEquals( true, $ok,
+ "Creation of file at $dest succeeded ($backendName)." );
+ } else {
+ $ok = file_put_contents( $dest, '' ) !== false;
+ $this->assertEquals( true, $ok,
+ "Creation of 0-byte file at $dest succeeded ($backendName)." );
+ }
+
+ // Combine the files into one
+ $status = $this->backend->concatenate( $params );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Creation of concat file at $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of concat file at $dest succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Creation of concat file at $dest failed ($backendName)." );
+ }
+
+ if ( $okStatus ) {
+ $this->assertEquals( true, is_file( $dest ),
+ "Dest concat file $dest exists after creation ($backendName)." );
+ } else {
+ $this->assertEquals( true, is_file( $dest ),
+ "Dest concat file $dest exists after failed creation ($backendName)." );
+ }
+
+ $contents = file_get_contents( $dest );
+ $this->assertNotEquals( false, $contents, "File at $dest exists ($backendName)." );
+
+ if ( $okStatus ) {
+ $this->assertEquals( $expContent, $contents,
+ "Concat file at $dest has correct contents ($backendName)." );
+ } else {
+ $this->assertNotEquals( $expContent, $contents,
+ "Concat file at $dest has correct contents ($backendName)." );
+ }
+ }
+
+ public static function provider_testConcatenate() {
+ $cases = array();
+
+ $rand = mt_rand( 0, 2000000000 ) . time();
+ $dest = wfTempDir() . "/randomfile!$rand.txt";
+ $srcs = array(
+ self::baseStorePath() . '/unittest-cont1/e/file1.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file2.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file3.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file4.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file5.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file6.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file7.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file8.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file9.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file10.txt'
+ );
+ $content = array(
+ 'egfage',
+ 'ageageag',
+ 'rhokohlr',
+ 'shgmslkg',
+ 'kenga',
+ 'owagmal',
+ 'kgmae',
+ 'g eak;g',
+ 'lkaem;a',
+ 'legma'
+ );
+ $params = array( 'srcs' => $srcs, 'dst' => $dest );
+
+ $cases[] = array(
+ $params, // operation
+ $srcs, // sources
+ $content, // content for each source
+ false, // no dest already exists
+ true, // succeeds
+ );
+
+ $cases[] = array(
+ $params, // operation
+ $srcs, // sources
+ $content, // content for each source
+ true, // dest already exists
+ false, // succeeds
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetFileStat
+ * @covers FileBackend::getFileStat
+ */
+ public function testGetFileStat( $path, $content, $alreadyExists ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileStat( $path, $content, $alreadyExists );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileStat( $path, $content, $alreadyExists );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileStat( $path, $content, $alreadyExists ) {
+ $backendName = $this->backendClass();
+
+ if ( $alreadyExists ) {
+ $this->prepare( array( 'dir' => dirname( $path ) ) );
+ $status = $this->create( array( 'dst' => $path, 'content' => $content ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $path succeeded ($backendName)." );
+
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+ $time = $this->backend->getFileTimestamp( array( 'src' => $path ) );
+ $stat = $this->backend->getFileStat( array( 'src' => $path ) );
+
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+ $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
+ "Correct file timestamp of '$path'" );
+
+ $size = $stat['size'];
+ $time = $stat['mtime'];
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+ $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
+ "Correct file timestamp of '$path'" );
+
+ $this->backend->clearCache( array( $path ) );
+
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+
+ $this->backend->preloadCache( array( $path ) );
+
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+ } else {
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+ $time = $this->backend->getFileTimestamp( array( 'src' => $path ) );
+ $stat = $this->backend->getFileStat( array( 'src' => $path ) );
+
+ $this->assertFalse( $size, "Correct file size of '$path'" );
+ $this->assertFalse( $time, "Correct file timestamp of '$path'" );
+ $this->assertFalse( $stat, "Correct file stat of '$path'" );
+ }
+ }
+
+ public static function provider_testGetFileStat() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "", true );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-diff_file.txt", null, false );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetFileContents
+ * @covers FileBackend::getFileContents
+ * @covers FileBackend::getFileContentsMulti
+ */
+ public function testGetFileContents( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileContents( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileContents( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileContents( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $srcs = (array)$source;
+ $content = (array)$content;
+ foreach ( $srcs as $i => $src ) {
+ $this->prepare( array( 'dir' => dirname( $src ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $src succeeded ($backendName)." );
+ }
+
+ if ( is_array( $source ) ) {
+ $contents = $this->backend->getFileContentsMulti( array( 'srcs' => $source ) );
+ foreach ( $contents as $path => $data ) {
+ $this->assertNotEquals( false, $data, "Contents of $path exists ($backendName)." );
+ $this->assertEquals( current( $content ), $data, "Contents of $path is correct ($backendName)." );
+ next( $content );
+ }
+ $this->assertEquals( $source, array_keys( $contents ), "Contents in right order ($backendName)." );
+ $this->assertEquals( count( $source ), count( $contents ), "Contents array size correct ($backendName)." );
+ } else {
+ $data = $this->backend->getFileContents( array( 'src' => $source ) );
+ $this->assertNotEquals( false, $data, "Contents of $source exists ($backendName)." );
+ $this->assertEquals( $content[0], $data, "Contents of $source is correct ($backendName)." );
+ }
+ }
+
+ public static function provider_testGetFileContents() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" );
+ $cases[] = array(
+ array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+ "$base/unittest-cont1/e/a/z.txt" ),
+ array( "contents xx", "contents xy", "contents xz" )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetLocalCopy
+ * @covers FileBackend::getLocalCopy
+ */
+ public function testGetLocalCopy( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopy( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopy( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetLocalCopy( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $srcs = (array)$source;
+ $content = (array)$content;
+ foreach ( $srcs as $i => $src ) {
+ $this->prepare( array( 'dir' => dirname( $src ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $src succeeded ($backendName)." );
+ }
+
+ if ( is_array( $source ) ) {
+ $tmpFiles = $this->backend->getLocalCopyMulti( array( 'srcs' => $source ) );
+ foreach ( $tmpFiles as $path => $tmpFile ) {
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $path succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local copy of $path exists ($backendName)." );
+ $this->assertEquals( current( $content ), $contents, "Local copy of $path is correct ($backendName)." );
+ next( $content );
+ }
+ $this->assertEquals( $source, array_keys( $tmpFiles ), "Local copies in right order ($backendName)." );
+ $this->assertEquals( count( $source ), count( $tmpFiles ), "Local copies array size correct ($backendName)." );
+ } else {
+ $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $source succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
+ $this->assertEquals( $content[0], $contents, "Local copy of $source is correct ($backendName)." );
+ }
+
+ $obj = new stdClass();
+ $tmpFile->bind( $obj );
+ }
+
+ public static function provider_testGetLocalCopy() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+ $cases[] = array(
+ array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+ "$base/unittest-cont1/e/a/z.txt" ),
+ array( "contents xx", "contents xy", "contents xz" )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetLocalReference
+ * @covers FileBackend::getLocalReference
+ */
+ public function testGetLocalReference( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalReference( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalReference( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetLocalReference( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $srcs = (array)$source;
+ $content = (array)$content;
+ foreach ( $srcs as $i => $src ) {
+ $this->prepare( array( 'dir' => dirname( $src ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $src succeeded ($backendName)." );
+ }
+
+ if ( is_array( $source ) ) {
+ $tmpFiles = $this->backend->getLocalReferenceMulti( array( 'srcs' => $source ) );
+ foreach ( $tmpFiles as $path => $tmpFile ) {
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $path succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local ref of $path exists ($backendName)." );
+ $this->assertEquals( current( $content ), $contents, "Local ref of $path is correct ($backendName)." );
+ next( $content );
+ }
+ $this->assertEquals( $source, array_keys( $tmpFiles ), "Local refs in right order ($backendName)." );
+ $this->assertEquals( count( $source ), count( $tmpFiles ), "Local refs array size correct ($backendName)." );
+ } else {
+ $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $source succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local ref of $source exists ($backendName)." );
+ $this->assertEquals( $content[0], $contents, "Local ref of $source is correct ($backendName)." );
+ }
+ }
+
+ public static function provider_testGetLocalReference() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+ $cases[] = array(
+ array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+ "$base/unittest-cont1/e/a/z.txt" ),
+ array( "contents xx", "contents xy", "contents xz" )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @covers FileBackend::getLocalCopy
+ * @covers FileBackend::getLocalReference
+ */
+ public function testGetLocalCopyAndReference404() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopyAndReference404();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopyAndReference404();
+ $this->tearDownFiles();
+ }
+
+ public function doTestGetLocalCopyAndReference404() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+
+ $tmpFile = $this->backend->getLocalCopy( array(
+ 'src' => "$base/unittest-cont1/not-there" ) );
+ $this->assertEquals( null, $tmpFile, "Local copy of not existing file is null ($backendName)." );
+
+ $tmpFile = $this->backend->getLocalReference( array(
+ 'src' => "$base/unittest-cont1/not-there" ) );
+ $this->assertEquals( null, $tmpFile, "Local ref of not existing file is null ($backendName)." );
+ }
+
+ /**
+ * @dataProvider provider_testGetFileHttpUrl
+ * @covers FileBackend::getFileHttpUrl
+ */
+ public function testGetFileHttpUrl( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileHttpUrl( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileHttpUrl( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileHttpUrl( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+
+ $url = $this->backend->getFileHttpUrl( array( 'src' => $source ) );
+
+ if ( $url !== null ) { // supported
+ $data = Http::request( "GET", $url );
+ $this->assertEquals( $content, $data,
+ "HTTP GET of URL has right contents ($backendName)." );
+ }
+ }
+
+ public static function provider_testGetFileHttpUrl() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testPrepareAndClean
+ * @covers FileBackend::prepare
+ * @covers FileBackend::clean
+ */
+ public function testPrepareAndClean( $path, $isOK ) {
+ $this->backend = $this->singleBackend;
+ $this->doTestPrepareAndClean( $path, $isOK );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->doTestPrepareAndClean( $path, $isOK );
+ $this->tearDownFiles();
+ }
+
+ public static function provider_testPrepareAndClean() {
+ $base = self::baseStorePath();
+
+ return array(
+ array( "$base/unittest-cont1/e/a/z/some_file1.txt", true ),
+ array( "$base/unittest-cont2/a/z/some_file2.txt", true ),
+ # Specific to FS backend with no basePath field set
+ #array( "$base/unittest-cont3/a/z/some_file3.txt", false ),
+ );
+ }
+
+ private function doTestPrepareAndClean( $path, $isOK ) {
+ $backendName = $this->backendClass();
+
+ $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
+ if ( $isOK ) {
+ $this->assertGoodStatus( $status,
+ "Preparing dir $path succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Preparing dir $path succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Preparing dir $path failed ($backendName)." );
+ }
+
+ $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) );
+ if ( $isOK ) {
+ $this->assertGoodStatus( $status,
+ "Cleaning dir $path succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Cleaning dir $path succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Cleaning dir $path failed ($backendName)." );
+ }
+ }
+
+ public function testRecursiveClean() {
+ $this->backend = $this->singleBackend;
+ $this->doTestRecursiveClean();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->doTestRecursiveClean();
+ $this->tearDownFiles();
+ }
+
+ /**
+ * @covers FileBackend::clean
+ */
+ private function doTestRecursiveClean() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+ $dirs = array(
+ "$base/unittest-cont1",
+ "$base/unittest-cont1/e",
+ "$base/unittest-cont1/e/a",
+ "$base/unittest-cont1/e/a/b",
+ "$base/unittest-cont1/e/a/b/c",
+ "$base/unittest-cont1/e/a/b/c/d0",
+ "$base/unittest-cont1/e/a/b/c/d1",
+ "$base/unittest-cont1/e/a/b/c/d2",
+ "$base/unittest-cont1/e/a/b/c/d0/1",
+ "$base/unittest-cont1/e/a/b/c/d0/2",
+ "$base/unittest-cont1/e/a/b/c/d1/3",
+ "$base/unittest-cont1/e/a/b/c/d1/4",
+ "$base/unittest-cont1/e/a/b/c/d2/5",
+ "$base/unittest-cont1/e/a/b/c/d2/6"
+ );
+ foreach ( $dirs as $dir ) {
+ $status = $this->prepare( array( 'dir' => $dir ) );
+ $this->assertGoodStatus( $status,
+ "Preparing dir $dir succeeded without warnings ($backendName)." );
+ }
+
+ if ( $this->backend instanceof FSFileBackend ) {
+ foreach ( $dirs as $dir ) {
+ $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+ "Dir $dir exists ($backendName)." );
+ }
+ }
+
+ $status = $this->backend->clean(
+ array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) );
+ $this->assertGoodStatus( $status,
+ "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
+
+ foreach ( $dirs as $dir ) {
+ $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+ "Dir $dir no longer exists ($backendName)." );
+ }
+ }
+
+ // @todo testSecure
+
+ /**
+ * @covers FileBackend::doOperations
+ */
+ public function testDoOperations() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperations();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperations();
+ $this->tearDownFiles();
+ }
+
+ private function doTestDoOperations() {
+ $base = self::baseStorePath();
+
+ $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+ $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'describe', 'src' => $fileA,
+ 'headers' => array( 'X-Content-Length' => '91.3' ), 'disposition' => 'inline' ),
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+ array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+ // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'null' ),
+ // Does nothing
+ ) );
+
+ $this->assertGoodStatus( $status, "Operation batch succeeded" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 14, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileC ) ),
+ "Correct file contents of $fileC" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileC ) ),
+ "Correct file size of $fileC" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+ "Correct file SHA-1 of $fileC" );
+ }
+
+ /**
+ * @covers FileBackend::doOperations
+ */
+ public function testDoOperationsPipeline() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsPipeline();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsPipeline();
+ $this->tearDownFiles();
+ }
+
+ // concurrency orientated
+ private function doTestDoOperationsPipeline() {
+ $base = self::baseStorePath();
+
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+
+ $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameA, $fileAContents );
+ $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameB, $fileBContents );
+ $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameC, $fileCContents );
+
+ $this->filesToPrune[] = $tmpNameA; # avoid file leaking
+ $this->filesToPrune[] = $tmpNameB; # avoid file leaking
+ $this->filesToPrune[] = $tmpNameC; # avoid file leaking
+
+ $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
+ $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
+ $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
+ $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ),
+ array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ),
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+ array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+ // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'null' ),
+ // Does nothing
+ ) );
+
+ $this->assertGoodStatus( $status, "Operation batch succeeded" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 16, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileC ) ),
+ "Correct file contents of $fileC" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileC ) ),
+ "Correct file size of $fileC" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+ "Correct file SHA-1 of $fileC" );
+ }
+
+ /**
+ * @covers FileBackend::doOperations
+ */
+ public function testDoOperationsFailing() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsFailing();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsFailing();
+ $this->tearDownFiles();
+ }
+
+ private function doTestDoOperationsFailing() {
+ $base = self::baseStorePath();
+
+ $fileA = "$base/unittest-cont2/a/b/fileA.txt";
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileB = "$base/unittest-cont2/a/b/fileB.txt";
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileC = "$base/unittest-cont2/a/b/fileC.txt";
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+ $fileD = "$base/unittest-cont2/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileB, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<B>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
+ array( 'op' => 'delete', 'src' => $fileD ),
+ // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
+ array( 'op' => 'null' ),
+ // Does nothing
+ ), array( 'force' => 1 ) );
+
+ $this->assertNotEquals( array(), $status->errors, "Operation had warnings" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 8, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileA ) ),
+ "Correct file contents of $fileA" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileA ) ),
+ "Correct file size of $fileA" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileA ) ),
+ "Correct file SHA-1 of $fileA" );
+ }
+
+ /**
+ * @covers FileBackend::getFileList
+ */
+ public function testGetFileList() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileList();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileList();
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileList() {
+ $backendName = $this->backendClass();
+ $base = self::baseStorePath();
+
+ // Should have no errors
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) );
+
+ $files = array(
+ "$base/unittest-cont1/e/test1.txt",
+ "$base/unittest-cont1/e/test2.txt",
+ "$base/unittest-cont1/e/test3.txt",
+ "$base/unittest-cont1/e/subdir1/test1.txt",
+ "$base/unittest-cont1/e/subdir1/test2.txt",
+ "$base/unittest-cont1/e/subdir2/test3.txt",
+ "$base/unittest-cont1/e/subdir2/test4.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test2.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test3.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test4.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test5.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt",
+ );
+
+ // Add the files
+ $ops = array();
+ foreach ( $files as $file ) {
+ $this->prepare( array( 'dir' => dirname( $file ) ) );
+ $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
+ }
+ $status = $this->backend->doQuickOperations( $ops );
+ $this->assertGoodStatus( $status,
+ "Creation of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of files succeeded with OK status ($backendName)." );
+
+ // Expected listing at root
+ $expected = array(
+ "e/test1.txt",
+ "e/test2.txt",
+ "e/test3.txt",
+ "e/subdir1/test1.txt",
+ "e/subdir1/test2.txt",
+ "e/subdir2/test3.txt",
+ "e/subdir2/test4.txt",
+ "e/subdir2/subdir/test1.txt",
+ "e/subdir2/subdir/test2.txt",
+ "e/subdir2/subdir/test3.txt",
+ "e/subdir2/subdir/test4.txt",
+ "e/subdir2/subdir/test5.txt",
+ "e/subdir2/subdir/sub/test0.txt",
+ "e/subdir2/subdir/sub/120-px-file.txt",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash) at root
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (no trailing slash) at root with advise
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1", 'adviseStat' => 1 ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (with trailing slash) at root
+ $list = array();
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Expected listing at subdir
+ $expected = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "test4.txt",
+ "test5.txt",
+ "sub/test0.txt",
+ "sub/120-px-file.txt",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash) at subdir
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (no trailing slash) at subdir with advise
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir", 'adviseStat' => 1 ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (with trailing slash) at subdir
+ $list = array();
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (using iterator second time)
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." );
+
+ // Actual listing (top files only) at root
+ $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( array(), $list, "Correct top file listing ($backendName)." );
+
+ // Expected listing (top files only) at subdir
+ $expected = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "test4.txt",
+ "test5.txt"
+ );
+ sort( $expected );
+
+ // Actual listing (top files only) at subdir
+ $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
+ // Actual listing (top files only) at subdir with advise
+ $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir", 'adviseStat' => 1 ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
+ foreach ( $files as $file ) { // clean up
+ $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+ }
+
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+ foreach ( $iter as $iter ) {
+ // no errors
+ }
+ }
+
+ /**
+ * @covers FileBackend::getTopDirectoryList
+ * @covers FileBackend::getDirectoryList
+ */
+ public function testGetDirectoryList() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetDirectoryList();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetDirectoryList();
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetDirectoryList() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+ $files = array(
+ "$base/unittest-cont1/e/test1.txt",
+ "$base/unittest-cont1/e/test2.txt",
+ "$base/unittest-cont1/e/test3.txt",
+ "$base/unittest-cont1/e/subdir1/test1.txt",
+ "$base/unittest-cont1/e/subdir1/test2.txt",
+ "$base/unittest-cont1/e/subdir2/test3.txt",
+ "$base/unittest-cont1/e/subdir2/test4.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
+ "$base/unittest-cont1/e/subdir3/subdir/test2.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/test3.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/test4.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/test5.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt",
+ );
+
+ // Add the files
+ $ops = array();
+ foreach ( $files as $file ) {
+ $this->prepare( array( 'dir' => dirname( $file ) ) );
+ $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
+ }
+ $status = $this->backend->doQuickOperations( $ops );
+ $this->assertGoodStatus( $status,
+ "Creation of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of files succeeded with OK status ($backendName)." );
+
+ $this->assertEquals( true,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ),
+ "Directory exists in ($backendName)." );
+ $this->assertEquals( true,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ),
+ "Directory exists in ($backendName)." );
+ $this->assertEquals( false,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ) ),
+ "Directory does not exists in ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "e",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "subdir1",
+ "subdir2",
+ "subdir3",
+ "subdir4",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (with trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "subdir",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (with trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (using iterator second time)
+ $list = array();
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." );
+
+ // Expected listing (recursive)
+ $expected = array(
+ "e",
+ "e/subdir1",
+ "e/subdir2",
+ "e/subdir3",
+ "e/subdir4",
+ "e/subdir2/subdir",
+ "e/subdir3/subdir",
+ "e/subdir4/subdir",
+ "e/subdir4/subdir/sub",
+ );
+ sort( $expected );
+
+ // Actual listing (recursive)
+ $list = array();
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ // Expected listing (recursive)
+ $expected = array(
+ "subdir",
+ "subdir/sub",
+ );
+ sort( $expected );
+
+ // Actual listing (recursive)
+ $list = array();
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir4" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ // Actual listing (recursive, second time)
+ $list = array();
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) );
+ $items = $this->listToArray( $iter );
+ $this->assertEquals( array(), $items, "Directory listing is empty." );
+
+ foreach ( $files as $file ) { // clean up
+ $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+ }
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+ foreach ( $iter as $file ) {
+ // no errors
+ }
+
+ $items = $this->listToArray( $iter );
+ $this->assertEquals( array(), $items, "Directory listing is empty." );
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/not/exists" ) );
+ $items = $this->listToArray( $iter );
+ $this->assertEquals( array(), $items, "Directory listing is empty." );
+ }
+
+ /**
+ * @covers FileBackend::lockFiles
+ * @covers FileBackend::unlockFiles
+ */
+ public function testLockCalls() {
+ $this->backend = $this->singleBackend;
+ $this->doTestLockCalls();
+ }
+
+ private function doTestLockCalls() {
+ $backendName = $this->backendClass();
+
+ $paths = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "subdir1",
+ "subdir1", // duplicate
+ "subdir1/test1.txt",
+ "subdir1/test2.txt",
+ "subdir2",
+ "subdir2", // duplicate
+ "subdir2/test3.txt",
+ "subdir2/test4.txt",
+ "subdir2/subdir",
+ "subdir2/subdir/test1.txt",
+ "subdir2/subdir/test2.txt",
+ "subdir2/subdir/test3.txt",
+ "subdir2/subdir/test4.txt",
+ "subdir2/subdir/test5.txt",
+ "subdir2/subdir/sub",
+ "subdir2/subdir/sub/test0.txt",
+ "subdir2/subdir/sub/120-px-file.txt",
+ );
+
+ for ( $i = 0; $i < 25; $i++ ) {
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName). ($i)" );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ ## Flip the acquire/release ordering around ##
+
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName). ($i)" );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+ }
+
+ $status = Status::newGood();
+ $sl = $this->backend->getScopedFileLocks( $paths, LockManager::LOCK_EX, $status );
+ $this->assertType( 'ScopedLock', $sl,
+ "Scoped locking of files succeeded ($backendName)." );
+ $this->assertEquals( array(), $status->errors,
+ "Scoped locking of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Scoped locking of files succeeded with OK status ($backendName)." );
+
+ ScopedLock::release( $sl );
+ $this->assertEquals( null, $sl,
+ "Scoped unlocking of files succeeded ($backendName)." );
+ $this->assertEquals( array(), $status->errors,
+ "Scoped unlocking of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Scoped unlocking of files succeeded with OK status ($backendName)." );
+ }
+
+ // helper function
+ private function listToArray( $iter ) {
+ return is_array( $iter ) ? $iter : iterator_to_array( $iter );
+ }
+
+ // test helper wrapper for backend prepare() function
+ private function prepare( array $params ) {
+ return $this->backend->prepare( $params );
+ }
+
+ // test helper wrapper for backend prepare() function
+ private function create( array $params ) {
+ $params['op'] = 'create';
+
+ return $this->backend->doQuickOperations( array( $params ) );
+ }
+
+ function tearDownFiles() {
+ foreach ( $this->filesToPrune as $file ) {
+ if ( is_file( $file ) ) {
+ unlink( $file );
+ }
+ }
+ $containers = array( 'unittest-cont1', 'unittest-cont2' );
+ foreach ( $containers as $container ) {
+ $this->deleteFiles( $container );
+ }
+ $this->filesToPrune = array();
+ }
+
+ private function deleteFiles( $container ) {
+ $base = self::baseStorePath();
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) );
+ if ( $iter ) {
+ foreach ( $iter as $file ) {
+ $this->backend->quickDelete( array( 'src' => "$base/$container/$file" ) );
+ }
+ // free the directory, to avoid Permission denied under windows on rmdir
+ unset( $iter );
+ }
+ $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
+ }
+
+ function assertBackendPathsConsistent( array $paths ) {
+ if ( $this->backend instanceof FileBackendMultiWrite ) {
+ $status = $this->backend->consistencyCheck( $paths );
+ $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+ }
+ }
+
+ function assertGoodStatus( $status, $msg ) {
+ $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
+ }
+}
diff --git a/tests/phpunit/includes/filerepo/FileRepoTest.php b/tests/phpunit/includes/filerepo/FileRepoTest.php
new file mode 100644
index 00000000..e3a75567
--- /dev/null
+++ b/tests/phpunit/includes/filerepo/FileRepoTest.php
@@ -0,0 +1,55 @@
+<?php
+
+class FileRepoTest extends MediaWikiTestCase {
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionCanNotBeNull() {
+ new FileRepo();
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionCanNotBeAnEmptyArray() {
+ new FileRepo( array() );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionNeedNameKey() {
+ new FileRepo( array(
+ 'backend' => 'foobar'
+ ) );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionNeedBackendKey() {
+ new FileRepo( array(
+ 'name' => 'foobar'
+ ) );
+ }
+
+ /**
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionWithRequiredOptions() {
+ $f = new FileRepo( array(
+ 'name' => 'FileRepoTestRepository',
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-testing',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array()
+ ) )
+ ) );
+ $this->assertInstanceOf( 'FileRepo', $f );
+ }
+}
diff --git a/tests/phpunit/includes/filerepo/StoreBatchTest.php b/tests/phpunit/includes/filerepo/StoreBatchTest.php
new file mode 100644
index 00000000..b33c1bbb
--- /dev/null
+++ b/tests/phpunit/includes/filerepo/StoreBatchTest.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @group FileRepo
+ * @group medium
+ */
+class StoreBatchTest extends MediaWikiTestCase {
+
+ protected $createdFiles;
+ protected $date;
+ /** @var FileRepo */
+ protected $repo;
+
+ protected function setUp() {
+ global $wgFileBackends;
+ parent::setUp();
+
+ # Forge a FSRepo object to not have to rely on local wiki settings
+ $tmpPrefix = wfTempDir() . '/storebatch-test-' . time() . '-' . mt_rand();
+ if ( $this->getCliArg( 'use-filebackend=' ) ) {
+ $name = $this->getCliArg( 'use-filebackend=' );
+ $useConfig = array();
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] == $name ) {
+ $useConfig = $conf;
+ }
+ }
+ $useConfig['name'] = 'local-testing'; // swap name
+ $class = $useConfig['class'];
+ $backend = new $class( $useConfig );
+ } else {
+ $backend = new FSFileBackend( array(
+ 'name' => 'local-testing',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array(
+ 'unittests-public' => "{$tmpPrefix}-public",
+ 'unittests-thumb' => "{$tmpPrefix}-thumb",
+ 'unittests-temp' => "{$tmpPrefix}-temp",
+ 'unittests-deleted' => "{$tmpPrefix}-deleted",
+ )
+ ) );
+ }
+ $this->repo = new FileRepo( array(
+ 'name' => 'unittests',
+ 'backend' => $backend
+ ) );
+
+ $this->date = gmdate( "YmdHis" );
+ $this->createdFiles = array();
+ }
+
+ protected function tearDown() {
+ $this->repo->cleanupBatch( $this->createdFiles ); // delete files
+ foreach ( $this->createdFiles as $tmp ) { // delete dirs
+ $tmp = $this->repo->resolveVirtualUrl( $tmp );
+ while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
+ $this->repo->getBackend()->clean( array( 'dir' => $tmp ) );
+ }
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * Store a file or virtual URL source into a media file name.
+ *
+ * @param $originalName string The title of the image
+ * @param $srcPath string The filepath or virtual URL
+ * @param $flags integer Flags to pass into repo::store().
+ * @return FileRepoStatus
+ */
+ private function storeit( $originalName, $srcPath, $flags ) {
+ $hashPath = $this->repo->getHashPath( $originalName );
+ $dstRel = "$hashPath{$this->date}!$originalName";
+ $dstUrlRel = $hashPath . $this->date . '!' . rawurlencode( $originalName );
+
+ $result = $this->repo->store( $srcPath, 'temp', $dstRel, $flags );
+ $result->value = $this->repo->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
+ $this->createdFiles[] = $result->value;
+
+ return $result;
+ }
+
+ /**
+ * Test storing a file using different flags.
+ *
+ * @param $fn string The title of the image
+ * @param $infn string The name of the file (in the filesystem)
+ * @param $otherfn string The name of the different file (in the filesystem)
+ * @param $fromrepo bool 'true' if we want to copy from a virtual URL out of the Repo.
+ */
+ private function storecohort( $fn, $infn, $otherfn, $fromrepo ) {
+ $f = $this->storeit( $fn, $infn, 0 );
+ $this->assertTrue( $f->isOK(), 'failed to store a new file' );
+ $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ if ( $fromrepo ) {
+ $f = $this->storeit( "Other-$fn", $infn, FileRepo::OVERWRITE );
+ $infn = $f->value;
+ }
+ // This should work because we're allowed to overwrite
+ $f = $this->storeit( $fn, $infn, FileRepo::OVERWRITE );
+ $this->assertTrue( $f->isOK(), 'We should be allowed to overwrite' );
+ $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ // This should fail because we're overwriting.
+ $f = $this->storeit( $fn, $infn, 0 );
+ $this->assertFalse( $f->isOK(), 'We should not be allowed to overwrite' );
+ $this->assertEquals( $f->failCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ // This should succeed because we're overwriting the same content.
+ $f = $this->storeit( $fn, $infn, FileRepo::OVERWRITE_SAME );
+ $this->assertTrue( $f->isOK(), 'We should be able to overwrite the same content' );
+ $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ // This should fail because we're overwriting different content.
+ if ( $fromrepo ) {
+ $f = $this->storeit( "Other-$fn", $otherfn, FileRepo::OVERWRITE );
+ $otherfn = $f->value;
+ }
+ $f = $this->storeit( $fn, $otherfn, FileRepo::OVERWRITE_SAME );
+ $this->assertFalse( $f->isOK(), 'We should not be allowed to overwrite different content' );
+ $this->assertEquals( $f->failCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ }
+
+ /**
+ * @covers FileRepo::store
+ */
+ public function teststore() {
+ global $IP;
+ $this->storecohort( "Test1.png", "$IP/skins/monobook/wiki.png", "$IP/skins/monobook/video.png", false );
+ $this->storecohort( "Test2.png", "$IP/skins/monobook/wiki.png", "$IP/skins/monobook/video.png", true );
+ }
+}
diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
new file mode 100644
index 00000000..0e5f2671
--- /dev/null
+++ b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
@@ -0,0 +1,65 @@
+<?php
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+class InstallDocFormatterTest extends MediaWikiTestCase {
+ /**
+ * @covers InstallDocFormatter::format
+ * @dataProvider provideDocFormattingTests
+ */
+ public function testFormat( $expected, $unformattedText, $message = '' ) {
+ $this->assertEquals(
+ $expected,
+ InstallDocFormatter::format( $unformattedText ),
+ $message
+ );
+ }
+
+ /**
+ * Provider for testFormat()
+ */
+ public static function provideDocFormattingTests() {
+ # Format: (expected string, unformattedText string, optional message)
+ return array(
+ # Escape some wikitext
+ array( 'Install &lt;tag>', 'Install <tag>', 'Escaping <' ),
+ array( 'Install &#123;&#123;template}}', 'Install {{template}}', 'Escaping [[' ),
+ array( 'Install &#91;&#91;page]]', 'Install [[page]]', 'Escaping {{' ),
+ array( 'Install &#95;&#95;TOC&#95;&#95;', 'Install __TOC__', 'Escaping __' ),
+ array( 'Install ', "Install \r", 'Removing \r' ),
+
+ # Transform \t{1,2} into :{1,2}
+ array( ':One indentation', "\tOne indentation", 'Replacing a single \t' ),
+ array( '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ),
+
+ # Transform 'bug 123' links
+ array(
+ '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>',
+ 'bug 123', 'Testing bug 123 links' ),
+ array(
+ '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)',
+ '(bug 987654)', 'Testing (bug 987654) links' ),
+
+ # "bug abc" shouldn't work
+ array( 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ),
+ array( 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ),
+
+ # Transform '$wgFooBar' links
+ array(
+ '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
+ '$wgFooBar', 'Testing basic $wgFooBar' ),
+ array(
+ '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
+ '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ),
+ array(
+ '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>',
+ '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ),
+
+ # Icky variables that shouldn't link
+ array( '$myAwesomeVariable', '$myAwesomeVariable', 'Testing $myAwesomeVariable (not starting with $wg)' ),
+ array( '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/installer/OracleInstallerTest.php b/tests/phpunit/includes/installer/OracleInstallerTest.php
new file mode 100644
index 00000000..66e65592
--- /dev/null
+++ b/tests/phpunit/includes/installer/OracleInstallerTest.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Tests for OracleInstaller
+ *
+ * @group Database
+ * @group Installer
+ */
+
+class OracleInstallerTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideOracleConnectStrings
+ * @covers OracleInstaller::checkConnectStringFormat
+ */
+ public function testCheckConnectStringFormat( $expected, $connectString, $msg = '' ) {
+ $validity = $expected ? 'should be valid' : 'should NOT be valid';
+ $msg = "'$connectString' ($msg) $validity.";
+ $this->assertEquals( $expected,
+ OracleInstaller::checkConnectStringFormat( $connectString ),
+ $msg
+ );
+ }
+
+ /**
+ * Provider to test OracleInstaller::checkConnectStringFormat()
+ */
+ function provideOracleConnectStrings() {
+ // expected result, connectString[, message]
+ return array(
+ array( true, 'simple_01', 'Simple TNS name' ),
+ array( true, 'simple_01.world', 'TNS name with domain' ),
+ array( true, 'simple_01.domain.net', 'TNS name with domain' ),
+ array( true, 'host123', 'Host only' ),
+ array( true, 'host123.domain.net', 'FQDN only' ),
+ array( true, '//host123.domain.net', 'FQDN URL only' ),
+ array( true, '123.223.213.132', 'Host IP only' ),
+ array( true, 'host:1521', 'Host and port' ),
+ array( true, 'host:1521/service', 'Host, port and service' ),
+ array( true, 'host:1521/service:shared', 'Host, port, service and shared server type' ),
+ array( true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ),
+ array( true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ),
+ array( true, 'host:1521/service:shared/instance1', 'Host, port, service, server type and instance' ),
+ array( true, 'host:1521//instance1', 'Host, port and instance' ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/jobqueue/JobQueueTest.php b/tests/phpunit/includes/jobqueue/JobQueueTest.php
new file mode 100644
index 00000000..4e51c4fc
--- /dev/null
+++ b/tests/phpunit/includes/jobqueue/JobQueueTest.php
@@ -0,0 +1,329 @@
+<?php
+
+/**
+ * @group JobQueue
+ * @group medium
+ * @group Database
+ */
+class JobQueueTest extends MediaWikiTestCase {
+ protected $key;
+ protected $queueRand, $queueRandTTL, $queueFifo, $queueFifoTTL;
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed[] = 'job';
+ }
+
+ protected function setUp() {
+ global $wgJobTypeConf;
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgMemc', new HashBagOStuff() );
+
+ if ( $this->getCliArg( 'use-jobqueue=' ) ) {
+ $name = $this->getCliArg( 'use-jobqueue=' );
+ if ( !isset( $wgJobTypeConf[$name] ) ) {
+ throw new MWException( "No \$wgJobTypeConf entry for '$name'." );
+ }
+ $baseConfig = $wgJobTypeConf[$name];
+ } else {
+ $baseConfig = array( 'class' => 'JobQueueDB' );
+ }
+ $baseConfig['type'] = 'null';
+ $baseConfig['wiki'] = wfWikiID();
+ $variants = array(
+ 'queueRand' => array( 'order' => 'random', 'claimTTL' => 0 ),
+ 'queueRandTTL' => array( 'order' => 'random', 'claimTTL' => 10 ),
+ 'queueTimestamp' => array( 'order' => 'timestamp', 'claimTTL' => 0 ),
+ 'queueTimestampTTL' => array( 'order' => 'timestamp', 'claimTTL' => 10 ),
+ 'queueFifo' => array( 'order' => 'fifo', 'claimTTL' => 0 ),
+ 'queueFifoTTL' => array( 'order' => 'fifo', 'claimTTL' => 10 ),
+ );
+ foreach ( $variants as $q => $settings ) {
+ try {
+ $this->$q = JobQueue::factory( $settings + $baseConfig );
+ if ( !( $this->$q instanceof JobQueueDB ) ) {
+ $this->$q->setTestingPrefix( 'unittests-' . wfRandomString( 32 ) );
+ }
+ } catch ( MWException $e ) {
+ // unsupported?
+ // @todo What if it was another error?
+ };
+ }
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ foreach (
+ array(
+ 'queueRand', 'queueRandTTL', 'queueTimestamp', 'queueTimestampTTL',
+ 'queueFifo', 'queueFifoTTL'
+ ) as $q
+ ) {
+ if ( $this->$q ) {
+ $this->$q->delete();
+ }
+ $this->$q = null;
+ }
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ */
+ public function testProperties( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" );
+ $this->assertEquals( 'null', $queue->getType(), "Proper job type ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ */
+ public function testBasicOperations( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $this->assertTrue( $queue->push( $this->newJob() ), "Push worked ($desc)" );
+ $this->assertTrue( $queue->batchPush( array( $this->newJob() ) ), "Push worked ($desc)" );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 2, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+ $jobs = iterator_to_array( $queue->getAllQueuedJobs() );
+ $this->assertEquals( 2, count( $jobs ), "Queue iterator size is correct ($desc)" );
+
+ $job1 = $queue->pop();
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $job2 = $queue->pop();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 2, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job1 );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job2 );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+
+ $this->assertTrue( $queue->batchPush( array( $this->newJob(), $this->newJob() ) ),
+ "Push worked ($desc)" );
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->delete();
+ $queue->flushCaches();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ */
+ public function testBasicDeduplication( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $this->assertTrue(
+ $queue->batchPush(
+ array( $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() )
+ ),
+ "Push worked ($desc)" );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $this->assertTrue(
+ $queue->batchPush(
+ array( $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() )
+ ),
+ "Push worked ($desc)"
+ );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $job1 = $queue->pop();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job1 );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ */
+ public function testRootDeduplication( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $id = wfRandomString( 32 );
+ $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+ for ( $i = 0; $i < 5; ++$i ) {
+ $this->assertTrue( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" );
+ }
+ $queue->deduplicateRootJob( $this->newJob( 0, $root1 ) );
+ sleep( 1 ); // roo job timestamp will increase
+ $root2 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+ $this->assertNotEquals( $root1['rootJobTimestamp'], $root2['rootJobTimestamp'],
+ "Root job signatures have different timestamps." );
+ for ( $i = 0; $i < 5; ++$i ) {
+ $this->assertTrue( $queue->push( $this->newJob( 0, $root2 ) ), "Push worked ($desc)" );
+ }
+ $queue->deduplicateRootJob( $this->newJob( 0, $root2 ) );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 10, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $dupcount = 0;
+ $jobs = array();
+ do {
+ $job = $queue->pop();
+ if ( $job ) {
+ $jobs[] = $job;
+ $queue->ack( $job );
+ }
+ if ( $job instanceof DuplicateJob ) {
+ ++$dupcount;
+ }
+ } while ( $job );
+
+ $this->assertEquals( 10, count( $jobs ), "Correct number of jobs popped ($desc)" );
+ $this->assertEquals( 5, $dupcount, "Correct number of duplicate jobs popped ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_fifoQueueLists
+ */
+ public function testJobOrder( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ for ( $i = 0; $i < 10; ++$i ) {
+ $this->assertTrue( $queue->push( $this->newJob( $i ) ), "Push worked ($desc)" );
+ }
+
+ for ( $i = 0; $i < 10; ++$i ) {
+ $job = $queue->pop();
+ $this->assertTrue( $job instanceof Job, "Jobs popped from queue ($desc)" );
+ $params = $job->getParams();
+ $this->assertEquals( $i, $params['i'], "Job popped from queue is FIFO ($desc)" );
+ $queue->ack( $job );
+ }
+
+ $this->assertFalse( $queue->pop(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+ }
+
+ public static function provider_queueLists() {
+ return array(
+ array( 'queueRand', false, 'Random queue without ack()' ),
+ array( 'queueRandTTL', true, 'Random queue with ack()' ),
+ array( 'queueTimestamp', false, 'Time ordered queue without ack()' ),
+ array( 'queueTimestampTTL', true, 'Time ordered queue with ack()' ),
+ array( 'queueFifo', false, 'FIFO ordered queue without ack()' ),
+ array( 'queueFifoTTL', true, 'FIFO ordered queue with ack()' )
+ );
+ }
+
+ public static function provider_fifoQueueLists() {
+ return array(
+ array( 'queueFifo', false, 'Ordered queue without ack()' ),
+ array( 'queueFifoTTL', true, 'Ordered queue with ack()' )
+ );
+ }
+
+ function newJob( $i = 0, $rootJob = array() ) {
+ return new NullJob( Title::newMainPage(),
+ array( 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 0, 'i' => $i ) + $rootJob );
+ }
+
+ function newDedupedJob( $i = 0, $rootJob = array() ) {
+ return new NullJob( Title::newMainPage(),
+ array( 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 1, 'i' => $i ) + $rootJob );
+ }
+}
diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php
new file mode 100644
index 00000000..149be05b
--- /dev/null
+++ b/tests/phpunit/includes/json/FormatJsonTest.php
@@ -0,0 +1,161 @@
+<?php
+
+class FormatJsonTest extends MediaWikiTestCase {
+
+ public function testEncoderPrettyPrinting() {
+ $obj = array(
+ 'emptyObject' => new stdClass,
+ 'emptyArray' => array(),
+ 'string' => 'foobar\\',
+ 'filledArray' => array(
+ array(
+ 123,
+ 456,
+ ),
+ // Nested json works without problems
+ '"7":["8",{"9":"10"}]',
+ // Whitespace clean up doesn't touch strings that look alike
+ "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
+ ),
+ );
+
+ // 4 space indent, no trailing whitespace, no trailing linefeed
+ $json = '{
+ "emptyObject": {},
+ "emptyArray": [],
+ "string": "foobar\\\\",
+ "filledArray": [
+ [
+ 123,
+ 456
+ ],
+ "\"7\":[\"8\",{\"9\":\"10\"}]",
+ "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
+ ]
+}';
+
+ $json = str_replace( "\r", '', $json ); // Windows compat
+ $this->assertSame( $json, FormatJson::encode( $obj, true ) );
+ }
+
+ public static function provideEncodeDefault() {
+ return self::getEncodeTestCases( array() );
+ }
+
+ /**
+ * @dataProvider provideEncodeDefault
+ */
+ public function testEncodeDefault( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from ) );
+ }
+
+ public static function provideEncodeUtf8() {
+ return self::getEncodeTestCases( array( 'unicode' ) );
+ }
+
+ /**
+ * @dataProvider provideEncodeUtf8
+ */
+ public function testEncodeUtf8( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) );
+ }
+
+ public static function provideEncodeXmlMeta() {
+ return self::getEncodeTestCases( array( 'xmlmeta' ) );
+ }
+
+ /**
+ * @dataProvider provideEncodeXmlMeta
+ */
+ public function testEncodeXmlMeta( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) );
+ }
+
+ public static function provideEncodeAllOk() {
+ return self::getEncodeTestCases( array( 'unicode', 'xmlmeta' ) );
+ }
+
+ /**
+ * @dataProvider provideEncodeAllOk
+ */
+ public function testEncodeAllOk( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) );
+ }
+
+ public function testEncodePhpBug46944() {
+ $this->assertNotEquals(
+ '\ud840\udc00',
+ strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
+ 'Test encoding an broken json_encode character (U+20000)'
+ );
+ }
+
+ public function testDecodeReturnType() {
+ $this->assertInternalType(
+ 'object',
+ FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
+ 'Default to object'
+ );
+
+ $this->assertInternalType(
+ 'array',
+ FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
+ 'Optional array'
+ );
+ }
+
+ /**
+ * Generate a set of test cases for a particular combination of encoder options.
+ *
+ * @param array $unescapedGroups List of character groups to leave unescaped
+ * @return array: Arrays of unencoded strings and corresponding encoded strings
+ */
+ private static function getEncodeTestCases( array $unescapedGroups ) {
+ $groups = array(
+ 'always' => array(
+ // Forward slash (always unescaped)
+ '/' => '/',
+
+ // Control characters
+ "\0" => '\u0000',
+ "\x08" => '\b',
+ "\t" => '\t',
+ "\n" => '\n',
+ "\r" => '\r',
+ "\f" => '\f',
+ "\x1f" => '\u001f', // representative example
+
+ // Double quotes
+ '"' => '\"',
+
+ // Backslashes
+ '\\' => '\\\\',
+ '\\\\' => '\\\\\\\\',
+ '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
+
+ // Line terminators
+ "\xe2\x80\xa8" => '\u2028',
+ "\xe2\x80\xa9" => '\u2029',
+ ),
+ 'unicode' => array(
+ "\xc3\xa9" => '\u00e9',
+ "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
+ ),
+ 'xmlmeta' => array(
+ '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
+ '>' => '\u003E',
+ '&' => '\u0026',
+ ),
+ );
+
+ $cases = array();
+ foreach ( $groups as $name => $rules ) {
+ $leaveUnescaped = in_array( $name, $unescapedGroups );
+ foreach ( $rules as $from => $to ) {
+ $cases[] = array( $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' );
+ }
+ }
+
+ return $cases;
+ }
+}
diff --git a/tests/phpunit/includes/libs/CSSJanusTest.php b/tests/phpunit/includes/libs/CSSJanusTest.php
new file mode 100644
index 00000000..5a3c1619
--- /dev/null
+++ b/tests/phpunit/includes/libs/CSSJanusTest.php
@@ -0,0 +1,606 @@
+<?php
+/**
+ * Based on the test suite of the original Python
+ * CSSJanus libary:
+ * http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus_test.py
+ * Ported to PHP for ResourceLoader and has been extended since.
+ *
+ * @covers CSSJanus
+ */
+class CSSJanusTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideTransformCases
+ */
+ public function testTransform( $cssA, $cssB = null ) {
+
+ if ( $cssB ) {
+ $transformedA = CSSJanus::transform( $cssA );
+ $this->assertEquals( $transformedA, $cssB, 'Test A-B transformation' );
+
+ $transformedB = CSSJanus::transform( $cssB );
+ $this->assertEquals( $transformedB, $cssA, 'Test B-A transformation' );
+ } else {
+ // If no B version is provided, it means
+ // the output should equal the input.
+ $transformedA = CSSJanus::transform( $cssA );
+ $this->assertEquals( $transformedA, $cssA, 'Nothing was flipped' );
+ }
+ }
+
+ /**
+ * @dataProvider provideTransformAdvancedCases
+ */
+ public function testTransformAdvanced( $code, $expectedOutput, $options = array() ) {
+ $swapLtrRtlInURL = isset( $options['swapLtrRtlInURL'] ) ? $options['swapLtrRtlInURL'] : false;
+ $swapLeftRightInURL = isset( $options['swapLeftRightInURL'] ) ? $options['swapLeftRightInURL'] : false;
+
+ $flipped = CSSJanus::transform( $code, $swapLtrRtlInURL, $swapLeftRightInURL );
+
+ $this->assertEquals( $expectedOutput, $flipped,
+ 'Test flipping, options: url-ltr-rtl=' . ( $swapLtrRtlInURL ? 'true' : 'false' )
+ . ' url-left-right=' . ( $swapLeftRightInURL ? 'true' : 'false' )
+ );
+ }
+
+ /**
+ * @dataProvider provideTransformBrokenCases
+ * @group Broken
+ */
+ public function testTransformBroken( $code, $expectedOutput ) {
+ $flipped = CSSJanus::transform( $code );
+
+ $this->assertEquals( $expectedOutput, $flipped, 'Test flipping' );
+ }
+
+ /**
+ * These transform cases are tested *in both directions*
+ * No need to declare a principle twice in both directions here.
+ */
+ public static function provideTransformCases() {
+ return array(
+ // Property keys
+ array(
+ '.foo { left: 0; }',
+ '.foo { right: 0; }'
+ ),
+ // Guard against partial keys
+ // (CSS currently doesn't have flippable properties
+ // that contain the direction as part of the key without
+ // dash separation)
+ array(
+ '.foo { alright: 0; }'
+ ),
+ array(
+ '.foo { balleft: 0; }'
+ ),
+
+ // Dashed property keys
+ array(
+ '.foo { padding-left: 0; }',
+ '.foo { padding-right: 0; }'
+ ),
+ array(
+ '.foo { margin-left: 0; }',
+ '.foo { margin-right: 0; }'
+ ),
+ array(
+ '.foo { border-left: 0; }',
+ '.foo { border-right: 0; }'
+ ),
+
+ // Double-dashed property keys
+ array(
+ '.foo { border-left-color: red; }',
+ '.foo { border-right-color: red; }'
+ ),
+ array(
+ // Includes unknown properties?
+ '.foo { x-left-y: 0; }',
+ '.foo { x-right-y: 0; }'
+ ),
+
+ // Multi-value properties
+ array(
+ '.foo { padding: 0; }'
+ ),
+ array(
+ '.foo { padding: 0 1px; }'
+ ),
+ array(
+ '.foo { padding: 0 1px 2px; }'
+ ),
+ array(
+ '.foo { padding: 0 1px 2px 3px; }',
+ '.foo { padding: 0 3px 2px 1px; }'
+ ),
+
+ // Shorthand / Four notation
+ array(
+ '.foo { padding: .25em 15px 0pt 0ex; }',
+ '.foo { padding: .25em 0ex 0pt 15px; }'
+ ),
+ array(
+ '.foo { margin: 1px -4px 3px 2px; }',
+ '.foo { margin: 1px 2px 3px -4px; }'
+ ),
+ array(
+ '.foo { padding: 0 15px .25em 0; }',
+ '.foo { padding: 0 0 .25em 15px; }'
+ ),
+ array(
+ '.foo { padding: 1px 4.1grad 3px 2%; }',
+ '.foo { padding: 1px 2% 3px 4.1grad; }'
+ ),
+ array(
+ '.foo { padding: 1px 2px 3px auto; }',
+ '.foo { padding: 1px auto 3px 2px; }'
+ ),
+ array(
+ '.foo { padding: 1px inherit 3px auto; }',
+ '.foo { padding: 1px auto 3px inherit; }'
+ ),
+ // border-radius assigns different meanings to the values
+ array(
+ '.foo { border-radius: .25em 15px 0pt 0ex; }',
+ '.foo { border-radius: 15px .25em 0ex 0pt; }'
+ ),
+ array(
+ '.foo { border-radius: 0px 0px 5px 5px; }',
+ ),
+ // Ensure the rule doesn't break other stuff
+ array(
+ '.foo { x-unknown: a b c d; }'
+ ),
+ array(
+ '.foo barpx 0 2% { opacity: 0; }'
+ ),
+ array(
+ '#settings td p strong'
+ ),
+ array(
+ // Color names
+ '.foo { border-color: red green blue white }',
+ '.foo { border-color: red white blue green }',
+ ),
+ array(
+ // Color name, hexdecimal, RGB & RGBA
+ '.foo { border-color: red #f00 rgb(255, 0, 0) rgba(255, 0, 0, 0.5) }',
+ '.foo { border-color: red rgba(255, 0, 0, 0.5) rgb(255, 0, 0) #f00 }',
+ ),
+ array(
+ // Color name, hexdecimal, HSL & HSLA
+ '.foo { border-color: red #f00 hsl(0, 100%, 50%) hsla(0, 100%, 50%, 0.5) }',
+ '.foo { border-color: red hsla(0, 100%, 50%, 0.5) hsl(0, 100%, 50%) #f00 }',
+ ),
+ array(
+ // Do not mangle 5 or more values
+ '.foo { -x-unknown: 1 2 3 4 5; }'
+ ),
+ array(
+ '.foo { -x-unknown: 1 2 3 4 5 6; }'
+ ),
+
+ // Shorthand / Three notation
+ array(
+ '.foo { margin: 1em 0 .25em; }'
+ ),
+ array(
+ '.foo { margin:-1.5em 0 -.75em; }'
+ ),
+
+ // Shorthand / Two notation
+ array(
+ '.foo { padding: 1px 2px; }'
+ ),
+
+ // Shorthand / One notation
+ array(
+ '.foo { padding: 1px; }'
+ ),
+
+ // text-shadow and box-shadow
+ array(
+ '.foo { box-shadow: -6px 3px 8px 5px rgba(0, 0, 0, 0.25); }',
+ '.foo { box-shadow: 6px 3px 8px 5px rgba(0, 0, 0, 0.25); }',
+ ),
+ array(
+ '.foo { box-shadow: inset -6px 3px 8px 5px rgba(0, 0, 0, 0.25); }',
+ '.foo { box-shadow: inset 6px 3px 8px 5px rgba(0, 0, 0, 0.25); }',
+ ),
+ array(
+ '.foo { text-shadow: orange 2px 0; }',
+ '.foo { text-shadow: orange -2px 0; }',
+ ),
+ array(
+ '.foo { text-shadow: 2px 0 orange; }',
+ '.foo { text-shadow: -2px 0 orange; }',
+ ),
+ array(
+ // Don't mangle zeroes
+ '.foo { text-shadow: orange 0 2px; }'
+ ),
+
+ // Direction
+ // Note: This differs from the Python implementation,
+ // see also CSSJanus::fixDirection for more info.
+ array(
+ '.foo { direction: ltr; }',
+ '.foo { direction: rtl; }'
+ ),
+ array(
+ '.foo { direction: rtl; }',
+ '.foo { direction: ltr; }'
+ ),
+ array(
+ 'input { direction: ltr; }',
+ 'input { direction: rtl; }'
+ ),
+ array(
+ 'input { direction: rtl; }',
+ 'input { direction: ltr; }'
+ ),
+ array(
+ 'body { direction: ltr; }',
+ 'body { direction: rtl; }'
+ ),
+ array(
+ '.foo, body, input { direction: ltr; }',
+ '.foo, body, input { direction: rtl; }'
+ ),
+ array(
+ 'body { padding: 10px; direction: ltr; }',
+ 'body { padding: 10px; direction: rtl; }'
+ ),
+ array(
+ 'body { direction: ltr } .myClass { direction: ltr }',
+ 'body { direction: rtl } .myClass { direction: rtl }'
+ ),
+
+ // Left/right values
+ array(
+ '.foo { float: left; }',
+ '.foo { float: right; }'
+ ),
+ array(
+ '.foo { text-align: left; }',
+ '.foo { text-align: right; }'
+ ),
+ array(
+ '.foo { -x-unknown: left; }',
+ '.foo { -x-unknown: right; }'
+ ),
+ // Guard against selectors that look flippable
+ array(
+ '.column-left { width: 0; }'
+ ),
+ array(
+ 'a.left { width: 0; }'
+ ),
+ array(
+ 'a.leftification { width: 0; }'
+ ),
+ array(
+ 'a.ltr { width: 0; }'
+ ),
+ array(
+ # <div class="a-ltr png">
+ '.a-ltr.png { width: 0; }'
+ ),
+ array(
+ # <foo-ltr attr="x">
+ 'foo-ltr[attr="x"] { width: 0; }'
+ ),
+ array(
+ 'div.left > span.right+span.left { width: 0; }'
+ ),
+ array(
+ '.thisclass .left .myclass { width: 0; }'
+ ),
+ array(
+ '.thisclass .left .myclass #myid { width: 0; }'
+ ),
+
+ // Cursor values (east/west)
+ array(
+ '.foo { cursor: e-resize; }',
+ '.foo { cursor: w-resize; }'
+ ),
+ array(
+ '.foo { cursor: se-resize; }',
+ '.foo { cursor: sw-resize; }'
+ ),
+ array(
+ '.foo { cursor: ne-resize; }',
+ '.foo { cursor: nw-resize; }'
+ ),
+
+ // Background
+ array(
+ '.foo { background-position: top left; }',
+ '.foo { background-position: top right; }'
+ ),
+ array(
+ '.foo { background: url(/foo/bar.png) top left; }',
+ '.foo { background: url(/foo/bar.png) top right; }'
+ ),
+ array(
+ '.foo { background: url(/foo/bar.png) top left no-repeat; }',
+ '.foo { background: url(/foo/bar.png) top right no-repeat; }'
+ ),
+ array(
+ '.foo { background: url(/foo/bar.png) no-repeat top left; }',
+ '.foo { background: url(/foo/bar.png) no-repeat top right; }'
+ ),
+ array(
+ '.foo { background: #fff url(/foo/bar.png) no-repeat top left; }',
+ '.foo { background: #fff url(/foo/bar.png) no-repeat top right; }'
+ ),
+ array(
+ '.foo { background-position: 100% 40%; }',
+ '.foo { background-position: 0% 40%; }'
+ ),
+ array(
+ '.foo { background-position: 23% 0; }',
+ '.foo { background-position: 77% 0; }'
+ ),
+ array(
+ '.foo { background-position: 23% auto; }',
+ '.foo { background-position: 77% auto; }'
+ ),
+ array(
+ '.foo { background-position-x: 23%; }',
+ '.foo { background-position-x: 77%; }'
+ ),
+ array(
+ '.foo { background-position-y: 23%; }',
+ '.foo { background-position-y: 23%; }'
+ ),
+ array(
+ '.foo { background:url(../foo.png) no-repeat 75% 50%; }',
+ '.foo { background:url(../foo.png) no-repeat 25% 50%; }'
+ ),
+ array(
+ '.foo { background: 10% 20% } .bar { background: 40% 30% }',
+ '.foo { background: 90% 20% } .bar { background: 60% 30% }'
+ ),
+
+ // Multiple rules
+ array(
+ 'body { direction: rtl; float: right; } .foo { direction: ltr; float: right; }',
+ 'body { direction: ltr; float: left; } .foo { direction: rtl; float: left; }',
+ ),
+
+ // Duplicate properties
+ array(
+ '.foo { float: left; float: right; float: left; }',
+ '.foo { float: right; float: left; float: right; }',
+ ),
+
+ // Preserve comments
+ array(
+ '/* left /* right */left: 10px',
+ '/* left /* right */right: 10px'
+ ),
+ array(
+ '/*left*//*left*/left: 10px',
+ '/*left*//*left*/right: 10px'
+ ),
+ array(
+ '/* Going right is cool */ .foo { width: 0 }',
+ ),
+ array(
+ "/* padding-right 1 2 3 4 */\n#test { width: 0}\n/*right*/"
+ ),
+ array(
+ "/** Two line comment\n * left\n \*/\n#test {width: 0}"
+ ),
+
+ // @noflip annotation
+ array(
+ // before selector (single)
+ '/* @noflip */ div { float: left; }'
+ ),
+ array(
+ // before selector (multiple)
+ '/* @noflip */ div, .notme { float: left; }'
+ ),
+ array(
+ // inside selector
+ 'div, /* @noflip */ .foo { float: left; }'
+ ),
+ array(
+ // after selector
+ 'div, .notme /* @noflip */ { float: left; }'
+ ),
+ array(
+ // before multiple rules
+ '/* @noflip */ div { float: left; } .foo { float: left; }',
+ '/* @noflip */ div { float: left; } .foo { float: right; }'
+ ),
+ array(
+ // support parentheses in selector
+ '/* @noflip */ .test:not(:first) { margin-right: -0.25em; margin-left: 0.25em; }',
+ '/* @noflip */ .test:not(:first) { margin-right: -0.25em; margin-left: 0.25em; }'
+ ),
+ array(
+ // after multiple rules
+ '.foo { float: left; } /* @noflip */ div { float: left; }',
+ '.foo { float: right; } /* @noflip */ div { float: left; }'
+ ),
+ array(
+ // before multiple properties
+ 'div { /* @noflip */ float: left; text-align: left; }',
+ 'div { /* @noflip */ float: left; text-align: right; }'
+ ),
+ array(
+ // after multiple properties
+ 'div { float: left; /* @noflip */ text-align: left; }',
+ 'div { float: right; /* @noflip */ text-align: left; }'
+ ),
+
+ // Guard against css3 stuff
+ array(
+ 'background-image: -moz-linear-gradient(#326cc1, #234e8c);'
+ ),
+ array(
+ 'background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, from(#666666), to(#ffffff));'
+ ),
+
+ // CSS syntax / white-space variations
+ // spaces, no spaces, tabs, new lines, omitting semi-colons
+ array(
+ ".foo { left: 0; }",
+ ".foo { right: 0; }"
+ ),
+ array(
+ ".foo{ left: 0; }",
+ ".foo{ right: 0; }"
+ ),
+ array(
+ ".foo{ left: 0 }",
+ ".foo{ right: 0 }"
+ ),
+ array(
+ ".foo{left:0 }",
+ ".foo{right:0 }"
+ ),
+ array(
+ ".foo{left:0}",
+ ".foo{right:0}"
+ ),
+ array(
+ ".foo { left : 0 ; }",
+ ".foo { right : 0 ; }"
+ ),
+ array(
+ ".foo\n { left : 0 ; }",
+ ".foo\n { right : 0 ; }"
+ ),
+ array(
+ ".foo\n { \nleft : 0 ; }",
+ ".foo\n { \nright : 0 ; }"
+ ),
+ array(
+ ".foo\n { \n left : 0 ; }",
+ ".foo\n { \n right : 0 ; }"
+ ),
+ array(
+ ".foo\n { \n left\n : 0; }",
+ ".foo\n { \n right\n : 0; }"
+ ),
+ array(
+ ".foo \n { \n left\n : 0; }",
+ ".foo \n { \n right\n : 0; }"
+ ),
+ array(
+ ".foo\n{\nleft\n:\n0;}",
+ ".foo\n{\nright\n:\n0;}"
+ ),
+ array(
+ ".foo\n.bar {\n\tleft: 0;\n}",
+ ".foo\n.bar {\n\tright: 0;\n}"
+ ),
+ array(
+ ".foo\t{\tleft\t:\t0;}",
+ ".foo\t{\tright\t:\t0;}"
+ ),
+
+ // Guard against partial keys
+ array(
+ '.foo { leftxx: 0; }',
+ '.foo { leftxx: 0; }'
+ ),
+ array(
+ '.foo { rightxx: 0; }',
+ '.foo { rightxx: 0; }'
+ ),
+ );
+ }
+
+ /**
+ * These cases are tested in one way only (format: actual, expected, msg).
+ * If both ways can be tested, either put both versions in here or move
+ * it to provideTransformCases().
+ */
+ public static function provideTransformAdvancedCases() {
+ $bgPairs = array(
+ # [ - _ . ] <-> [ left right ltr rtl ]
+ 'foo.jpg' => 'foo.jpg',
+ 'left.jpg' => 'right.jpg',
+ 'ltr.jpg' => 'rtl.jpg',
+
+ 'foo-left.png' => 'foo-right.png',
+ 'foo_left.png' => 'foo_right.png',
+ 'foo.left.png' => 'foo.right.png',
+
+ 'foo-ltr.png' => 'foo-rtl.png',
+ 'foo_ltr.png' => 'foo_rtl.png',
+ 'foo.ltr.png' => 'foo.rtl.png',
+
+ 'left-foo.png' => 'right-foo.png',
+ 'left_foo.png' => 'right_foo.png',
+ 'left.foo.png' => 'right.foo.png',
+
+ 'ltr-foo.png' => 'rtl-foo.png',
+ 'ltr_foo.png' => 'rtl_foo.png',
+ 'ltr.foo.png' => 'rtl.foo.png',
+
+ 'foo-ltr-left.gif' => 'foo-rtl-right.gif',
+ 'foo_ltr_left.gif' => 'foo_rtl_right.gif',
+ 'foo.ltr.left.gif' => 'foo.rtl.right.gif',
+ 'foo-ltr_left.gif' => 'foo-rtl_right.gif',
+ 'foo_ltr.left.gif' => 'foo_rtl.right.gif',
+ );
+ $provider = array();
+ foreach ( $bgPairs as $left => $right ) {
+ # By default '-rtl' and '-left' etc. are not touched,
+ # Only when the appropiate parameter is set.
+ $provider[] = array(
+ ".foo { background: url(images/$left); }",
+ ".foo { background: url(images/$left); }"
+ );
+ $provider[] = array(
+ ".foo { background: url(images/$right); }",
+ ".foo { background: url(images/$right); }"
+ );
+ $provider[] = array(
+ ".foo { background: url(images/$left); }",
+ ".foo { background: url(images/$right); }",
+ array(
+ 'swapLtrRtlInURL' => true,
+ 'swapLeftRightInURL' => true,
+ )
+ );
+ $provider[] = array(
+ ".foo { background: url(images/$right); }",
+ ".foo { background: url(images/$left); }",
+ array(
+ 'swapLtrRtlInURL' => true,
+ 'swapLeftRightInURL' => true,
+ )
+ );
+ }
+
+ return $provider;
+ }
+
+ /**
+ * Cases that are currently failing, but
+ * should be looked at in the future as enhancements and/or bug fix
+ */
+ public static function provideTransformBrokenCases() {
+ return array(
+ // Guard against selectors that look flippable
+ array(
+ # <foo-left-x attr="x">
+ 'foo-left-x[attr="x"] { width: 0; }',
+ 'foo-left-x[attr="x"] { width: 0; }'
+ ),
+ array(
+ # <div class="foo" data-left="x">
+ '.foo[data-left="x"] { width: 0; }',
+ '.foo[data-left="x"] { width: 0; }'
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php
new file mode 100644
index 00000000..951dd7b9
--- /dev/null
+++ b/tests/phpunit/includes/libs/CSSMinTest.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * This file test the CSSMin library shipped with Mediawiki.
+ *
+ * @author Timo Tijhof
+ */
+
+class CSSMinTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $server = 'http://doc.example.org';
+
+ $this->setMwGlobals( array(
+ 'wgServer' => $server,
+ 'wgCanonicalServer' => $server,
+ ) );
+ }
+
+ /**
+ * @dataProvider provideMinifyCases
+ */
+ public function testMinify( $code, $expectedOutput ) {
+ $minified = CSSMin::minify( $code );
+
+ $this->assertEquals( $expectedOutput, $minified, 'Minified output should be in the form expected.' );
+ }
+
+ public static function provideMinifyCases() {
+ return array(
+ // Whitespace
+ array( "\r\t\f \v\n\r", "" ),
+ array( "foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+
+ // Loose comments
+ array( "/* foo */", "" ),
+ array( "/*******\n foo\n *******/", "" ),
+ array( "/*!\n foo\n */", "" ),
+
+ // Inline comments in various different places
+ array( "/* comment */foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo/* comment */, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo,/* comment */ bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar/* comment */ {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar {\n\t/* comment */prop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar {\n\tprop: /* comment */value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar {\n\tprop: value /* comment */;\n}", "foo,bar{prop:value }" ),
+ array( "foo, bar {\n\tprop: value; /* comment */\n}", "foo,bar{prop:value; }" ),
+
+ // Keep track of things that aren't as minified as much as they
+ // could be (bug 35493)
+ array( 'foo { prop: value ;}', 'foo{prop:value }' ),
+ array( 'foo { prop : value; }', 'foo{prop :value}' ),
+ array( 'foo { prop: value ; }', 'foo{prop:value }' ),
+ array( 'foo { font-family: "foo" , "bar"; }', 'foo{font-family:"foo" ,"bar"}' ),
+ array( "foo { src:\n\turl('foo') ,\n\turl('bar') ; }", "foo{src:url('foo') ,url('bar') }" ),
+
+ // Interesting cases with string values
+ // - Double quotes, single quotes
+ array( 'foo { content: ""; }', 'foo{content:""}' ),
+ array( "foo { content: ''; }", "foo{content:''}" ),
+ array( 'foo { content: "\'"; }', 'foo{content:"\'"}' ),
+ array( "foo { content: '\"'; }", "foo{content:'\"'}" ),
+ // - Whitespace in string values
+ array( 'foo { content: " "; }', 'foo{content:" "}' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRemapCases
+ */
+ public function testRemap( $message, $params, $expectedOutput ) {
+ $remapped = call_user_func_array( 'CSSMin::remap', $params );
+
+ $messageAdd = " Case: $message";
+ $this->assertEquals( $expectedOutput, $remapped, 'CSSMin::remap should return the expected url form.' . $messageAdd );
+ }
+
+ public static function provideRemapCases() {
+ // Parameter signature:
+ // CSSMin::remap( $code, $local, $remote, $embedData = true )
+ return array(
+ array(
+ 'Simple case',
+ array( 'foo { prop: url(bar.png); }', false, 'http://example.org', false ),
+ 'foo { prop: url(http://example.org/bar.png); }',
+ ),
+ array(
+ 'Without trailing slash',
+ array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux', false ),
+ 'foo { prop: url(http://example.org/quux/../bar.png); }',
+ ),
+ array(
+ 'With trailing slash on remote (bug 27052)',
+ array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux/', false ),
+ 'foo { prop: url(http://example.org/quux/../bar.png); }',
+ ),
+ array(
+ 'Guard against stripping double slashes from query',
+ array( 'foo { prop: url(bar.png?corge=//grault); }', false, 'http://example.org/quux/', false ),
+ 'foo { prop: url(http://example.org/quux/bar.png?corge=//grault); }',
+ ),
+ array(
+ 'Expand absolute paths',
+ array( 'foo { prop: url(/w/skin/images/bar.png); }', false, 'http://example.org/quux', false ),
+ 'foo { prop: url(http://doc.example.org/w/skin/images/bar.png); }',
+ ),
+ );
+ }
+
+ /**
+ * Seperated because they are currently broken (bug 35492)
+ *
+ * @group Broken
+ * @dataProvider provideStringCases
+ */
+ public function testMinifyWithCSSStringValues( $code, $expectedOutput ) {
+ $this->testMinifyOutput( $code, $expectedOutput );
+ }
+
+ public static function provideStringCases() {
+ return array(
+ // String values should be respected
+ // - More than one space in a string value
+ array( 'foo { content: " "; }', 'foo{content:" "}' ),
+ // - Using a tab in a string value (turns into a space)
+ array( "foo { content: '\t'; }", "foo{content:'\t'}" ),
+ // - Using css-like syntax in string values
+ array( 'foo::after { content: "{;}"; position: absolute; }', 'foo::after{content:"{;}";position:absolute}' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/includes/libs/GenericArrayObjectTest.php
new file mode 100644
index 00000000..7436c43c
--- /dev/null
+++ b/tests/phpunit/includes/libs/GenericArrayObjectTest.php
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * Tests for the GenericArrayObject and deriving classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.20
+ *
+ * @ingroup Test
+ * @group GenericArrayObject
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class GenericArrayObjectTest extends MediaWikiTestCase {
+
+ /**
+ * Returns objects that can serve as elements in the concrete GenericArrayObject deriving class being tested.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ abstract public function elementInstancesProvider();
+
+ /**
+ * Returns the name of the concrete class being tested.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ abstract public function getInstanceClass();
+
+ /**
+ * Provides instances of the concrete class being tested.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function instanceProvider() {
+ $instances = array();
+
+ foreach ( $this->elementInstancesProvider() as $elementInstances ) {
+ $instances[] = $this->getNew( $elementInstances[0] );
+ }
+
+ return $this->arrayWrap( $instances );
+ }
+
+ /**
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @return GenericArrayObject
+ */
+ protected function getNew( array $elements = array() ) {
+ $class = $this->getInstanceClass();
+
+ return new $class( $elements );
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ */
+ public function testConstructor( array $elements ) {
+ $arrayObject = $this->getNew( $elements );
+
+ $this->assertEquals( count( $elements ), $arrayObject->count() );
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ */
+ public function testIsEmpty( array $elements ) {
+ $arrayObject = $this->getNew( $elements );
+
+ $this->assertEquals( $elements === array(), $arrayObject->isEmpty() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ *
+ * @since 1.20
+ *
+ * @param GenericArrayObject $list
+ */
+ public function testUnset( GenericArrayObject $list ) {
+ if ( $list->isEmpty() ) {
+ $this->assertTrue( true ); // We cannot test unset if there are no elements
+ } else {
+ $offset = $list->getIterator()->key();
+ $count = $list->count();
+ $list->offsetUnset( $offset );
+ $this->assertEquals( $count - 1, $list->count() );
+ }
+
+ if ( !$list->isEmpty() ) {
+ $offset = $list->getIterator()->key();
+ $count = $list->count();
+ unset( $list[$offset] );
+ $this->assertEquals( $count - 1, $list->count() );
+ }
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ */
+ public function testAppend( array $elements ) {
+ $list = $this->getNew();
+
+ $listSize = count( $elements );
+
+ foreach ( $elements as $element ) {
+ $list->append( $element );
+ }
+
+ $this->assertEquals( $listSize, $list->count() );
+
+ $list = $this->getNew();
+
+ foreach ( $elements as $element ) {
+ $list[] = $element;
+ }
+
+ $this->assertEquals( $listSize, $list->count() );
+
+ $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
+ $list->append( $element );
+ } );
+ }
+
+ /**
+ * @since 1.20
+ *
+ * @param callback $function
+ */
+ protected function checkTypeChecks( $function ) {
+ $excption = null;
+ $list = $this->getNew();
+
+ $elementClass = $list->getObjectType();
+
+ foreach ( array( 42, 'foo', array(), new stdClass(), 4.2 ) as $element ) {
+ $validValid = $element instanceof $elementClass;
+
+ try {
+ call_user_func( $function, $list, $element );
+ $valid = true;
+ } catch ( InvalidArgumentException $exception ) {
+ $valid = false;
+ }
+
+ $this->assertEquals(
+ $validValid,
+ $valid,
+ 'Object of invalid type got successfully added to a GenericArrayObject'
+ );
+ }
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ */
+ public function testOffsetSet( array $elements ) {
+ if ( $elements === array() ) {
+ $this->assertTrue( true );
+
+ return;
+ }
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list->offsetSet( 42, $element );
+ $this->assertEquals( $element, $list->offsetGet( 42 ) );
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list['oHai'] = $element;
+ $this->assertEquals( $element, $list['oHai'] );
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list->offsetSet( 9001, $element );
+ $this->assertEquals( $element, $list[9001] );
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list->offsetSet( null, $element );
+ $this->assertEquals( $element, $list[0] );
+
+ $list = $this->getNew();
+ $offset = 0;
+
+ foreach ( $elements as $element ) {
+ $list->offsetSet( null, $element );
+ $this->assertEquals( $element, $list[$offset++] );
+ }
+
+ $this->assertEquals( count( $elements ), $list->count() );
+
+ $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
+ $list->offsetSet( mt_rand(), $element );
+ } );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ *
+ * @since 1.21
+ *
+ * @param GenericArrayObject $list
+ */
+ public function testSerialization( GenericArrayObject $list ) {
+ $serialization = serialize( $list );
+ $copy = unserialize( $serialization );
+
+ $this->assertEquals( $serialization, serialize( $copy ) );
+ $this->assertEquals( count( $list ), count( $copy ) );
+
+ $list = $list->getArrayCopy();
+ $copy = $copy->getArrayCopy();
+
+ $this->assertArrayEquals( $list, $copy, true, true );
+ }
+}
diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
new file mode 100644
index 00000000..66fe915a
--- /dev/null
+++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * Tests for IEUrlExtension::findIE6Extension
+ */
+class IEUrlExtensionTest extends MediaWikiTestCase {
+ public function testSimple() {
+ $this->assertEquals(
+ 'y',
+ IEUrlExtension::findIE6Extension( 'x.y' ),
+ 'Simple extension'
+ );
+ }
+
+ public function testSimpleNoExt() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'x' ),
+ 'No extension'
+ );
+ }
+
+ public function testEmpty() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '' ),
+ 'Empty string'
+ );
+ }
+
+ public function testQuestionMark() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '?' ),
+ 'Question mark only'
+ );
+ }
+
+ public function testExtQuestionMark() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '.x?' ),
+ 'Extension then question mark'
+ );
+ }
+
+ public function testQuestionMarkExt() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '?.x' ),
+ 'Question mark then extension'
+ );
+ }
+
+ public function testInvalidChar() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '.x*' ),
+ 'Extension with invalid character'
+ );
+ }
+
+ public function testInvalidCharThenExtension() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '*.x' ),
+ 'Invalid character followed by an extension'
+ );
+ }
+
+ public function testMultipleQuestionMarks() {
+ $this->assertEquals(
+ 'c',
+ IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ),
+ 'Multiple question marks'
+ );
+ }
+
+ public function testExeException() {
+ $this->assertEquals(
+ 'd',
+ IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ),
+ '.exe exception'
+ );
+ }
+
+ public function testExeException2() {
+ $this->assertEquals(
+ 'exe',
+ IEUrlExtension::findIE6Extension( 'a?b?.exe' ),
+ '.exe exception 2'
+ );
+ }
+
+ public function testHash() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'a#b.c' ),
+ 'Hash character preceding extension'
+ );
+ }
+
+ public function testHash2() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'a?#b.c' ),
+ 'Hash character preceding extension 2'
+ );
+ }
+
+ public function testDotAtEnd() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '.' ),
+ 'Dot at end of string'
+ );
+ }
+
+ public function testTwoDots() {
+ $this->assertEquals(
+ 'z',
+ IEUrlExtension::findIE6Extension( 'x.y.z' ),
+ 'Two dots'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
new file mode 100644
index 00000000..ab72e361
--- /dev/null
+++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
@@ -0,0 +1,170 @@
+<?php
+
+class JavaScriptMinifierTest extends MediaWikiTestCase {
+
+ public static function provideCases() {
+ return array(
+
+ // Basic whitespace and comments that should be stripped entirely
+ array( "\r\t\f \v\n\r", "" ),
+ array( "/* Foo *\n*bar\n*/", "" ),
+
+ /**
+ * Slashes used inside block comments (bug 26931).
+ * At some point there was a bug that caused this comment to be ended at '* /',
+ * causing /M... to be left as the beginning of a regex.
+ */
+ array( "/**\n * Foo\n * {\n * 'bar' : {\n * //Multiple rules with configurable operators\n * 'baz' : false\n * }\n */", "" ),
+
+ /**
+ * ' Foo \' bar \
+ * baz \' quox ' .
+ */
+ array( "' Foo \\' bar \\\n baz \\' quox ' .length", "' Foo \\' bar \\\n baz \\' quox '.length" ),
+ array( "\" Foo \\\" bar \\\n baz \\\" quox \" .length", "\" Foo \\\" bar \\\n baz \\\" quox \".length" ),
+ array( "// Foo b/ar baz", "" ),
+ array( "/ Foo \\/ bar [ / \\] / ] baz / .length", "/ Foo \\/ bar [ / \\] / ] baz /.length" ),
+
+ // HTML comments
+ array( "<!-- Foo bar", "" ),
+ array( "<!-- Foo --> bar", "" ),
+ array( "--> Foo", "" ),
+ array( "x --> y", "x-->y" ),
+
+ // Semicolon insertion
+ array( "(function(){return\nx;})", "(function(){return\nx;})" ),
+ array( "throw\nx;", "throw\nx;" ),
+ array( "while(p){continue\nx;}", "while(p){continue\nx;}" ),
+ array( "while(p){break\nx;}", "while(p){break\nx;}" ),
+ array( "var\nx;", "var x;" ),
+ array( "x\ny;", "x\ny;" ),
+ array( "x\n++y;", "x\n++y;" ),
+ array( "x\n!y;", "x\n!y;" ),
+ array( "x\n{y}", "x\n{y}" ),
+ array( "x\n+y;", "x+y;" ),
+ array( "x\n(y);", "x(y);" ),
+ array( "5.\nx;", "5.\nx;" ),
+ array( "0xFF.\nx;", "0xFF.x;" ),
+ array( "5.3.\nx;", "5.3.x;" ),
+
+ // Semicolon insertion between an expression having an inline
+ // comment after it, and a statement on the next line (bug 27046).
+ array( "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}", "var a=this\nfor(b=0;c<d;b++){}" ),
+
+ // Token separation
+ array( "x in y", "x in y" ),
+ array( "/x/g in y", "/x/g in y" ),
+ array( "x in 30", "x in 30" ),
+ array( "x + ++ y", "x+ ++y" ),
+ array( "x ++ + y", "x++ +y" ),
+ array( "x / /y/.exec(z)", "x/ /y/.exec(z)" ),
+
+ // State machine
+ array( "/ x/g", "/ x/g" ),
+ array( "(function(){return/ x/g})", "(function(){return/ x/g})" ),
+ array( "+/ x/g", "+/ x/g" ),
+ array( "++/ x/g", "++/ x/g" ),
+ array( "x/ x/g", "x/x/g" ),
+ array( "(/ x/g)", "(/ x/g)" ),
+ array( "if(/ x/g);", "if(/ x/g);" ),
+ array( "(x/ x/g)", "(x/x/g)" ),
+ array( "([/ x/g])", "([/ x/g])" ),
+ array( "+x/ x/g", "+x/x/g" ),
+ array( "{}/ x/g", "{}/ x/g" ),
+ array( "+{}/ x/g", "+{}/x/g" ),
+ array( "(x)/ x/g", "(x)/x/g" ),
+ array( "if(x)/ x/g", "if(x)/ x/g" ),
+ array( "for(x;x;{}/ x/g);", "for(x;x;{}/x/g);" ),
+ array( "x;x;{}/ x/g", "x;x;{}/ x/g" ),
+ array( "x:{}/ x/g", "x:{}/ x/g" ),
+ array( "switch(x){case y?z:{}/ x/g:{}/ x/g;}", "switch(x){case y?z:{}/x/g:{}/ x/g;}" ),
+ array( "function x(){}/ x/g", "function x(){}/ x/g" ),
+ array( "+function x(){}/ x/g", "+function x(){}/x/g" ),
+
+ // Multiline quoted string
+ array( "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ),
+
+ // Multiline quoted string followed by string with spaces
+ array( "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n", "var foo=\"\\\nblah\\\n\";var baz=\" foo \";" ),
+
+ // URL in quoted string ( // is not a comment)
+ array( "aNode.setAttribute('href','http://foo.bar.org/baz');", "aNode.setAttribute('href','http://foo.bar.org/baz');" ),
+
+ // URL in quoted string after multiline quoted string
+ array( "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');", "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');" ),
+
+ // Division vs. regex nastiness
+ array( "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );", "alert((10+10)/'/'.charCodeAt(0)+'//');" ),
+ array( "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ),
+
+ // newline insertion after 1000 chars: break after the "++", not before
+ array( str_repeat( ';', 996 ) . "if(x++);", str_repeat( ';', 996 ) . "if(x++\n);" ),
+
+ // Unicode letter characters should pass through ok in identifiers (bug 31187)
+ array( "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}' ),
+
+ // Per spec unicode char escape values should work in identifiers,
+ // as long as it's a valid char. In future it might get normalized.
+ array( "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}' ),
+
+ // Some structures that might look invalid at first sight
+ array( "var a = 5.;", "var a=5.;" ),
+ array( "5.0.toString();", "5.0.toString();" ),
+ array( "5..toString();", "5..toString();" ),
+ array( "5...toString();", false ),
+ array( "5.\n.toString();", '5..toString();' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCases
+ */
+ public function testJavaScriptMinifierOutput( $code, $expectedOutput ) {
+ $minified = JavaScriptMinifier::minify( $code );
+
+ // JSMin+'s parser will throw an exception if output is not valid JS.
+ // suppression of warnings needed for stupid crap
+ wfSuppressWarnings();
+ $parser = new JSParser();
+ wfRestoreWarnings();
+ $parser->parse( $minified, 'minify-test.js', 1 );
+
+ $this->assertEquals( $expectedOutput, $minified, "Minified output should be in the form expected." );
+ }
+
+ public static function provideBug32548() {
+ return array(
+ array(
+ // This one gets interpreted all together by the prior code;
+ // no break at the 'E' happens.
+ '1.23456789E55',
+ ),
+ array(
+ // This one breaks under the bad code; splits between 'E' and '+'
+ '1.23456789E+5',
+ ),
+ array(
+ // This one breaks under the bad code; splits between 'E' and '-'
+ '1.23456789E-5',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideBug32548
+ */
+ public function testBug32548Exponent( $num ) {
+ // Long line breaking was being incorrectly done between the base and
+ // exponent part of a number, causing a syntax error. The line should
+ // instead break at the start of the number.
+ $prefix = 'var longVarName' . str_repeat( '_', 973 ) . '=';
+ $suffix = ',shortVarName=0;';
+
+ $input = $prefix . $num . $suffix;
+ $expected = $prefix . "\n" . $num . $suffix;
+
+ $minified = JavaScriptMinifier::minify( $input );
+
+ $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent" );
+ }
+}
diff --git a/tests/phpunit/includes/logging/LogFormatterTest.php b/tests/phpunit/includes/logging/LogFormatterTest.php
new file mode 100644
index 00000000..e8ccf433
--- /dev/null
+++ b/tests/phpunit/includes/logging/LogFormatterTest.php
@@ -0,0 +1,207 @@
+<?php
+/**
+ * @group Database
+ */
+class LogFormatterTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ protected function setUp() {
+ parent::setUp();
+
+ global $wgLang;
+
+ $this->setMwGlobals( array(
+ 'wgLogTypes' => array( 'phpunit' ),
+ 'wgLogActionsHandlers' => array( 'phpunit/test' => 'LogFormatter',
+ 'phpunit/param' => 'LogFormatter' ),
+ 'wgUser' => User::newFromName( 'Testuser' ),
+ 'wgExtensionMessagesFiles' => array( 'LogTests' => __DIR__ . '/LogTests.i18n.php' ),
+ ) );
+
+ $wgLang->getLocalisationCache()->recache( $wgLang->getCode() );
+
+ $this->user = User::newFromName( 'Testuser' );
+ $this->title = Title::newMainPage();
+
+ $this->context = new RequestContext();
+ $this->context->setUser( $this->user );
+ $this->context->setTitle( $this->title );
+ $this->context->setLanguage( $wgLang );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ global $wgLang;
+ $wgLang->getLocalisationCache()->recache( $wgLang->getCode() );
+ }
+
+ public function newLogEntry( $action, $params ) {
+ $logEntry = new ManualLogEntry( 'phpunit', $action );
+ $logEntry->setPerformer( $this->user );
+ $logEntry->setTarget( $this->title );
+ $logEntry->setComment( 'A very good reason' );
+
+ $logEntry->setParameters( $params );
+
+ return $logEntry;
+ }
+
+ public function testNormalLogParams() {
+ $entry = $this->newLogEntry( 'test', array() );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $formatter->setShowUserToolLinks( false );
+ $paramsWithoutTools = $formatter->getMessageParametersForTesting();
+ unset( $formatter->parsedParameters );
+
+ $formatter->setShowUserToolLinks( true );
+ $paramsWithTools = $formatter->getMessageParametersForTesting();
+
+ $userLink = Linker::userLink(
+ $this->user->getId(),
+ $this->user->getName()
+ );
+
+ $userTools = Linker::userToolLinksRedContribs(
+ $this->user->getId(),
+ $this->user->getName(),
+ $this->user->getEditCount()
+ );
+
+ $titleLink = Linker::link( $this->title, null, array(), array() );
+
+ // $paramsWithoutTools and $paramsWithTools should be only different
+ // in index 0
+ $this->assertEquals( $paramsWithoutTools[1], $paramsWithTools[1] );
+ $this->assertEquals( $paramsWithoutTools[2], $paramsWithTools[2] );
+
+ $this->assertEquals( $userLink, $paramsWithoutTools[0]['raw'] );
+ $this->assertEquals( $userLink . $userTools, $paramsWithTools[0]['raw'] );
+
+ $this->assertEquals( $this->user->getName(), $paramsWithoutTools[1] );
+
+ $this->assertEquals( $titleLink, $paramsWithoutTools[2]['raw'] );
+ }
+
+ public function testLogParamsTypeRaw() {
+ $params = array( '4:raw:raw' => Linker::link( $this->title, null, array(), array() ) );
+ $expected = Linker::link( $this->title, null, array(), array() );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogParamsTypeMsg() {
+ $params = array( '4:msg:msg' => 'log-description-phpunit' );
+ $expected = wfMessage( 'log-description-phpunit' )->text();
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogParamsTypeMsgContent() {
+ $params = array( '4:msg-content:msgContent' => 'log-description-phpunit' );
+ $expected = wfMessage( 'log-description-phpunit' )->inContentLanguage()->text();
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogParamsTypeNumber() {
+ global $wgLang;
+
+ $params = array( '4:number:number' => 123456789 );
+ $expected = $wgLang->formatNum( 123456789 );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogParamsTypeUserLink() {
+ $params = array( '4:user-link:userLink' => $this->user->getName() );
+ $expected = Linker::userLink(
+ $this->user->getId(),
+ $this->user->getName()
+ );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogParamsTypeTitleLink() {
+ $params = array( '4:title-link:titleLink' => $this->title->getText() );
+ $expected = Linker::link( $this->title, null, array(), array() );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogParamsTypePlain() {
+ $params = array( '4:plain:plain' => 'Some plain text' );
+ $expected = 'Some plain text';
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ public function testLogComment() {
+ $entry = $this->newLogEntry( 'test', array() );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $comment = ltrim( Linker::commentBlock( $entry->getComment() ) );
+
+ $this->assertEquals( $comment, $formatter->getComment() );
+ }
+}
diff --git a/tests/phpunit/includes/logging/LogTests.i18n.php b/tests/phpunit/includes/logging/LogTests.i18n.php
new file mode 100644
index 00000000..78787ba1
--- /dev/null
+++ b/tests/phpunit/includes/logging/LogTests.i18n.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Internationalisation file for log tests.
+ *
+ * @file
+ */
+
+$messages = array();
+
+$messages['en'] = array(
+ 'log-name-phpunit' => 'PHPUnit-log',
+ 'log-description-phpunit' => 'Log for PHPUnit-tests',
+ 'logentry-phpunit-test' => '$1 {{GENDER:$2|tests}} with page $3',
+ 'logentry-phpunit-param' => '$4',
+);
diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
new file mode 100644
index 00000000..a0e63a8a
--- /dev/null
+++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
@@ -0,0 +1,167 @@
+<?php
+class BitmapMetadataHandlerTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgShowEXIF', false );
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * Test if having conflicting metadata values from different
+ * types of metadata, that the right one takes precedence.
+ *
+ * Basically the file has IPTC and XMP metadata, the
+ * IPTC should override the XMP, except for the multilingual
+ * translation (to en) where XMP should win.
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testMultilingualCascade() {
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+ if ( !extension_loaded( 'xml' ) ) {
+ $this->markTestSkipped( "This test needs the xml extension." );
+ }
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ '/Xmp-exif-multilingual_test.jpg' );
+
+ $expected = array(
+ 'x-default' => 'right(iptc)',
+ 'en' => 'right translation',
+ '_type' => 'lang'
+ );
+
+ $this->assertArrayHasKey( 'ImageDescription', $meta,
+ 'Did not extract any ImageDescription info?!' );
+
+ $this->assertEquals( $expected, $meta['ImageDescription'] );
+ }
+
+ /**
+ * Test for jpeg comments are being handled by
+ * BitmapMetadataHandler correctly.
+ *
+ * There's more extensive tests of comment extraction in
+ * JpegMetadataExtractorTests.php
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testJpegComment() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'jpeg-comment-utf.jpg' );
+
+ $this->assertEquals( 'UTF-8 JPEG Comment — ¼',
+ $meta['JPEGFileComment'][0] );
+ }
+
+ /**
+ * Make sure a bad iptc block doesn't stop the other metadata
+ * from being extracted.
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testBadIPTC() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'iptc-invalid-psir.jpg' );
+ $this->assertEquals( 'Created with GIMP', $meta['JPEGFileComment'][0] );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testIPTCDates() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'iptc-timetest.jpg' );
+
+ $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] );
+ $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] );
+ }
+
+ /**
+ * File has an invalid time (+ one valid but really weird time)
+ * that shouldn't be included
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testIPTCDatesInvalid() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'iptc-timetest-invalid.jpg' );
+
+ $this->assertEquals( '1845:03:02 00:01:02', $meta['DateTimeOriginal'] );
+ $this->assertFalse( isset( $meta['DateTimeDigitized'] ) );
+ }
+
+ /**
+ * XMP data should take priority over iptc data
+ * when hash has been updated, but not when
+ * the hash is wrong.
+ * @covers BitmapMetadataHandler::addMetadata
+ * @covers BitmapMetadataHandler::getMetadataArray
+ */
+ public function testMerging() {
+ $merger = new BitmapMetadataHandler();
+ $merger->addMetadata( array( 'foo' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'bar' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'baz' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'fred' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'foo' => 'iptc (hash)' ), 'iptc-good-hash' );
+ $merger->addMetadata( array( 'bar' => 'iptc (bad hash)' ), 'iptc-bad-hash' );
+ $merger->addMetadata( array( 'baz' => 'iptc (bad hash)' ), 'iptc-bad-hash' );
+ $merger->addMetadata( array( 'fred' => 'iptc (no hash)' ), 'iptc-no-hash' );
+ $merger->addMetadata( array( 'baz' => 'exif' ), 'exif' );
+
+ $actual = $merger->getMetadataArray();
+ $expected = array(
+ 'foo' => 'xmp',
+ 'bar' => 'iptc (bad hash)',
+ 'baz' => 'exif',
+ 'fred' => 'xmp',
+ );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::png
+ */
+ public function testPNGXMP() {
+ if ( !extension_loaded( 'xml' ) ) {
+ $this->markTestSkipped( "This test needs the xml extension." );
+ }
+ $handler = new BitmapMetadataHandler();
+ $result = $handler->png( $this->filePath . 'xmp.png' );
+ $expected = array(
+ 'frameCount' => 0,
+ 'loopCount' => 1,
+ 'duration' => 0,
+ 'bitDepth' => 1,
+ 'colorType' => 'index-coloured',
+ 'metadata' => array(
+ 'SerialNumber' => '123456789',
+ '_MW_PNG_VERSION' => 1,
+ ),
+ );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::png
+ */
+ public function testPNGNative() {
+ $handler = new BitmapMetadataHandler();
+ $result = $handler->png( $this->filePath . 'Png-native-test.png' );
+ $expected = 'http://example.com/url';
+ $this->assertEquals( $expected, $result['metadata']['Identifier']['x-default'] );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::getTiffByteOrder
+ */
+ public function testTiffByteOrder() {
+ $handler = new BitmapMetadataHandler();
+ $res = $handler->getTiffByteOrder( $this->filePath . 'test.tiff' );
+ $this->assertEquals( 'LE', $res );
+ }
+}
diff --git a/tests/phpunit/includes/media/BitmapScalingTest.php b/tests/phpunit/includes/media/BitmapScalingTest.php
new file mode 100644
index 00000000..9395b660
--- /dev/null
+++ b/tests/phpunit/includes/media/BitmapScalingTest.php
@@ -0,0 +1,137 @@
+<?php
+
+class BitmapScalingTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgMaxImageArea' => 1.25e7, // 3500x3500
+ 'wgCustomConvertCommand' => 'dummy', // Set so that we don't get client side rendering
+ ) );
+ }
+
+ /**
+ * @dataProvider provideNormaliseParams
+ * @covers BitmapHandler::normaliseParams
+ */
+ public function testNormaliseParams( $fileDimensions, $expectedParams, $params, $msg ) {
+ $file = new FakeDimensionFile( $fileDimensions );
+ $handler = new BitmapHandler;
+ $valid = $handler->normaliseParams( $file, $params );
+ $this->assertTrue( $valid );
+ $this->assertEquals( $expectedParams, $params, $msg );
+ }
+
+ public static function provideNormaliseParams() {
+ return array(
+ /* Regular resize operations */
+ array(
+ array( 1024, 768 ),
+ array(
+ 'width' => 512, 'height' => 384,
+ 'physicalWidth' => 512, 'physicalHeight' => 384,
+ 'page' => 1,
+ ),
+ array( 'width' => 512 ),
+ 'Resizing with width set',
+ ),
+ array(
+ array( 1024, 768 ),
+ array(
+ 'width' => 512, 'height' => 384,
+ 'physicalWidth' => 512, 'physicalHeight' => 384,
+ 'page' => 1,
+ ),
+ array( 'width' => 512, 'height' => 768 ),
+ 'Resizing with height set too high',
+ ),
+ array(
+ array( 1024, 768 ),
+ array(
+ 'width' => 512, 'height' => 384,
+ 'physicalWidth' => 512, 'physicalHeight' => 384,
+ 'page' => 1,
+ ),
+ array( 'width' => 1024, 'height' => 384 ),
+ 'Resizing with height set',
+ ),
+
+ /* Very tall images */
+ array(
+ array( 1000, 100 ),
+ array(
+ 'width' => 5, 'height' => 1,
+ 'physicalWidth' => 5, 'physicalHeight' => 1,
+ 'page' => 1,
+ ),
+ array( 'width' => 5 ),
+ 'Very wide image',
+ ),
+
+ array(
+ array( 100, 1000 ),
+ array(
+ 'width' => 1, 'height' => 10,
+ 'physicalWidth' => 1, 'physicalHeight' => 10,
+ 'page' => 1,
+ ),
+ array( 'width' => 1 ),
+ 'Very high image',
+ ),
+ array(
+ array( 100, 1000 ),
+ array(
+ 'width' => 1, 'height' => 5,
+ 'physicalWidth' => 1, 'physicalHeight' => 10,
+ 'page' => 1,
+ ),
+ array( 'width' => 10, 'height' => 5 ),
+ 'Very high image with height set',
+ ),
+ /* Max image area */
+ array(
+ array( 4000, 4000 ),
+ array(
+ 'width' => 5000, 'height' => 5000,
+ 'physicalWidth' => 4000, 'physicalHeight' => 4000,
+ 'page' => 1,
+ ),
+ array( 'width' => 5000 ),
+ 'Bigger than max image size but doesn\'t need scaling',
+ ),
+ );
+ }
+
+ /**
+ * @covers BitmapHandler::doTransform
+ */
+ public function testTooBigImage() {
+ $file = new FakeDimensionFile( array( 4000, 4000 ) );
+ $handler = new BitmapHandler;
+ $params = array( 'width' => '3700' ); // Still bigger than max size.
+ $this->assertEquals( 'TransformParameterError',
+ get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) );
+ }
+
+ /**
+ * @covers BitmapHandler::doTransform
+ */
+ public function testTooBigMustRenderImage() {
+ $file = new FakeDimensionFile( array( 4000, 4000 ) );
+ $file->mustRender = true;
+ $handler = new BitmapHandler;
+ $params = array( 'width' => '5000' ); // Still bigger than max size.
+ $this->assertEquals( 'TransformParameterError',
+ get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) );
+ }
+
+ /**
+ * @covers BitmapHandler::getImageArea
+ */
+ public function testImageArea() {
+ $file = new FakeDimensionFile( array( 7, 9 ) );
+ $handler = new BitmapHandler;
+ $this->assertEquals( 63, $handler->getImageArea( $file ) );
+ }
+}
diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php
new file mode 100644
index 00000000..a2e0eb62
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifBitmapTest.php
@@ -0,0 +1,139 @@
+<?php
+
+class ExifBitmapTest extends MediaWikiTestCase {
+
+ /**
+ * @var ExifBitmapHandler
+ */
+ protected $handler;
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $this->handler = new ExifBitmapHandler;
+
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsOldBroken() {
+ $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::OLD_BROKEN_FILE );
+ $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsBrokenFile() {
+ $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::BROKEN_FILE );
+ $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsInvalid() {
+ $res = $this->handler->isMetadataValid( null, 'Something Invalid Here.' );
+ $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testGoodMetadata() {
+ $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}';
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsOldGood() {
+ $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}';
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res );
+ }
+
+ /**
+ * Handle metadata from paged tiff handler (gotten via instant commons) gracefully.
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testPagedTiffHandledGracefully() {
+ $meta = 'a:6:{s:9:"page_data";a:1:{i:1;a:5:{s:5:"width";i:643;s:6:"height";i:448;s:5:"alpha";s:4:"true";s:4:"page";i:1;s:6:"pixels";i:288064;}}s:10:"page_count";i:1;s:10:"first_page";i:1;s:9:"last_page";i:1;s:4:"exif";a:9:{s:10:"ImageWidth";i:643;s:11:"ImageLength";i:448;s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:4;s:12:"RowsPerStrip";i:50;s:19:"PlanarConfiguration";i:1;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}s:21:"TIFF_METADATA_VERSION";s:3:"1.4";}';
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public function testConvertMetadataLatest() {
+ $metadata = array(
+ 'foo' => array( 'First', 'Second', '_type' => 'ol' ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 2 );
+ $this->assertEquals( $metadata, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public function testConvertMetadataToOld() {
+ $metadata = array(
+ 'foo' => array( 'First', 'Second', '_type' => 'ol' ),
+ 'bar' => array( 'First', 'Second', '_type' => 'ul' ),
+ 'baz' => array( 'First', 'Second' ),
+ 'fred' => 'Single',
+ 'MEDIAWIKI_EXIF_VERSION' => 2,
+ );
+ $expected = array(
+ 'foo' => "\n#First\n#Second",
+ 'bar' => "\n*First\n*Second",
+ 'baz' => "\n*First\n*Second",
+ 'fred' => 'Single',
+ 'MEDIAWIKI_EXIF_VERSION' => 1,
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 1 );
+ $this->assertEquals( $expected, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public function testConvertMetadataSoftware() {
+ $metadata = array(
+ 'Software' => array( array( 'GIMP', '1.1' ) ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2,
+ );
+ $expected = array(
+ 'Software' => 'GIMP (Version 1.1)',
+ 'MEDIAWIKI_EXIF_VERSION' => 1,
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 1 );
+ $this->assertEquals( $expected, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public function testConvertMetadataSoftwareNormal() {
+ $metadata = array(
+ 'Software' => array( "GIMP 1.2", "vim" ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2,
+ );
+ $expected = array(
+ 'Software' => "\n*GIMP 1.2\n*vim",
+ 'MEDIAWIKI_EXIF_VERSION' => 1,
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 1 );
+ $this->assertEquals( $expected, $res );
+ }
+}
diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php
new file mode 100644
index 00000000..64276d92
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifRotationTest.php
@@ -0,0 +1,248 @@
+<?php
+/**
+ * Tests related to auto rotation.
+ *
+ * @group medium
+ *
+ * @todo covers tags
+ */
+class ExifRotationTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+
+ $this->handler = new BitmapHandler();
+ $filePath = __DIR__ . '/../../data/media';
+
+ $tmpDir = $this->getNewTempDirectory();
+
+ $this->repo = new FSRepo( array(
+ 'name' => 'temp',
+ 'url' => 'http://localhost/thumbtest',
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array( 'temp-thumb' => $tmpDir, 'data' => $filePath )
+ ) )
+ ) );
+
+ $this->setMwGlobals( array(
+ 'wgShowEXIF' => true,
+ 'wgEnableAutoRotation' => true,
+ ) );
+ }
+
+ /**
+ * @dataProvider provideFiles
+ */
+ public function testMetadata( $name, $type, $info ) {
+ if ( !BitmapHandler::canRotate() ) {
+ $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
+ }
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ *
+ * @dataProvider provideFiles
+ */
+ public function testRotationRendering( $name, $type, $info, $thumbs ) {
+ if ( !BitmapHandler::canRotate() ) {
+ $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
+ }
+ foreach ( $thumbs as $size => $out ) {
+ if ( preg_match( '/^(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ );
+ } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ 'height' => $matches[2]
+ );
+ } else {
+ throw new MWException( 'bogus test data format ' . $size );
+ }
+
+ $file = $this->dataFile( $name, $type );
+ $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE );
+
+ $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" );
+ $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" );
+
+ $gis = getimagesize( $thumb->getLocalCopyPath() );
+ if ( $out[0] > $info['width'] ) {
+ // Physical image won't be scaled bigger than the original.
+ $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size" );
+ $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size" );
+ } else {
+ $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size" );
+ $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size" );
+ }
+ }
+ }
+
+ /* Utility function */
+ private function dataFile( $name, $type ) {
+ return new UnregisteredLocalFile( false, $this->repo,
+ "mwstore://localtesting/data/$name", $type );
+ }
+
+ public static function provideFiles() {
+ return array(
+ array(
+ 'landscape-plain.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 1024,
+ 'height' => 768,
+ ),
+ array(
+ '800x600px' => array( 800, 600 ),
+ '9999x800px' => array( 1067, 800 ),
+ '800px' => array( 800, 600 ),
+ '600px' => array( 600, 450 ),
+ )
+ ),
+ array(
+ 'portrait-rotated.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 768, // as rotated
+ 'height' => 1024, // as rotated
+ ),
+ array(
+ '800x600px' => array( 450, 600 ),
+ '9999x800px' => array( 600, 800 ),
+ '800px' => array( 800, 1067 ),
+ '600px' => array( 600, 800 ),
+ )
+ )
+ );
+ }
+
+ /**
+ * Same as before, but with auto-rotation disabled.
+ * @dataProvider provideFilesNoAutoRotate
+ */
+ public function testMetadataNoAutoRotate( $name, $type, $info ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', false );
+
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ *
+ * @dataProvider provideFilesNoAutoRotate
+ */
+ public function testRotationRenderingNoAutoRotate( $name, $type, $info, $thumbs ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', false );
+
+ foreach ( $thumbs as $size => $out ) {
+ if ( preg_match( '/^(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ );
+ } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ 'height' => $matches[2]
+ );
+ } else {
+ throw new MWException( 'bogus test data format ' . $size );
+ }
+
+ $file = $this->dataFile( $name, $type );
+ $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE );
+
+ $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" );
+ $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" );
+
+ $gis = getimagesize( $thumb->getLocalCopyPath() );
+ if ( $out[0] > $info['width'] ) {
+ // Physical image won't be scaled bigger than the original.
+ $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size" );
+ $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size" );
+ } else {
+ $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size" );
+ $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size" );
+ }
+ }
+ }
+
+ public static function provideFilesNoAutoRotate() {
+ return array(
+ array(
+ 'landscape-plain.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 1024,
+ 'height' => 768,
+ ),
+ array(
+ '800x600px' => array( 800, 600 ),
+ '9999x800px' => array( 1067, 800 ),
+ '800px' => array( 800, 600 ),
+ '600px' => array( 600, 450 ),
+ )
+ ),
+ array(
+ 'portrait-rotated.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 1024, // since not rotated
+ 'height' => 768, // since not rotated
+ ),
+ array(
+ '800x600px' => array( 800, 600 ),
+ '9999x800px' => array( 1067, 800 ),
+ '800px' => array( 800, 600 ),
+ '600px' => array( 600, 450 ),
+ )
+ )
+ );
+ }
+
+
+ const TEST_WIDTH = 100;
+ const TEST_HEIGHT = 200;
+
+ /**
+ * @dataProvider provideBitmapExtractPreRotationDimensions
+ */
+ public function testBitmapExtractPreRotationDimensions( $rotation, $expected ) {
+ $result = $this->handler->extractPreRotationDimensions( array(
+ 'physicalWidth' => self::TEST_WIDTH,
+ 'physicalHeight' => self::TEST_HEIGHT,
+ ), $rotation );
+ $this->assertEquals( $expected, $result );
+ }
+
+ public static function provideBitmapExtractPreRotationDimensions() {
+ return array(
+ array(
+ 0,
+ array( self::TEST_WIDTH, self::TEST_HEIGHT )
+ ),
+ array(
+ 90,
+ array( self::TEST_HEIGHT, self::TEST_WIDTH )
+ ),
+ array(
+ 180,
+ array( self::TEST_WIDTH, self::TEST_HEIGHT )
+ ),
+ array(
+ 270,
+ array( self::TEST_HEIGHT, self::TEST_WIDTH )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php
new file mode 100644
index 00000000..dea36b03
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifTest.php
@@ -0,0 +1,46 @@
+<?php
+class ExifTest extends MediaWikiTestCase {
+
+ /** @var string */
+ protected $mediaPath;
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+
+ $this->mediaPath = __DIR__ . '/../../data/media/';
+
+
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+ }
+
+ public function testGPSExtraction() {
+ $filename = $this->mediaPath . 'exif-gps.jpg';
+ $seg = JpegMetadataExtractor::segmentSplitter( $filename );
+ $exif = new Exif( $filename, $seg['byteOrder'] );
+ $data = $exif->getFilteredData();
+ $expected = array(
+ 'GPSLatitude' => 88.5180555556,
+ 'GPSLongitude' => -21.12357,
+ 'GPSAltitude' => -3.141592653,
+ 'GPSDOP' => '5/1',
+ 'GPSVersionID' => '2.2.0.0',
+ );
+ $this->assertEquals( $expected, $data, '', 0.0000000001 );
+ }
+
+ public function testUnicodeUserComment() {
+ $filename = $this->mediaPath . 'exif-user-comment.jpg';
+ $seg = JpegMetadataExtractor::segmentSplitter( $filename );
+ $exif = new Exif( $filename, $seg['byteOrder'] );
+ $data = $exif->getFilteredData();
+
+ $expected = array(
+ 'UserComment' => 'test⁔comment'
+ );
+ $this->assertEquals( $expected, $data );
+ }
+}
diff --git a/tests/phpunit/includes/media/FakeDimensionFile.php b/tests/phpunit/includes/media/FakeDimensionFile.php
new file mode 100644
index 00000000..7926000b
--- /dev/null
+++ b/tests/phpunit/includes/media/FakeDimensionFile.php
@@ -0,0 +1,28 @@
+<?php
+
+class FakeDimensionFile extends File {
+ public $mustRender = false;
+
+ public function __construct( $dimensions ) {
+ parent::__construct( Title::makeTitle( NS_FILE, 'Test' ),
+ new NullRepo( null ) );
+
+ $this->dimensions = $dimensions;
+ }
+
+ public function getWidth( $page = 1 ) {
+ return $this->dimensions[0];
+ }
+
+ public function getHeight( $page = 1 ) {
+ return $this->dimensions[1];
+ }
+
+ public function mustRender() {
+ return $this->mustRender;
+ }
+
+ public function getPath() {
+ return '';
+ }
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php
new file mode 100644
index 00000000..a073e4ca
--- /dev/null
+++ b/tests/phpunit/includes/media/FormatMetadataTest.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @todo covers tags
+ */
+class FormatMetadataTest extends MediaWikiTestCase {
+
+ /** @var FSFileBackend */
+ protected $backend;
+ /** @var FSRepo */
+ protected $repo;
+
+ protected function setUp() {
+ parent::setUp();
+
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+ $filePath = __DIR__ . '/../../data/media';
+ $this->backend = new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array( 'data' => $filePath )
+ ) );
+ $this->repo = new FSRepo( array(
+ 'name' => 'temp',
+ 'url' => 'http://localhost/thumbtest',
+ 'backend' => $this->backend
+ ) );
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+ }
+
+ public function testInvalidDate() {
+ $file = $this->dataFile( 'broken_exif_date.jpg', 'image/jpeg' );
+
+ // Throws an error if bug hit
+ $meta = $file->formatMetadata();
+ $this->assertNotEquals( false, $meta, 'Valid metadata extracted' );
+
+ // Find date exif entry
+ $this->assertArrayHasKey( 'visible', $meta );
+ $dateIndex = null;
+ foreach ( $meta['visible'] as $i => $data ) {
+ if ( $data['id'] == 'exif-datetimeoriginal' ) {
+ $dateIndex = $i;
+ }
+ }
+ $this->assertNotNull( $dateIndex, 'Date entry exists in metadata' );
+ $this->assertEquals( '0000:01:00 00:02:27',
+ $meta['visible'][$dateIndex]['value'],
+ 'File with invalid date metadata (bug 29471)' );
+ }
+
+ private function dataFile( $name, $type ) {
+ return new UnregisteredLocalFile( false, $this->repo,
+ "mwstore://localtesting/data/$name", $type );
+ }
+}
diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
new file mode 100644
index 00000000..9e3f9244
--- /dev/null
+++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
@@ -0,0 +1,107 @@
+<?php
+class GIFMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mediaPath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * Put in a file, and see if the metadata coming out is as expected.
+ * @param $filename String
+ * @param $expected Array The extracted metadata.
+ * @dataProvider provideGetMetadata
+ * @covers GIFMetadataExtractor::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetMetadata() {
+
+ $xmpNugget = <<<EOF
+<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
+<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'>
+<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+
+ <rdf:Description rdf:about=''
+ xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'>
+ <Iptc4xmpCore:Location>The interwebs</Iptc4xmpCore:Location>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+ xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
+ <tiff:Artist>Bawolff</tiff:Artist>
+ <tiff:ImageDescription>
+ <rdf:Alt>
+ <rdf:li xml:lang='x-default'>A file to test GIF</rdf:li>
+ </rdf:Alt>
+ </tiff:ImageDescription>
+ </rdf:Description>
+</rdf:RDF>
+</x:xmpmeta>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<?xpacket end='w'?>
+EOF;
+ $xmpNugget = str_replace( "\r", '', $xmpNugget ); // Windows compat
+
+ return array(
+ array(
+ 'nonanimated.gif',
+ array(
+ 'comment' => array( 'GIF test file ⁕ Created with GIMP' ),
+ 'duration' => 0.1,
+ 'frameCount' => 1,
+ 'looped' => false,
+ 'xmp' => '',
+ )
+ ),
+ array(
+ 'animated.gif',
+ array(
+ 'comment' => array( 'GIF test file . Created with GIMP' ),
+ 'duration' => 2.4,
+ 'frameCount' => 4,
+ 'looped' => true,
+ 'xmp' => '',
+ )
+ ),
+
+ array(
+ 'animated-xmp.gif',
+ array(
+ 'xmp' => $xmpNugget,
+ 'duration' => 2.4,
+ 'frameCount' => 4,
+ 'looped' => true,
+ 'comment' => array( 'GIƒ·test·file' ),
+ )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php
new file mode 100644
index 00000000..c8e729c8
--- /dev/null
+++ b/tests/phpunit/includes/media/GIFTest.php
@@ -0,0 +1,120 @@
+<?php
+class GIFHandlerTest extends MediaWikiTestCase {
+
+ /** @var FSFileBackend */
+ protected $backend;
+ /** @var GIFHandler */
+ protected $handler;
+ /** @var FSRepo */
+ protected $repo;
+ /** @var string */
+ protected $filePath;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->filePath = __DIR__ . '/../../data/media';
+ $this->backend = new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array( 'data' => $this->filePath )
+ ) );
+ $this->repo = new FSRepo( array(
+ 'name' => 'temp',
+ 'url' => 'http://localhost/thumbtest',
+ 'backend' => $this->backend
+ ) );
+ $this->handler = new GIFHandler();
+ }
+
+ /**
+ * @covers GIFHandler::getMetadata
+ */
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . '/README' );
+ $this->assertEquals( GIFHandler::BROKEN_FILE, $res );
+ }
+
+ /**
+ * @param $filename String basename of the file to check
+ * @param $expected boolean Expected result.
+ * @dataProvider provideIsAnimated
+ * @covers GIFHandler::isAnimatedImage
+ */
+ public function testIsAnimanted( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->isAnimatedImage( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsAnimated() {
+ return array(
+ array( 'animated.gif', true ),
+ array( 'nonanimated.gif', false ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected Integer Total image area
+ * @dataProvider provideGetImageArea
+ * @covers GIFHandler::getImageArea
+ */
+ public function testGetImageArea( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetImageArea() {
+ return array(
+ array( 'animated.gif', 5400 ),
+ array( 'nonanimated.gif', 1350 ),
+ );
+ }
+
+ /**
+ * @param $metadata String Serialized metadata
+ * @param $expected Integer One of the class constants of GIFHandler
+ * @dataProvider provideIsMetadataValid
+ * @covers GIFHandler::isMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsMetadataValid() {
+ return array(
+ array( GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ),
+ array( '', GIFHandler::METADATA_BAD ),
+ array( null, GIFHandler::METADATA_BAD ),
+ array( 'Something invalid!', GIFHandler::METADATA_BAD ),
+ array( 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', GIFHandler::METADATA_GOOD ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected String Serialized array
+ * @dataProvider provideGetMetadata
+ * @covers GIFHandler::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" );
+ $this->assertEquals( unserialize( $expected ), unserialize( $actual ) );
+ }
+
+ public static function provideGetMetadata() {
+ return array(
+ array( 'nonanimated.gif', 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' ),
+ array( 'animated-xmp.gif', 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' ),
+ );
+ }
+
+ private function dataFile( $name, $type ) {
+ return new UnregisteredLocalFile( false, $this->repo,
+ "mwstore://localtesting/data/$name", $type );
+ }
+}
diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php
new file mode 100644
index 00000000..81c1d287
--- /dev/null
+++ b/tests/phpunit/includes/media/IPTCTest.php
@@ -0,0 +1,81 @@
+<?php
+
+class IPTCTest extends MediaWikiTestCase {
+
+ /**
+ * @covers IPTC::getCharset
+ */
+ public function testRecognizeUtf8() {
+ // utf-8 is the only one used in practise.
+ $res = IPTC::getCharset( "\x1b%G" );
+ $this->assertEquals( 'UTF-8', $res );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseNoCharset88591() {
+ // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1
+ // This data doesn't specify a charset. We're supposed to guess
+ // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not)
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼' ), $res['Keywords'] );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseNoCharset88591b() {
+ /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */
+ /* \xC3 = Ã, \xB8 = ¸ */
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( 'ÃÃø' ), $res['Keywords'] );
+ }
+
+ /**
+ * Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8.
+ * What should happen is the first "\xC3\xC3" should be dropped as invalid,
+ * leaving \xC3\xB8, which is ø
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseForcedUTFButInvalid() {
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"
+ . "\x1c\x01\x5A\x00\x03\x1B\x25\x47";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( 'ø' ), $res['Keywords'] );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseNoCharsetUTF8() {
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼' ), $res['Keywords'] );
+ }
+
+ /**
+ * Testing something that has 2 values for keyword
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseMulti() {
+ $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4"
+ /* length */ . "\0\0\0\0\0\x0D"
+ . "\x1c\x02\x19" . "\x00\x01" . "\xBC"
+ . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼', '¼½' ), $res['Keywords'] );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseUTF8() {
+ // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8.
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼' ), $res['Keywords'] );
+ }
+}
diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
new file mode 100644
index 00000000..eafc8a2e
--- /dev/null
+++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * @todo Could use a test of extended XMP segments. Hard to find programs that
+ * create example files, and creating my own in vim propbably wouldn't
+ * serve as a very good "test". (Adobe photoshop probably creates such files
+ * but it costs money). The implementation of it currently in MediaWiki is based
+ * solely on reading the standard, without any real world test files.
+ * @todo covers tags
+ */
+class JpegMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected $filePath;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * We also use this test to test padding bytes don't
+ * screw stuff up
+ *
+ * @param string $file filename
+ *
+ * @dataProvider provideUtf8Comment
+ */
+ public function testUtf8Comment( $file ) {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+ $this->assertEquals( array( 'UTF-8 JPEG Comment — ¼' ), $res['COM'] );
+ }
+
+ public static function provideUtf8Comment() {
+ return array(
+ array( 'jpeg-comment-utf.jpg' ),
+ array( 'jpeg-padding-even.jpg' ),
+ array( 'jpeg-padding-odd.jpg' ),
+ );
+ }
+
+ /** The file is iso-8859-1, but it should get auto converted */
+ public function testIso88591Comment() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
+ $this->assertEquals( array( 'ISO-8859-1 JPEG Comment - ¼' ), $res['COM'] );
+ }
+
+ /** Comment values that are non-textual (random binary junk) should not be shown.
+ * The example test file has a comment with a 0x5 byte in it which is a control character
+ * and considered binary junk for our purposes.
+ */
+ public function testBinaryCommentStripped() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
+ $this->assertEmpty( $res['COM'] );
+ }
+
+ /* Very rarely a file can have multiple comments.
+ * Order of comments is based on order inside the file.
+ */
+ public function testMultipleComment() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
+ $this->assertEquals( array( 'foo', 'bar' ), $res['COM'] );
+ }
+
+ public function testXMPExtraction() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+ $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+ $this->assertEquals( $expected, $res['XMP'] );
+ }
+
+ public function testPSIRExtraction() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+ $expected = '50686f746f73686f7020332e30003842494d04040000000000181c02190004746573741c02190003666f6f1c020000020004';
+ $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
+ }
+
+ public function testXMPExtractionAltAppId() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
+ $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+ $this->assertEquals( $expected, $res['XMP'] );
+ }
+
+
+ public function testIPTCHashComparisionNoHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+ $this->assertEquals( 'iptc-no-hash', $res );
+ }
+
+ public function testIPTCHashComparisionBadHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+ $this->assertEquals( 'iptc-bad-hash', $res );
+ }
+
+ public function testIPTCHashComparisionGoodHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+ $this->assertEquals( 'iptc-good-hash', $res );
+ }
+
+ public function testExifByteOrder() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
+ $expected = 'BE';
+ $this->assertEquals( $expected, $res['byteOrder'] );
+ }
+}
diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php
new file mode 100644
index 00000000..9af4f1e1
--- /dev/null
+++ b/tests/phpunit/includes/media/JpegTest.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @todo covers tags
+ */
+class JpegTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+ }
+
+ public function testInvalidFile() {
+ $jpeg = new JpegHandler;
+ $res = $jpeg->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res );
+ }
+
+ public function testJpegMetadataExtraction() {
+ $h = new JpegHandler;
+ $res = $h->getMetadata( null, $this->filePath . 'test.jpg' );
+ $expected = 'a:7:{s:16:"ImageDescription";s:9:"Test file";s:11:"XResolution";s:4:"72/1";s:11:"YResolution";s:4:"72/1";s:14:"ResolutionUnit";i:2;s:16:"YCbCrPositioning";i:1;s:15:"JPEGFileComment";a:1:{i:0;s:17:"Created with GIMP";}s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}';
+
+ // Unserialize in case serialization format ever changes.
+ $this->assertEquals( unserialize( $expected ), unserialize( $res ) );
+ }
+}
diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php
new file mode 100644
index 00000000..c28898bb
--- /dev/null
+++ b/tests/phpunit/includes/media/MediaHandlerTest.php
@@ -0,0 +1,53 @@
+<?php
+
+class MediaHandlerTest extends MediaWikiTestCase {
+
+ /**
+ * @covers MediaHandler::fitBoxWidth
+ * @todo split into a dataprovider and test method
+ */
+ public 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 ) {
+ $tests = $row['tests'];
+ $height = $row['height'];
+ $width = $row['width'];
+ foreach ( $tests as $max => $expected ) {
+ $y = round( $expected * $height / $width );
+ $result = MediaHandler::fitBoxWidth( $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/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php
new file mode 100644
index 00000000..939f2cfc
--- /dev/null
+++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @todo covers tags
+ */
+class PNGMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->filePath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * Tests zTXt tag (compressed textual metadata)
+ */
+ public function testPngNativetZtxt() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+ $expected = "foo bar baz foo foo foo foof foo foo foo foo";
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+ $this->assertArrayHasKey( 'Make', $meta );
+ $this->assertArrayHasKey( 'x-default', $meta['Make'] );
+
+ $this->assertEquals( $expected, $meta['Make']['x-default'] );
+ }
+
+ /**
+ * Test tEXt tag (Uncompressed textual metadata)
+ */
+ public function testPngNativeText() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+ $expected = "Some long image desc";
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+ $this->assertArrayHasKey( 'ImageDescription', $meta );
+ $this->assertArrayHasKey( 'x-default', $meta['ImageDescription'] );
+ $this->assertArrayHasKey( '_type', $meta['ImageDescription'] );
+
+ $this->assertEquals( $expected, $meta['ImageDescription']['x-default'] );
+ }
+
+ /**
+ * tEXt tags must be encoded iso-8859-1 (vs iTXt which are utf-8)
+ * Make sure non-ascii characters get converted properly
+ */
+ public function testPngNativeTextNonAscii() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ // Note the Copyright symbol here is a utf-8 one
+ // (aka \xC2\xA9) where in the file its iso-8859-1
+ // encoded as just \xA9.
+ $expected = "© 2010 Bawolff";
+
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+ $this->assertArrayHasKey( 'Copyright', $meta );
+ $this->assertArrayHasKey( 'x-default', $meta['Copyright'] );
+
+ $this->assertEquals( $expected, $meta['Copyright']['x-default'] );
+ }
+
+ /**
+ * Test extraction of pHYs tags, which can tell what the
+ * actual resolution of the image is (aka in dots per meter).
+ */
+ /*
+ public function testPngPhysTag() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+
+ $this->assertEquals( '2835/100', $meta['XResolution'] );
+ $this->assertEquals( '2835/100', $meta['YResolution'] );
+ $this->assertEquals( 3, $meta['ResolutionUnit'] ); // 3 = cm
+ }
+ */
+
+ /**
+ * Given a normal static PNG, check the animation metadata returned.
+ */
+ public function testStaticPngAnimationMetadata() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 0, $meta['frameCount'] );
+ $this->assertEquals( 1, $meta['loopCount'] );
+ $this->assertEquals( 0, $meta['duration'] );
+ }
+
+ /**
+ * Given an animated APNG image file
+ * check it gets animated metadata right.
+ */
+ public function testApngAnimationMetadata() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Animated_PNG_example_bouncing_beach_ball.png' );
+
+ $this->assertEquals( 20, $meta['frameCount'] );
+ // Note loop count of 0 = infinity
+ $this->assertEquals( 0, $meta['loopCount'] );
+ $this->assertEquals( 1.5, $meta['duration'], '', 0.00001 );
+ }
+
+ public function testPngBitDepth8() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 8, $meta['bitDepth'] );
+ }
+
+ public function testPngBitDepth1() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ '1bit-png.png' );
+ $this->assertEquals( 1, $meta['bitDepth'] );
+ }
+
+
+ public function testPngIndexColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 'index-coloured', $meta['colorType'] );
+ }
+
+ public function testPngRgbColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'rgb-png.png' );
+ $this->assertEquals( 'truecolour-alpha', $meta['colorType'] );
+ }
+
+ public function testPngRgbNoAlphaColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'rgb-na-png.png' );
+ $this->assertEquals( 'truecolour', $meta['colorType'] );
+ }
+
+ public function testPngGreyscaleColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'greyscale-png.png' );
+ $this->assertEquals( 'greyscale-alpha', $meta['colorType'] );
+ }
+
+ public function testPngGreyscaleNoAlphaColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'greyscale-na-png.png' );
+ $this->assertEquals( 'greyscale', $meta['colorType'] );
+ }
+}
diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php
new file mode 100644
index 00000000..ad4c2493
--- /dev/null
+++ b/tests/phpunit/includes/media/PNGTest.php
@@ -0,0 +1,123 @@
+<?php
+class PNGHandlerTest extends MediaWikiTestCase {
+
+ /** @var PNGHandler */
+ protected $handler;
+ /** @var FSRepo */
+ protected $repo;
+ /** @var FSFileBackend */
+ protected $backend;
+ /** @var string */
+ protected $filePath;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->filePath = __DIR__ . '/../../data/media';
+ $this->backend = new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array( 'data' => $this->filePath )
+ ) );
+ $this->repo = new FSRepo( array(
+ 'name' => 'temp',
+ 'url' => 'http://localhost/thumbtest',
+ 'backend' => $this->backend
+ ) );
+ $this->handler = new PNGHandler();
+ }
+
+ /**
+ * @covers PNGHandler::getMetadata
+ */
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . '/README' );
+ $this->assertEquals( PNGHandler::BROKEN_FILE, $res );
+ }
+
+ /**
+ * @param $filename String basename of the file to check
+ * @param $expected boolean Expected result.
+ * @dataProvider provideIsAnimated
+ * @covers PNGHandler::isAnimatedImage
+ */
+ public function testIsAnimanted( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->isAnimatedImage( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsAnimated() {
+ return array(
+ array( 'Animated_PNG_example_bouncing_beach_ball.png', true ),
+ array( '1bit-png.png', false ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected Integer Total image area
+ * @dataProvider provideGetImageArea
+ * @covers PNGHandler::getImageArea
+ */
+ public function testGetImageArea( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetImageArea() {
+ return array(
+ array( '1bit-png.png', 2500 ),
+ array( 'greyscale-png.png', 2500 ),
+ array( 'Png-native-test.png', 126000 ),
+ array( 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ),
+ );
+ }
+
+ /**
+ * @param $metadata String Serialized metadata
+ * @param $expected Integer One of the class constants of PNGHandler
+ * @dataProvider provideIsMetadataValid
+ * @covers PNGHandler::isMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsMetadataValid() {
+ return array(
+ array( PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ),
+ array( '', PNGHandler::METADATA_BAD ),
+ array( null, PNGHandler::METADATA_BAD ),
+ array( 'Something invalid!', PNGHandler::METADATA_BAD ),
+ array( 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', PNGHandler::METADATA_GOOD ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected String Serialized array
+ * @dataProvider provideGetMetadata
+ * @covers PNGHandler::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" );
+// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) );
+ $this->assertEquals( ( $expected ), ( $actual ) );
+ }
+
+ public static function provideGetMetadata() {
+ return array(
+ array( 'rgb-na-png.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' ),
+ array( 'xmp.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' ),
+ );
+ }
+
+ private function dataFile( $name, $type ) {
+ return new UnregisteredLocalFile( false, $this->repo,
+ "mwstore://localtesting/data/$name", $type );
+ }
+}
diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
new file mode 100644
index 00000000..257009b0
--- /dev/null
+++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @todo covers tags
+ */
+class SVGMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ AutoLoader::loadClass( 'SVGMetadataExtractorTest' );
+ }
+
+ /**
+ * @dataProvider provideSvgFiles
+ */
+ public function testGetMetadata( $infile, $expected ) {
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider provideSvgFilesWithXMLMetadata
+ */
+ public function testGetXMLMetadata( $infile, $expected ) {
+ $r = new XMLReader();
+ if ( !method_exists( $r, 'readInnerXML' ) ) {
+ $this->markTestSkipped( 'XMLReader::readInnerXML() does not exist (libxml >2.6.20 needed).' );
+
+ return;
+ }
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ function assertMetadata( $infile, $expected ) {
+ try {
+ $data = SVGMetadataExtractor::getMetadata( $infile );
+ $this->assertEquals( $expected, $data, 'SVG metadata extraction test' );
+ } catch ( MWException $e ) {
+ if ( $expected === false ) {
+ $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' );
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ public static function provideSvgFiles() {
+ $base = __DIR__ . '/../../data/media';
+
+ return array(
+ array(
+ "$base/Wikimedia-logo.svg",
+ array(
+ 'width' => 1024,
+ 'height' => 1024,
+ 'originalWidth' => '1024',
+ 'originalHeight' => '1024',
+ )
+ ),
+ array(
+ "$base/QA_icon.svg",
+ array(
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60',
+ 'originalHeight' => '60',
+ )
+ ),
+ array(
+ "$base/Gtk-media-play-ltr.svg",
+ array(
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60.0000000',
+ 'originalHeight' => '60.0000000',
+ )
+ ),
+ array(
+ "$base/Toll_Texas_1.svg",
+ // This file triggered bug 31719, needs entity expansion in the xmlns checks
+ array(
+ 'width' => 385,
+ 'height' => 385,
+ 'originalWidth' => '385',
+ 'originalHeight' => '385.0004883',
+ )
+ )
+ );
+ }
+
+ public static function provideSvgFilesWithXMLMetadata() {
+ $base = __DIR__ . '/../../data/media';
+ $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about="">
+ <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
+ <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ </ns4:Work>
+ </rdf:RDF>';
+ $metadata = str_replace( "\r", '', $metadata ); // Windows compat
+ return array(
+ array(
+ "$base/US_states_by_total_state_tax_revenue.svg",
+ array(
+ 'height' => 593,
+ 'metadata' => $metadata,
+ 'width' => 959,
+ 'originalWidth' => '958.69',
+ 'originalHeight' => '592.78998',
+ )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php
new file mode 100644
index 00000000..8d74b98d
--- /dev/null
+++ b/tests/phpunit/includes/media/TiffTest.php
@@ -0,0 +1,39 @@
+<?php
+class TiffTest extends MediaWikiTestCase {
+
+ /** @var TiffHandler */
+ protected $handler;
+ /** @var string */
+ protected $filePath;
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+ $this->handler = new TiffHandler;
+ }
+
+ /**
+ * @covers TiffHandler::getMetadata
+ */
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res );
+ }
+
+ /**
+ * @covers TiffHandler::getMetadata
+ */
+ public function testTiffMetadataExtraction() {
+ $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' );
+ $expected = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}';
+ // Re-unserialize in case there are subtle differences between how versions
+ // of php serialize stuff.
+ $this->assertEquals( unserialize( $expected ), unserialize( $res ) );
+ }
+}
diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php
new file mode 100644
index 00000000..d12e9b00
--- /dev/null
+++ b/tests/phpunit/includes/media/XMPTest.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * @todo covers tags
+ */
+class XMPTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !extension_loaded( 'xml' ) ) {
+ $this->markTestSkipped( 'Requires libxml to do XMP parsing' );
+ }
+ }
+
+ /**
+ * Put XMP in, compare what comes out...
+ *
+ * @param $xmp String the actual xml data.
+ * @param $expected Array expected result of parsing the xmp.
+ * @param $info String Short sentence on what's being tested.
+ *
+ * @throws Exception
+ * @dataProvider provideXMPParse
+ */
+ public function testXMPParse( $xmp, $expected, $info ) {
+ if ( !is_string( $xmp ) || !is_array( $expected ) ) {
+ throw new Exception( "Invalid data provided to " . __METHOD__ );
+ }
+ $reader = new XMPReader;
+ $reader->parse( $xmp );
+ $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 );
+ }
+
+ public static function provideXMPParse() {
+ $xmpPath = __DIR__ . '/../../data/xmp/';
+ $data = array();
+
+ // $xmpFiles format: array of arrays with first arg file base name,
+ // with the actual file having .xmp on the end for the xmp
+ // and .result.php on the end for a php file containing the result
+ // array. Second argument is some info on what's being tested.
+ $xmpFiles = array(
+ array( '1', 'parseType=Resource test' ),
+ array( '2', 'Structure with mixed attribute and element props' ),
+ array( '3', 'Extra qualifiers (that should be ignored)' ),
+ array( '3-invalid', 'Test ignoring qualifiers that look like normal props' ),
+ array( '4', 'Flash as qualifier' ),
+ array( '5', 'Flash as qualifier 2' ),
+ array( '6', 'Multiple rdf:Description' ),
+ array( '7', 'Generic test of several property types' ),
+ array( 'flash', 'Test of Flash property' ),
+ array( 'invalid-child-not-struct', 'Test child props not in struct or ignored' ),
+ array( 'no-recognized-props', 'Test namespace and no recognized props' ),
+ array( 'no-namespace', 'Test non-namespaced attributes are ignored' ),
+ array( 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ),
+ array( 'utf16BE', 'UTF-16BE encoding' ),
+ array( 'utf16LE', 'UTF-16LE encoding' ),
+ array( 'utf32BE', 'UTF-32BE encoding' ),
+ array( 'utf32LE', 'UTF-32LE encoding' ),
+ array( 'xmpExt', 'Extended XMP missing second part' ),
+ array( 'gps', 'Handling of exif GPS parameters in XMP' ),
+ );
+
+ foreach ( $xmpFiles as $file ) {
+ $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' );
+ // I'm not sure if this is the best way to handle getting the
+ // result array, but it seems kind of big to put directly in the test
+ // file.
+ $result = null;
+ include $xmpPath . $file[0] . '.result.php';
+ $data[] = array( $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] );
+ }
+
+ return $data;
+ }
+
+ /** Test ExtendedXMP block support. (Used when the XMP has to be split
+ * over multiple jpeg segments, due to 64k size limit on jpeg segments.
+ *
+ * @todo This is based on what the standard says. Need to find a real
+ * world example file to double check the support for this is right.
+ */
+ public function testExtendedXMP() {
+ $xmpPath = __DIR__ . '/../../data/xmp/';
+ $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+ $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+ $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
+ $length = pack( 'N', strlen( $extendedXMP ) );
+ $offset = pack( 'N', 0 );
+ $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+ $reader = new XMPReader();
+ $reader->parse( $standardXMP );
+ $reader->parseExtended( $extendedPacket );
+ $actual = $reader->getResults();
+
+ $expected = array(
+ 'xmp-exif' => array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => 9,
+ 'FNumber' => '2/10',
+ )
+ );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ /**
+ * This test has an extended XMP block with a wrong guid (md5sum)
+ * and thus should only return the StandardXMP, not the ExtendedXMP.
+ */
+ public function testExtendedXMPWithWrongGUID() {
+ $xmpPath = __DIR__ . '/../../data/xmp/';
+ $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+ $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+ $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit.
+ $length = pack( 'N', strlen( $extendedXMP ) );
+ $offset = pack( 'N', 0 );
+ $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+ $reader = new XMPReader();
+ $reader->parse( $standardXMP );
+ $reader->parseExtended( $extendedPacket );
+ $actual = $reader->getResults();
+
+ $expected = array(
+ 'xmp-exif' => array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => 9,
+ )
+ );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ /**
+ * Have a high offset to simulate a missing packet,
+ * which should cause it to ignore the ExtendedXMP packet.
+ */
+ public function testExtendedXMPMissingPacket() {
+ $xmpPath = __DIR__ . '/../../data/xmp/';
+ $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+ $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+ $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
+ $length = pack( 'N', strlen( $extendedXMP ) );
+ $offset = pack( 'N', 2048 );
+ $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+ $reader = new XMPReader();
+ $reader->parse( $standardXMP );
+ $reader->parseExtended( $extendedPacket );
+ $actual = $reader->getResults();
+
+ $expected = array(
+ 'xmp-exif' => array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => 9,
+ )
+ );
+
+ $this->assertEquals( $expected, $actual );
+ }
+}
diff --git a/tests/phpunit/includes/media/XMPValidateTest.php b/tests/phpunit/includes/media/XMPValidateTest.php
new file mode 100644
index 00000000..96bf5e47
--- /dev/null
+++ b/tests/phpunit/includes/media/XMPValidateTest.php
@@ -0,0 +1,46 @@
+<?php
+class XMPValidateTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideDates
+ * @covers XMPValidate::validateDate
+ */
+ public function testValidateDate( $value, $expected ) {
+ // The method should modify $value.
+ XMPValidate::validateDate( array(), $value, true );
+ $this->assertEquals( $expected, $value );
+ }
+
+ public static function provideDates() {
+ /* For reference valid date formats are:
+ * YYYY
+ * YYYY-MM
+ * YYYY-MM-DD
+ * YYYY-MM-DDThh:mmTZD
+ * YYYY-MM-DDThh:mm:ssTZD
+ * YYYY-MM-DDThh:mm:ss.sTZD
+ * (Time zone is optional)
+ */
+ return array(
+ array( '1992', '1992' ),
+ array( '1992-04', '1992:04' ),
+ array( '1992-02-01', '1992:02:01' ),
+ array( '2011-09-29', '2011:09:29' ),
+ array( '1982-12-15T20:12', '1982:12:15 20:12' ),
+ array( '1982-12-15T20:12Z', '1982:12:15 20:12' ),
+ array( '1982-12-15T20:12+02:30', '1982:12:15 22:42' ),
+ array( '1982-12-15T01:12-02:30', '1982:12:14 22:42' ),
+ array( '1982-12-15T20:12:11', '1982:12:15 20:12:11' ),
+ array( '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ),
+ array( '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ),
+ array( '2045-12-15T20:12:11', '2045:12:15 20:12:11' ),
+ array( '1867-06-01T15:00:00', '1867:06:01 15:00:00' ),
+ /* some invalid ones */
+ array( '2001--12', null ),
+ array( '2001-5-12', null ),
+ array( '2001-5-12TZ', null ),
+ array( '2001-05-12T15', null ),
+ array( '2001-12T15:13', null ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/normal/CleanUpTest.php b/tests/phpunit/includes/normal/CleanUpTest.php
new file mode 100644
index 00000000..52dd2ef5
--- /dev/null
+++ b/tests/phpunit/includes/normal/CleanUpTest.php
@@ -0,0 +1,409 @@
+<?php
+/**
+ * Tests for UtfNormal::cleanUp() function.
+ *
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Additional tests for UtfNormal::cleanUp() function, inclusion
+ * regression checks for known problems.
+ * Requires PHPUnit.
+ *
+ * @ingroup UtfNormal
+ * @group Large
+ *
+ * We ignore code coverage for this test suite until they are rewritten
+ * to use data providers (bug 46561).
+ * @codeCoverageIgnore
+ */
+class CleanUpTest extends MediaWikiTestCase {
+ /** @todo document */
+ public function testAscii() {
+ $text = 'This is plain ASCII text.';
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ public function testNull() {
+ $text = "a \x00 null";
+ $expect = "a \xef\xbf\xbd null";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testLatin() {
+ $text = "L'\xc3\xa9cole";
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ public function testLatinNormal() {
+ $text = "L'e\xcc\x81cole";
+ $expect = "L'\xc3\xa9cole";
+ $this->assertEquals( $expect, UtfNormal::cleanUp( $text ) );
+ }
+
+ /**
+ * This test is *very* expensive!
+ * @todo document
+ */
+ function XtestAllChars() {
+ $rep = UTF8_REPLACEMENT;
+ for ( $i = 0x0; $i < UNICODE_MAX; $i++ ) {
+ $char = codepointToUtf8( $i );
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%04X", $i );
+
+ if ( $i % 0x1000 == 0 ) {
+ echo "U+$x\n";
+ }
+
+ if ( $i == 0x0009 ||
+ $i == 0x000a ||
+ $i == 0x000d ||
+ ( $i > 0x001f && $i < UNICODE_SURROGATE_FIRST ) ||
+ ( $i > UNICODE_SURROGATE_LAST && $i < 0xfffe ) ||
+ ( $i > 0xffff && $i <= UNICODE_MAX )
+ ) {
+ if ( isset( UtfNormal::$utfCanonicalComp[$char] ) || isset( UtfNormal::$utfCanonicalDecomp[$char] ) ) {
+ $comp = UtfNormal::NFC( $char );
+ $this->assertEquals(
+ bin2hex( $comp ),
+ bin2hex( $clean ),
+ "U+$x should be decomposed" );
+ } else {
+ $this->assertEquals(
+ bin2hex( $char ),
+ bin2hex( $clean ),
+ "U+$x should be intact" );
+ }
+ } else {
+ $this->assertEquals( bin2hex( $rep ), bin2hex( $clean ), $x );
+ }
+ }
+ }
+
+ /** @todo document */
+ public function testAllBytes() {
+ $this->doTestBytes( '', '' );
+ $this->doTestBytes( 'x', '' );
+ $this->doTestBytes( '', 'x' );
+ $this->doTestBytes( 'x', 'x' );
+ }
+
+ /** @todo document */
+ function doTestBytes( $head, $tail ) {
+ for ( $i = 0x0; $i < 256; $i++ ) {
+ $char = $head . chr( $i ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X", $i );
+
+ if ( $i == 0x0009 ||
+ $i == 0x000a ||
+ $i == 0x000d ||
+ ( $i > 0x001f && $i < 0x80 )
+ ) {
+ $this->assertEquals(
+ bin2hex( $char ),
+ bin2hex( $clean ),
+ "ASCII byte $x should be intact" );
+ if ( $char != $clean ) {
+ return;
+ }
+ } else {
+ $norm = $head . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden byte $x should be rejected" );
+ if ( $norm != $clean ) {
+ return;
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ public function testDoubleBytes() {
+ $this->doTestDoubleBytes( '', '' );
+ $this->doTestDoubleBytes( 'x', '' );
+ $this->doTestDoubleBytes( '', 'x' );
+ $this->doTestDoubleBytes( 'x', 'x' );
+ }
+
+ /**
+ * @todo document
+ */
+ function doTestDoubleBytes( $head, $tail ) {
+ for ( $first = 0xc0; $first < 0x100; $first += 2 ) {
+ for ( $second = 0x80; $second < 0x100; $second += 2 ) {
+ $char = $head . chr( $first ) . chr( $second ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X,%02X", $first, $second );
+ if ( $first > 0xc1 &&
+ $first < 0xe0 &&
+ $second < 0xc0
+ ) {
+ $norm = UtfNormal::NFC( $char );
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Pair $x should be intact" );
+ if ( $norm != $clean ) {
+ return;
+ }
+ } elseif ( $first > 0xfd || $second > 0xbf ) {
+ # fe and ff are not legal head bytes -- expect two replacement chars
+ $norm = $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden pair $x should be rejected" );
+ if ( $norm != $clean ) {
+ return;
+ }
+ } else {
+ $norm = $head . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden pair $x should be rejected" );
+ if ( $norm != $clean ) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ public function testTripleBytes() {
+ $this->doTestTripleBytes( '', '' );
+ $this->doTestTripleBytes( 'x', '' );
+ $this->doTestTripleBytes( '', 'x' );
+ $this->doTestTripleBytes( 'x', 'x' );
+ }
+
+ /** @todo document */
+ function doTestTripleBytes( $head, $tail ) {
+ for ( $first = 0xc0; $first < 0x100; $first += 2 ) {
+ for ( $second = 0x80; $second < 0x100; $second += 2 ) {
+ #for( $third = 0x80; $third < 0x100; $third++ ) {
+ for ( $third = 0x80; $third < 0x81; $third++ ) {
+ $char = $head . chr( $first ) . chr( $second ) . chr( $third ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X,%02X,%02X", $first, $second, $third );
+
+ if ( $first >= 0xe0 &&
+ $first < 0xf0 &&
+ $second < 0xc0 &&
+ $third < 0xc0
+ ) {
+ if ( $first == 0xe0 && $second < 0xa0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Overlong triplet $x should be rejected" );
+ } elseif ( $first == 0xed &&
+ ( chr( $first ) . chr( $second ) . chr( $third ) ) >= UTF8_SURROGATE_FIRST
+ ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Surrogate triplet $x should be rejected" );
+ } else {
+ $this->assertEquals(
+ bin2hex( UtfNormal::NFC( $char ) ),
+ bin2hex( $clean ),
+ "Triplet $x should be intact" );
+ }
+ } elseif ( $first > 0xc1 && $first < 0xe0 && $second < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( UtfNormal::NFC( $head . chr( $first ) . chr( $second ) ) . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Valid 2-byte $x + broken tail" );
+ } elseif ( $second > 0xc1 && $second < 0xe0 && $third < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UtfNormal::NFC( chr( $second ) . chr( $third ) . $tail ) ),
+ bin2hex( $clean ),
+ "Broken head + valid 2-byte $x" );
+ } elseif ( ( $first > 0xfd || $second > 0xfd ) &&
+ ( ( $second > 0xbf && $third > 0xbf ) ||
+ ( $second < 0xc0 && $third < 0xc0 ) ||
+ ( $second > 0xfd ) ||
+ ( $third > 0xfd ) )
+ ) {
+ # fe and ff are not legal head bytes -- expect three replacement chars
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ } elseif ( $first > 0xc2 && $second < 0xc0 && $third < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ } else {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ }
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ public function testChunkRegression() {
+ # Check for regression against a chunking bug
+ $text = "\x46\x55\xb8" .
+ "\xdc\x96" .
+ "\xee" .
+ "\xe7" .
+ "\x44" .
+ "\xaa" .
+ "\x2f\x25";
+ $expect = "\x46\x55\xef\xbf\xbd" .
+ "\xdc\x96" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x44" .
+ "\xef\xbf\xbd" .
+ "\x2f\x25";
+
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testInterposeRegression() {
+ $text = "\x4e\x30" .
+ "\xb1" . # bad tail
+ "\x3a" .
+ "\x92" . # bad tail
+ "\x62\x3a" .
+ "\x84" . # bad tail
+ "\x43" .
+ "\xc6" . # bad head
+ "\x3f" .
+ "\x92" . # bad tail
+ "\xad" . # bad tail
+ "\x7d" .
+ "\xd9\x95";
+
+ $expect = "\x4e\x30" .
+ "\xef\xbf\xbd" .
+ "\x3a" .
+ "\xef\xbf\xbd" .
+ "\x62\x3a" .
+ "\xef\xbf\xbd" .
+ "\x43" .
+ "\xef\xbf\xbd" .
+ "\x3f" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x7d" .
+ "\xd9\x95";
+
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testOverlongRegression() {
+ $text = "\x67" .
+ "\x1a" . # forbidden ascii
+ "\xea" . # bad head
+ "\xc1\xa6" . # overlong sequence
+ "\xad" . # bad tail
+ "\x1c" . # forbidden ascii
+ "\xb0" . # bad tail
+ "\x3c" .
+ "\x9e"; # bad tail
+ $expect = "\x67" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x3c" .
+ "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testSurrogateRegression() {
+ $text = "\xed\xb4\x96" . # surrogate 0xDD16
+ "\x83" . # bad tail
+ "\xb4" . # bad tail
+ "\xac"; # bad head
+ $expect = "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testBomRegression() {
+ $text = "\xef\xbf\xbe" . # U+FFFE, illegal char
+ "\xb2" . # bad tail
+ "\xef" . # bad head
+ "\x59";
+ $expect = "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x59";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testForbiddenRegression() {
+ $text = "\xef\xbf\xbf"; # U+FFFF, illegal char
+ $expect = "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testHangulRegression() {
+ $text = "\xed\x9c\xaf" . # Hangul char
+ "\xe1\x87\x81"; # followed by another final jamo
+ $expect = $text; # Should *not* change.
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+}
diff --git a/tests/phpunit/includes/objectcache/BagOStuffTest.php b/tests/phpunit/includes/objectcache/BagOStuffTest.php
new file mode 100644
index 00000000..aa783943
--- /dev/null
+++ b/tests/phpunit/includes/objectcache/BagOStuffTest.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * This class will test BagOStuff.
+ *
+ * @author Matthias Mullie <mmullie@wikimedia.org>
+ */
+class BagOStuffTest extends MediaWikiTestCase {
+ private $cache;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // type defined through parameter
+ if ( $this->getCliArg( 'use-bagostuff=' ) ) {
+ $name = $this->getCliArg( 'use-bagostuff=' );
+
+ $this->cache = ObjectCache::newFromId( $name );
+ } else {
+ // no type defined - use simple hash
+ $this->cache = new HashBagOStuff;
+ }
+
+ $this->cache->delete( wfMemcKey( 'test' ) );
+ }
+
+ protected function tearDown() {
+ }
+
+ public function testMerge() {
+ $key = wfMemcKey( 'test' );
+
+ $usleep = 0;
+
+ /**
+ * Callback method: append "merged" to whatever is in cache.
+ *
+ * @param BagOStuff $cache
+ * @param string $key
+ * @param int $existingValue
+ * @use int $usleep
+ * @return int
+ */
+ $callback = function ( BagOStuff $cache, $key, $existingValue ) use ( &$usleep ) {
+ // let's pretend this is an expensive callback to test concurrent merge attempts
+ usleep( $usleep );
+
+ if ( $existingValue === false ) {
+ return 'merged';
+ }
+
+ return $existingValue . 'merged';
+ };
+
+ // merge on non-existing value
+ $merged = $this->cache->merge( $key, $callback, 0 );
+ $this->assertTrue( $merged );
+ $this->assertEquals( $this->cache->get( $key ), 'merged' );
+
+ // merge on existing value
+ $merged = $this->cache->merge( $key, $callback, 0 );
+ $this->assertTrue( $merged );
+ $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' );
+
+ /*
+ * Test concurrent merges by forking this process, if:
+ * - not manually called with --use-bagostuff
+ * - pcntl_fork is supported by the system
+ * - cache type will correctly support calls over forks
+ */
+ $fork = (bool)$this->getCliArg( 'use-bagostuff=' );
+ $fork &= function_exists( 'pcntl_fork' );
+ $fork &= !$this->cache instanceof HashBagOStuff;
+ $fork &= !$this->cache instanceof EmptyBagOStuff;
+ $fork &= !$this->cache instanceof MultiWriteBagOStuff;
+ if ( $fork ) {
+ // callback should take awhile now so that we can test concurrent merge attempts
+ $usleep = 5000;
+
+ $pid = pcntl_fork();
+ if ( $pid == -1 ) {
+ // can't fork, ignore this test...
+ } elseif ( $pid ) {
+ // wait a little, making sure that the child process is calling merge
+ usleep( 3000 );
+
+ // attempt a merge - this should fail
+ $merged = $this->cache->merge( $key, $callback, 0, 1 );
+
+ // merge has failed because child process was merging (and we only attempted once)
+ $this->assertFalse( $merged );
+
+ // make sure the child's merge is completed and verify
+ usleep( 3000 );
+ $this->assertEquals( $this->cache->get( $key ), 'mergedmergedmerged' );
+ } else {
+ $this->cache->merge( $key, $callback, 0, 1 );
+
+ // Note: I'm not even going to check if the merge worked, I'll
+ // compare values in the parent process to test if this merge worked.
+ // I'm just going to exit this child process, since I don't want the
+ // child to output any test results (would be rather confusing to
+ // have test output twice)
+ exit;
+ }
+ }
+ }
+
+ public function testAdd() {
+ $key = wfMemcKey( 'test' );
+ $this->assertTrue( $this->cache->add( $key, 'test' ) );
+ }
+
+ public function testGet() {
+ $value = array( 'this' => 'is', 'a' => 'test' );
+
+ $key = wfMemcKey( 'test' );
+ $this->cache->add( $key, $value );
+ $this->assertEquals( $this->cache->get( $key ), $value );
+ }
+
+ /**
+ * @covers BagOStuff::incr
+ */
+ public function testIncr() {
+ $key = wfMemcKey( 'test' );
+ $this->cache->add( $key, 0 );
+ $this->cache->incr( $key );
+ $expectedValue = 1;
+ $actualValue = $this->cache->get( $key );
+ $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
+ }
+
+ public function testGetMulti() {
+ $value1 = array( 'this' => 'is', 'a' => 'test' );
+ $value2 = array( 'this' => 'is', 'another' => 'test' );
+
+ $key1 = wfMemcKey( 'test1' );
+ $key2 = wfMemcKey( 'test2' );
+
+ $this->cache->add( $key1, $value1 );
+ $this->cache->add( $key2, $value2 );
+
+ $this->assertEquals( $this->cache->getMulti( array( $key1, $key2 ) ), array( $key1 => $value1, $key2 => $value2 ) );
+
+ // cleanup
+ $this->cache->delete( $key1 );
+ $this->cache->delete( $key2 );
+ }
+}
diff --git a/tests/phpunit/includes/parser/MagicVariableTest.php b/tests/phpunit/includes/parser/MagicVariableTest.php
new file mode 100644
index 00000000..c2c97c01
--- /dev/null
+++ b/tests/phpunit/includes/parser/MagicVariableTest.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * This file is intended to test magic variables in the parser
+ * It was inspired by Raymond & Matěj Grabovský commenting about r66200
+ *
+ * As of february 2011, it only tests some revisions and date related
+ * magic variables.
+ *
+ * @author Antoine Musso
+ * @copyright Copyright © 2011, Antoine Musso
+ * @file
+ * @todo covers tags
+ */
+
+class MagicVariableTest extends MediaWikiTestCase {
+ /**
+ * @var Parser
+ */
+ private $testParser = null;
+
+ /**
+ * An array of magicword returned as type integer by the parser
+ * They are usually returned as a string for i18n since we support
+ * persan numbers for example, but some magic explicitly return
+ * them as integer.
+ * @see MagicVariableTest::assertMagic()
+ */
+ private $expectedAsInteger = array(
+ 'revisionday',
+ 'revisionmonth1',
+ );
+
+ /** setup a basic parser object */
+ protected function setUp() {
+ parent::setUp();
+
+ $contLang = Language::factory( 'en' );
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => $contLang,
+ ) );
+
+ $this->testParser = new Parser();
+ $this->testParser->Options( ParserOptions::newFromUserAndLang( new User, $contLang ) );
+
+ # initialize parser output
+ $this->testParser->clearState();
+
+ # Needs a title to do magic word stuff
+ $title = Title::newFromText( 'Tests' );
+ $title->mRedirect = false; # Else it needs a db connection just to check if it's a redirect (when deciding the page language)
+
+ $this->testParser->setTitle( $title );
+ }
+
+ /**
+ * @param int $num upper limit for numbers
+ * @return array of numbers from 1 up to $num
+ */
+ private static function createProviderUpTo( $num ) {
+ $ret = array();
+ for ( $i = 1; $i <= $num; $i++ ) {
+ $ret[] = array( $i );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @return array of months numbers (as an integer)
+ */
+ public static function provideMonths() {
+ return self::createProviderUpTo( 12 );
+ }
+
+ /**
+ * @return array of days numbers (as an integer)
+ */
+ public static function provideDays() {
+ return self::createProviderUpTo( 31 );
+ }
+
+ ############### TESTS #############################################
+ # @todo FIXME:
+ # - those got copy pasted, we can probably make them cleaner
+ # - tests are lacking useful messages
+
+ # day
+
+ /** @dataProvider provideDays */
+ public function testCurrentdayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'currentday', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testCurrentdaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'currentday2', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testLocaldayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'localday', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testLocaldaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'localday2', $day );
+ }
+
+ # month
+
+ /** @dataProvider provideMonths */
+ public function testCurrentmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'currentmonth', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testCurrentmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'currentmonth1', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testLocalmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'localmonth', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testLocalmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'localmonth1', $month );
+ }
+
+ # revision day
+
+ /** @dataProvider provideDays */
+ public function testRevisiondayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'revisionday', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testRevisiondaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'revisionday2', $day );
+ }
+
+ # revision month
+
+ /** @dataProvider provideMonths */
+ public function testRevisionmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'revisionmonth', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testRevisionmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'revisionmonth1', $month );
+ }
+
+ /**
+ * Rough tests for {{SERVERNAME}} magic word
+ * Bug 31176
+ * @group Database
+ * @dataProvider provideDataServernameFromDifferentProtocols
+ */
+ public function testServernameFromDifferentProtocols( $server ) {
+ $this->setMwGlobals( 'wgServer', $server );
+
+ $this->assertMagic( 'localhost', 'servername' );
+ }
+
+ public static function provideDataServernameFromDifferentProtocols() {
+ return array(
+ array( 'http://localhost/' ),
+ array( 'https://localhost/' ),
+ array( '//localhost/' ), # bug 31176
+ );
+ }
+
+ ############### HELPERS ############################################
+
+ /** assertion helper expecting a magic output which is zero padded */
+ public function assertZeroPadded( $magic, $value ) {
+ $this->assertMagicPadding( $magic, $value, '%02d' );
+ }
+
+ /** assertion helper expecting a magic output which is unpadded */
+ public function assertUnPadded( $magic, $value ) {
+ $this->assertMagicPadding( $magic, $value, '%d' );
+ }
+
+ /**
+ * Main assertion helper for magic variables padding
+ * @param $magic string Magic variable name
+ * @param $value mixed Month or day
+ * @param $format string sprintf format for $value
+ */
+ private function assertMagicPadding( $magic, $value, $format ) {
+ # Initialize parser timestamp as year 2010 at 12h34 56s.
+ # month and day are given by the caller ($value). Month < 12!
+ if ( $value > 12 ) {
+ $month = $value % 12;
+ } else {
+ $month = $value;
+ }
+
+ $this->setParserTS(
+ sprintf( '2010%02d%02d123456', $month, $value )
+ );
+
+ # please keep the following commented line of code. It helps debugging.
+ //print "\nDEBUG (value $value):" . sprintf( '2010%02d%02d123456', $value, $value ) . "\n";
+
+ # format expectation and test it
+ $expected = sprintf( $format, $value );
+ $this->assertMagic( $expected, $magic );
+ }
+
+ /** helper to set the parser timestamp and revision timestamp */
+ private function setParserTS( $ts ) {
+ $this->testParser->Options()->setTimestamp( $ts );
+ $this->testParser->mRevisionTimestamp = $ts;
+ }
+
+ /**
+ * Assertion helper to test a magic variable output
+ */
+ private function assertMagic( $expected, $magic ) {
+ if ( in_array( $magic, $this->expectedAsInteger ) ) {
+ $expected = (int)$expected;
+ }
+
+ # Generate a message for the assertion
+ $msg = sprintf( "Magic %s should be <%s:%s>",
+ $magic,
+ $expected,
+ gettype( $expected )
+ );
+
+ $this->assertSame(
+ $expected,
+ $this->testParser->getVariableValue( $magic ),
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php
new file mode 100644
index 00000000..c120ca34
--- /dev/null
+++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php
@@ -0,0 +1,120 @@
+<?php
+require_once __DIR__ . '/NewParserTest.php';
+
+/**
+ * The UnitTest must be either a class that inherits from MediaWikiTestCase
+ * or a class that provides a public static suite() method which returns
+ * an PHPUnit_Framework_Test object
+ *
+ * @group Parser
+ * @group Database
+ */
+class MediaWikiParserTest {
+
+ /**
+ * @defgroup filtering_constants Filtering constants
+ *
+ * Limit inclusion of parser tests files coming from MediaWiki core
+ * @{
+ */
+
+ /** Include files shipped with MediaWiki core */
+ const CORE_ONLY = 1;
+ /** Include non core files as set in $wgParserTestFiles */
+ const NO_CORE = 2;
+ /** Include anything set via $wgParserTestFiles */
+ const WITH_ALL = 3; # CORE_ONLY | NO_CORE
+
+ /** @} */
+
+ /**
+ * Get a PHPUnit test suite of parser tests. Optionally filtered with
+ * $flags.
+ *
+ * @par Examples:
+ * Get a suite of parser tests shipped by MediaWiki core:
+ * @code
+ * MediaWikiParserTest::suite( MediaWikiParserTest::CORE_ONLY );
+ * @endcode
+ * Get a suite of various parser tests, like extensions:
+ * @code
+ * MediaWikiParserTest::suite( MediaWikiParserTest::NO_CORE );
+ * @endcode
+ * Get any test defined via $wgParserTestFiles:
+ * @code
+ * MediaWikiParserTest::suite( MediaWikiParserTest::WITH_ALL );
+ * @endcode
+ *
+ * @param $flags bitwise flag to filter out the $wgParserTestFiles that
+ * will be included. Default: MediaWikiParserTest::CORE_ONLY
+ *
+ * @return PHPUnit_Framework_TestSuite
+ */
+ public static function suite( $flags = self::CORE_ONLY ) {
+ if ( is_string( $flags ) ) {
+ $flags = self::CORE_ONLY;
+ }
+ global $wgParserTestFiles, $IP;
+
+ $mwTestDir = $IP . '/tests/';
+
+ # Human friendly helpers
+ $wantsCore = ( $flags & self::CORE_ONLY );
+ $wantsRest = ( $flags & self::NO_CORE );
+
+ # Will hold the .txt parser test files we will include
+ $filesToTest = array();
+
+ # Filter out .txt files
+ foreach ( $wgParserTestFiles as $parserTestFile ) {
+ $isCore = ( 0 === strpos( $parserTestFile, $mwTestDir ) );
+
+ if ( $isCore && $wantsCore ) {
+ self::debug( "included core parser tests: $parserTestFile" );
+ $filesToTest[] = $parserTestFile;
+ } elseif ( !$isCore && $wantsRest ) {
+ self::debug( "included non core parser tests: $parserTestFile" );
+ $filesToTest[] = $parserTestFile;
+ } else {
+ self::debug( "skipped parser tests: $parserTestFile" );
+ }
+ }
+ self::debug( 'parser tests files: '
+ . implode( ' ', $filesToTest ) );
+
+ $suite = new PHPUnit_Framework_TestSuite;
+ foreach ( $filesToTest as $fileName ) {
+ $testsName = basename( $fileName, '.txt' );
+ $escapedFileName = strtr( $fileName, array( "'" => "\\'", '\\' => '\\\\' ) );
+ /* This used to be ucfirst( basename( dirname( $filename ) ) )
+ * and then was ucfirst( basename( $filename, '.txt' )
+ * but that didn't work with names like foo.tests.txt
+ */
+ $parserTestClassName = str_replace( '.', '_', ucfirst( $testsName ) );
+ $parserTestClassDefinition = <<<EOT
+/**
+ * @group Database
+ * @group Parser
+ * @group ParserTests
+ * @group ParserTests_$parserTestClassName
+ */
+class $parserTestClassName extends NewParserTest {
+ protected \$file = '$escapedFileName';
+}
+EOT;
+
+ eval( $parserTestClassDefinition );
+ self::debug( "Adding test class $parserTestClassName" );
+ $suite->addTestSuite( $parserTestClassName );
+ }
+ return $suite;
+ }
+
+ /**
+ * Write $msg under log group 'tests-parser'
+ * @param string $msg Message to log
+ */
+ protected static function debug( $msg ) {
+ return wfDebugLog( 'tests-parser', wfGetCaller() . ' ' . $msg );
+ }
+}
diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php
new file mode 100644
index 00000000..eac4de5c
--- /dev/null
+++ b/tests/phpunit/includes/parser/NewParserTest.php
@@ -0,0 +1,969 @@
+<?php
+
+/**
+ * Although marked as a stub, can work independently.
+ *
+ * @group Database
+ * @group Parser
+ * @group Stub
+ *
+ * @todo covers tags
+ */
+class NewParserTest extends MediaWikiTestCase {
+ static protected $articles = array(); // Array of test articles defined by the tests
+ /* The data provider is run on a different instance than the test, so it must be static
+ * When running tests from several files, all tests will see all articles.
+ */
+ static protected $backendToUse;
+
+ public $keepUploads = false;
+ public $runDisabled = false;
+ public $runParsoid = false;
+ public $regex = '';
+ public $showProgress = true;
+ public $savedWeirdGlobals = array();
+ public $savedGlobals = array();
+ public $hooks = array();
+ public $functionHooks = array();
+
+ //Fuzz test
+ public $maxFuzzTestLength = 300;
+ public $fuzzSeed = 0;
+ public $memoryLimit = 50;
+
+ protected $file = false;
+
+ public static function setUpBeforeClass() {
+ // Inject ParserTest well-known interwikis
+ ParserTest::setupInterwikis();
+ }
+
+ protected function setUp() {
+ global $wgNamespaceAliases, $wgContLang;
+ global $wgHooks, $IP;
+
+ parent::setUp();
+
+ //Setup CLI arguments
+ if ( $this->getCliArg( 'regex=' ) ) {
+ $this->regex = $this->getCliArg( 'regex=' );
+ } else {
+ # Matches anything
+ $this->regex = '';
+ }
+
+ $this->keepUploads = $this->getCliArg( 'keep-uploads' );
+
+ $tmpGlobals = array();
+
+ $tmpGlobals['wgLanguageCode'] = 'en';
+ $tmpGlobals['wgContLang'] = Language::factory( 'en' );
+ $tmpGlobals['wgSitename'] = 'MediaWiki';
+ $tmpGlobals['wgServer'] = 'http://example.org';
+ $tmpGlobals['wgScript'] = '/index.php';
+ $tmpGlobals['wgScriptPath'] = '/';
+ $tmpGlobals['wgArticlePath'] = '/wiki/$1';
+ $tmpGlobals['wgActionPaths'] = array();
+ $tmpGlobals['wgVariantArticlePath'] = false;
+ $tmpGlobals['wgExtensionAssetsPath'] = '/extensions';
+ $tmpGlobals['wgStylePath'] = '/skins';
+ $tmpGlobals['wgEnableUploads'] = true;
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgLocalFileRepo'] = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => 'local-backend'
+ );
+ $tmpGlobals['wgForeignFileRepos'] = array();
+ $tmpGlobals['wgDefaultExternalStore'] = array();
+ $tmpGlobals['wgEnableParserCache'] = false;
+ $tmpGlobals['wgCapitalLinks'] = true;
+ $tmpGlobals['wgNoFollowLinks'] = true;
+ $tmpGlobals['wgNoFollowDomainExceptions'] = array();
+ $tmpGlobals['wgExternalLinkTarget'] = false;
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgUseImageResize'] = true;
+ $tmpGlobals['wgAllowExternalImages'] = true;
+ $tmpGlobals['wgRawHtml'] = false;
+ $tmpGlobals['wgUseTidy'] = false;
+ $tmpGlobals['wgAlwaysUseTidy'] = false;
+ $tmpGlobals['wgWellFormedXml'] = true;
+ $tmpGlobals['wgAllowMicrodataAttributes'] = true;
+ $tmpGlobals['wgExperimentalHtmlIds'] = false;
+ $tmpGlobals['wgAdaptiveMessageCache'] = true;
+ $tmpGlobals['wgUseDatabaseMessages'] = true;
+ $tmpGlobals['wgLocaltimezone'] = 'UTC';
+ $tmpGlobals['wgDeferredUpdateList'] = array();
+ $tmpGlobals['wgGroupPermissions'] = array(
+ '*' => array(
+ 'createaccount' => true,
+ 'read' => true,
+ 'edit' => true,
+ 'createpage' => true,
+ 'createtalk' => true,
+ ) );
+ $tmpGlobals['wgNamespaceProtection'] = array( NS_MEDIAWIKI => 'editinterface' );
+
+ $tmpGlobals['wgParser'] = new StubObject(
+ 'wgParser', $GLOBALS['wgParserConf']['class'],
+ array( $GLOBALS['wgParserConf'] ) );
+
+ $tmpGlobals['wgFileExtensions'][] = 'svg';
+ $tmpGlobals['wgSVGConverter'] = 'rsvg';
+ $tmpGlobals['wgSVGConverters']['rsvg'] =
+ '$path/rsvg-convert -w $width -h $height $input -o $output';
+
+ if ( $GLOBALS['wgStyleDirectory'] === false ) {
+ $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
+ }
+
+ # Replace all media handlers with a mock. We do not need to generate
+ # actual thumbnails to do parser testing, we only care about receiving
+ # a ThumbnailImage properly initialized.
+ global $wgMediaHandlers;
+ foreach ( $wgMediaHandlers as $type => $handler ) {
+ $tmpGlobals['wgMediaHandlers'][$type] = 'MockBitmapHandler';
+ }
+ // Vector images have to be handled slightly differently
+ $tmpGlobals['wgMediaHandlers']['image/svg+xml'] = 'MockSvgHandler';
+
+ $tmpHooks = $wgHooks;
+ $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
+ $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
+ $tmpGlobals['wgHooks'] = $tmpHooks;
+ # add a namespace shadowing a interwiki link, to test
+ # proper precedence when resolving links. (bug 51680)
+ $tmpGlobals['wgExtraNamespaces'] = array( 100 => 'MemoryAlpha' );
+
+ $this->setMwGlobals( $tmpGlobals );
+
+ $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
+ $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
+
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ protected function tearDown() {
+ global $wgNamespaceAliases, $wgContLang;
+
+ $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
+ $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
+
+ // Restore backends
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+
+ // Remove temporary pages from the link cache
+ LinkCache::singleton()->clear();
+
+ // Restore message cache (temporary pages and $wgUseDatabaseMessages)
+ MessageCache::destroyInstance();
+
+ parent::tearDown();
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ public static function tearDownAfterClass() {
+ ParserTest::tearDownInterwikis();
+ parent::tearDownAfterClass();
+ }
+
+ function addDBData() {
+ $this->tablesUsed[] = 'site_stats';
+ # disabled for performance
+ #$this->tablesUsed[] = 'image';
+
+ # Update certain things in site_stats
+ $this->db->insert( 'site_stats',
+ array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
+ __METHOD__
+ );
+
+ $user = User::newFromId( 0 );
+ LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
+
+ # Upload DB table entries for files.
+ # We will upload the actual files later. Note that if anything causes LocalFile::load()
+ # to be triggered before then, it will break via maybeUpgrade() setting the fileExists
+ # member to false and storing it in cache.
+ # note that the size/width/height/bits/etc of the file
+ # are actually set by inspecting the file itself; the arguments
+ # to recordUpload2 have no effect. That said, we try to make things
+ # match up so it is less confusing to readers of the code & tests.
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2(
+ '', // archive name
+ 'Upload of some lame file',
+ 'Some lame file',
+ array(
+ 'size' => 7881,
+ 'width' => 1941,
+ 'height' => 220,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '1', 16, 36, 31 ),
+ 'fileExists' => true ),
+ $this->db->timestamp( '20010115123500' ), $user
+ );
+ }
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2(
+ '', // archive name
+ 'Upload of some lame thumbnail',
+ 'Some lame thumbnail',
+ array(
+ 'size' => 22589,
+ 'width' => 135,
+ 'height' => 135,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/png',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '2', 16, 36, 31 ),
+ 'fileExists' => true ),
+ $this->db->timestamp( '20130225203040' ), $user
+ );
+ }
+
+ # This image will be blacklisted in [[MediaWiki:Bad image list]]
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2(
+ '', // archive name
+ 'zomgnotcensored',
+ 'Borderline image',
+ array(
+ 'size' => 12345,
+ 'width' => 320,
+ 'height' => 240,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '3', 16, 36, 31 ),
+ 'fileExists' => true ),
+ $this->db->timestamp( '20010115123500' ), $user
+ );
+ }
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', array(
+ 'size' => 12345,
+ 'width' => 200,
+ 'height' => 200,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_DRAWING,
+ 'mime' => 'image/svg+xml',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+ }
+ }
+
+ //ParserTest setup/teardown functions
+
+ /**
+ * Set up the global variables for a consistent environment for each test.
+ * Ideally this should replace the global configuration entirely.
+ */
+ protected function setupGlobals( $opts = array(), $config = '' ) {
+ global $wgFileBackends;
+ # Find out values for some special options.
+ $lang =
+ self::getOptionValue( 'language', $opts, 'en' );
+ $variant =
+ self::getOptionValue( 'variant', $opts, false );
+ $maxtoclevel =
+ self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
+ $linkHolderBatchSize =
+ self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
+
+ $uploadDir = $this->getUploadDir();
+ if ( $this->getCliArg( 'use-filebackend=' ) ) {
+ if ( self::$backendToUse ) {
+ $backend = self::$backendToUse;
+ } else {
+ $name = $this->getCliArg( 'use-filebackend=' );
+ $useConfig = array();
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] == $name ) {
+ $useConfig = $conf;
+ }
+ }
+ $useConfig['name'] = 'local-backend'; // swap name
+ $class = $conf['class'];
+ self::$backendToUse = new $class( $useConfig );
+ $backend = self::$backendToUse;
+ }
+ } else {
+ # Replace with a mock. We do not care about generating real
+ # files on the filesystem, just need to expose the file
+ # informations.
+ $backend = new MockFileBackend( array(
+ 'name' => 'local-backend',
+ 'lockManager' => 'nullLockManager',
+ 'containerPaths' => array(
+ 'local-public' => "$uploadDir",
+ 'local-thumb' => "$uploadDir/thumb",
+ )
+ ) );
+ }
+
+ $settings = array(
+ 'wgLocalFileRepo' => array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => $backend
+ ),
+ 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
+ 'wgLanguageCode' => $lang,
+ 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_',
+ 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
+ 'wgNamespacesWithSubpages' => array( NS_MAIN => isset( $opts['subpage'] ) ),
+ 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
+ 'wgMaxTocLevel' => $maxtoclevel,
+ 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ),
+ 'wgMathDirectory' => $uploadDir . '/math',
+ 'wgDefaultLanguageVariant' => $variant,
+ 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
+ );
+
+ if ( $config ) {
+ $configLines = explode( "\n", $config );
+
+ foreach ( $configLines as $line ) {
+ list( $var, $value ) = explode( '=', $line, 2 );
+
+ $settings[$var] = eval( "return $value;" ); //???
+ }
+ }
+
+ $this->savedGlobals = array();
+
+ /** @since 1.20 */
+ wfRunHooks( 'ParserTestGlobals', array( &$settings ) );
+
+ $langObj = Language::factory( $lang );
+ $settings['wgContLang'] = $langObj;
+ $settings['wgLang'] = $langObj;
+
+ $context = new RequestContext();
+ $settings['wgOut'] = $context->getOutput();
+ $settings['wgUser'] = $context->getUser();
+ $settings['wgRequest'] = $context->getRequest();
+
+ foreach ( $settings as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ MagicWord::clearCache();
+
+ # The entries saved into RepoGroup cache with previous globals will be wrong.
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+
+ # Create dummy files in storage
+ $this->setupUploads();
+
+ # Publish the articles after we have the final language set
+ $this->publishTestArticles();
+
+ MessageCache::destroyInstance();
+
+ return $context;
+ }
+
+ /**
+ * Get an FS upload directory (only applies to FSFileBackend)
+ *
+ * @return String: the directory
+ */
+ protected function getUploadDir() {
+ 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;
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return String: the directory
+ */
+ protected function setupUploads() {
+ global $IP;
+
+ $base = $this->getBaseDir();
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ $backend->prepare( array( 'dir' => "$base/local-public/3/3a" ) );
+ $backend->store( array(
+ 'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/3/3a/Foobar.jpg"
+ ) );
+ $backend->prepare( array( 'dir' => "$base/local-public/e/ea" ) );
+ $backend->store( array(
+ 'src' => "$IP/skins/monobook/wiki.png", 'dst' => "$base/local-public/e/ea/Thumb.png"
+ ) );
+ $backend->prepare( array( 'dir' => "$base/local-public/0/09" ) );
+ $backend->store( array(
+ 'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/0/09/Bad.jpg"
+ ) );
+
+ // No helpful SVG file to copy, so make one ourselves
+ $tmpDir = wfTempDir();
+ $tempFsFile = new TempFSFile( "$tmpDir/Foobar.svg" );
+ $tempFsFile->autocollect(); // destroy file when $tempFsFile leaves scope
+ file_put_contents( "$tmpDir/Foobar.svg",
+ '<?xml version="1.0" encoding="utf-8"?>' .
+ '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><text>Foo</text></svg>' );
+
+ $backend->prepare( array( 'dir' => "$base/local-public/f/ff" ) );
+ $backend->quickStore( array(
+ 'src' => "$tmpDir/Foobar.svg", 'dst' => "$base/local-public/f/ff/Foobar.svg"
+ ) );
+ }
+
+ /**
+ * Restore default values and perform any necessary clean-up
+ * after each test runs.
+ */
+ protected function teardownGlobals() {
+ $this->teardownUploads();
+
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+ }
+
+ /**
+ * Remove the dummy uploads directory
+ */
+ private function teardownUploads() {
+ if ( $this->keepUploads ) {
+ return;
+ }
+
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ if ( $backend instanceof MockFileBackend ) {
+ # In memory backend, so dont bother cleaning them up.
+ return;
+ }
+
+ $base = $this->getBaseDir();
+ // delete the files first, then the dirs.
+ self::deleteFiles(
+ array(
+ "$base/local-public/3/3a/Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg",
+
+ "$base/local-public/e/ea/Thumb.png",
+
+ "$base/local-public/0/09/Bad.jpg",
+
+ "$base/local-public/f/ff/Foobar.svg",
+ "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.jpg",
+ "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.jpg",
+ "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.jpg",
+ "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.jpg",
+ "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.jpg",
+ "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.jpg",
+
+ "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
+ )
+ );
+ }
+
+ /**
+ * Delete the specified files, if they exist.
+ * @param $files Array: full paths to files to delete.
+ */
+ private static function deleteFiles( $files ) {
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ foreach ( $files as $file ) {
+ $backend->delete( array( 'src' => $file ), array( 'force' => 1 ) );
+ }
+ foreach ( $files as $file ) {
+ $tmp = $file;
+ while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
+ if ( !$backend->clean( array( 'dir' => $tmp ) )->isOK() ) {
+ break;
+ }
+ }
+ }
+ }
+
+ protected function getBaseDir() {
+ return 'mwstore://local-backend';
+ }
+
+ public function parserTestProvider() {
+ if ( $this->file === false ) {
+ global $wgParserTestFiles;
+ $this->file = $wgParserTestFiles[0];
+ }
+
+ return new TestFileIterator( $this->file, $this );
+ }
+
+ /**
+ * Set the file from whose tests will be run by this instance
+ */
+ public function setParserTestFile( $filename ) {
+ $this->file = $filename;
+ }
+
+ /**
+ * @group medium
+ * @dataProvider parserTestProvider
+ */
+ public function testParserTest( $desc, $input, $result, $opts, $config ) {
+ if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) {
+ $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions"
+ //$this->markTestSkipped( 'Filtered out by the user' );
+ return;
+ }
+
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ // parser tests frequently assume that the main namespace contains wikitext.
+ // @todo When setting up pages, force the content model. Only skip if
+ // $wgtContentModelUseDB is false.
+ $this->markTestSkipped( "Main namespace does not support wikitext,"
+ . "skipping parser test: $desc" );
+ }
+
+ wfDebug( "Running parser test: $desc\n" );
+
+ $opts = $this->parseOptions( $opts );
+ $context = $this->setupGlobals( $opts, $config );
+
+ $user = $context->getUser();
+ $options = ParserOptions::newFromContext( $context );
+
+ if ( isset( $opts['title'] ) ) {
+ $titleText = $opts['title'];
+ } else {
+ $titleText = 'Parser test';
+ }
+
+ $local = isset( $opts['local'] );
+ $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
+ $parser = $this->getParser( $preprocessor );
+
+ $title = Title::newFromText( $titleText );
+
+ # Parser test requiring math. Make sure texvc is executable
+ # or just skip such tests.
+ if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) {
+ global $wgTexvc;
+
+ if ( !isset( $wgTexvc ) ) {
+ $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" );
+ } elseif ( !is_executable( $wgTexvc ) ) {
+ $this->markTestSkipped( "SKIPPED: texvc binary does not exist"
+ . " or is not executable.\n"
+ . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" );
+ }
+ }
+
+ if ( isset( $opts['pst'] ) ) {
+ $out = $parser->preSaveTransform( $input, $title, $user, $options );
+ } elseif ( isset( $opts['msg'] ) ) {
+ $out = $parser->transformMsg( $input, $options, $title );
+ } elseif ( isset( $opts['section'] ) ) {
+ $section = $opts['section'];
+ $out = $parser->getSection( $input, $section );
+ } elseif ( isset( $opts['replace'] ) ) {
+ $section = $opts['replace'][0];
+ $replace = $opts['replace'][1];
+ $out = $parser->replaceSection( $input, $section, $replace );
+ } elseif ( isset( $opts['comment'] ) ) {
+ $out = Linker::formatComment( $input, $title, $local );
+ } elseif ( isset( $opts['preload'] ) ) {
+ $out = $parser->getPreloadText( $input, $title, $options );
+ } else {
+ $output = $parser->parse( $input, $title, $options, true, true, 1337 );
+ $output->setTOCEnabled( !isset( $opts['notoc'] ) );
+ $out = $output->getText();
+
+ if ( isset( $opts['showtitle'] ) ) {
+ if ( $output->getTitleText() ) {
+ $title = $output->getTitleText();
+ }
+
+ $out = "$title\n$out";
+ }
+
+ if ( isset( $opts['ill'] ) ) {
+ $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
+ } elseif ( isset( $opts['cat'] ) ) {
+ $outputPage = $context->getOutput();
+ $outputPage->addCategoryLinks( $output->getCategories() );
+ $cats = $outputPage->getCategoryLinks();
+
+ if ( isset( $cats['normal'] ) ) {
+ $out = $this->tidy( implode( ' ', $cats['normal'] ) );
+ } else {
+ $out = '';
+ }
+ }
+ $parser->mPreprocessor = null;
+
+ $result = $this->tidy( $result );
+ }
+
+ $this->teardownGlobals();
+
+ $this->assertEquals( $result, $out, $desc );
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ *
+ * @todo fixme Needs some work to not eat memory until the world explodes
+ *
+ * @group ParserFuzz
+ */
+ public function testFuzzTests() {
+ global $wgParserTestFiles;
+
+ $files = $wgParserTestFiles;
+
+ if ( $this->getCliArg( 'file=' ) ) {
+ $files = array( $this->getCliArg( 'file=' ) );
+ }
+
+ $dict = $this->getFuzzInput( $files );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+
+ ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
+
+ $user = new User;
+ $opts = ParserOptions::newFromUser( $user );
+ $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+ $id = 1;
+
+ while ( true ) {
+
+ // Generate test input
+ mt_srand( ++$this->fuzzSeed );
+ $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+ $input = '';
+
+ while ( strlen( $input ) < $totalLength ) {
+ $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+ $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+ $offset = mt_rand( 0, $dictSize - $hairLength );
+ $input .= substr( $dict, $offset, $hairLength );
+ }
+
+ $this->setupGlobals();
+ $parser = $this->getParser();
+
+ // Run the test
+ try {
+ $parser->parse( $input, $title, $opts );
+ $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
+ } catch ( Exception $exception ) {
+ $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
+
+ $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\n" .
+ "Input: $input_dump\n\nError: {$exception->getMessage()}\n\n" .
+ "Backtrace: {$exception->getTraceAsString()}" );
+ }
+
+ $this->teardownGlobals();
+ $parser->__destruct();
+
+ if ( $id % 100 == 0 ) {
+ $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+ //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+ if ( $usage > 90 ) {
+ $ret = "Out of memory:\n";
+ $memStats = $this->getMemoryBreakdown();
+
+ foreach ( $memStats as $name => $usage ) {
+ $ret .= "$name: $usage\n";
+ }
+
+ throw new MWException( $ret );
+ }
+ }
+
+ $id++;
+ }
+ }
+
+ //Various getter functions
+
+ /**
+ * Get an input dictionary from a set of parser test files
+ */
+ function getFuzzInput( $filenames ) {
+ $dict = '';
+
+ foreach ( $filenames as $filename ) {
+ $contents = file_get_contents( $filename );
+ preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
+
+ foreach ( $matches[1] as $match ) {
+ $dict .= $match . "\n";
+ }
+ }
+
+ return $dict;
+ }
+
+ /**
+ * Get a memory usage breakdown
+ */
+ function getMemoryBreakdown() {
+ $memStats = array();
+
+ foreach ( $GLOBALS as $name => $value ) {
+ $memStats['$' . $name] = strlen( serialize( $value ) );
+ }
+
+ $classes = get_declared_classes();
+
+ foreach ( $classes as $class ) {
+ $rc = new ReflectionClass( $class );
+ $props = $rc->getStaticProperties();
+ $memStats[$class] = strlen( serialize( $props ) );
+ $methods = $rc->getMethods();
+
+ foreach ( $methods as $method ) {
+ $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
+ }
+ }
+
+ $functions = get_defined_functions();
+
+ foreach ( $functions['user'] as $function ) {
+ $rf = new ReflectionFunction( $function );
+ $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
+ }
+
+ asort( $memStats );
+
+ return $memStats;
+ }
+
+ /**
+ * Get a Parser object
+ */
+ function getParser( $preprocessor = null ) {
+ global $wgParserConf;
+
+ $class = $wgParserConf['class'];
+ $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
+
+ wfRunHooks( 'ParserTestParser', array( &$parser ) );
+
+ return $parser;
+ }
+
+ //Various action functions
+
+ public function addArticle( $name, $text, $line ) {
+ self::$articles[$name] = array( $text, $line );
+ }
+
+ public function publishTestArticles() {
+ if ( empty( self::$articles ) ) {
+ return;
+ }
+
+ foreach ( self::$articles as $name => $info ) {
+ list( $text, $line ) = $info;
+ ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' );
+ }
+ }
+
+ /**
+ * Steal a callback function from the primary parser, save it for
+ * application to our scary parser. If the hook is not installed,
+ * abort processing of this file.
+ *
+ * @param $name String
+ * @return Bool true if tag hook is present
+ */
+ public function requireHook( $name ) {
+ global $wgParser;
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+ return isset( $wgParser->mTagHooks[$name] );
+ }
+
+ public function requireFunctionHook( $name ) {
+ global $wgParser;
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+ return isset( $wgParser->mFunctionHooks[$name] );
+ }
+
+ //Various "cleanup" functions
+
+ /**
+ * Run the "tidy" command on text if the $wgUseTidy
+ * global is true
+ *
+ * @param $text String: the text to tidy
+ * @return String
+ */
+ protected function tidy( $text ) {
+ global $wgUseTidy;
+
+ if ( $wgUseTidy ) {
+ $text = MWTidy::tidy( $text );
+ }
+
+ return $text;
+ }
+
+ /**
+ * Remove last character if it is a newline
+ */
+ public function removeEndingNewline( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ } else {
+ return $s;
+ }
+ }
+
+ //Test options parser functions
+
+ protected function parseOptions( $instring ) {
+ $opts = array();
+ // foo
+ // foo=bar
+ // foo="bar baz"
+ // foo=[[bar baz]]
+ // foo=bar,"baz quux"
+ $regex = '/\b
+ ([\w-]+) # Key
+ \b
+ (?:\s*
+ = # First sub-value
+ \s*
+ (
+ "
+ [^"]* # Quoted val
+ "
+ |
+ \[\[
+ [^]]* # Link target
+ \]\]
+ |
+ [\w-]+ # Plain word
+ )
+ (?:\s*
+ , # Sub-vals 1..N
+ \s*
+ (
+ "[^"]*" # Quoted val
+ |
+ \[\[[^]]*\]\] # Link target
+ |
+ [\w-]+ # Plain word
+ )
+ )*
+ )?
+ /x';
+
+ if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
+ foreach ( $matches as $bits ) {
+ array_shift( $bits );
+ $key = strtolower( array_shift( $bits ) );
+ if ( count( $bits ) == 0 ) {
+ $opts[$key] = true;
+ } elseif ( count( $bits ) == 1 ) {
+ $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
+ } else {
+ // Array!
+ $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
+ }
+ }
+ }
+
+ return $opts;
+ }
+
+ protected function cleanupOption( $opt ) {
+ if ( substr( $opt, 0, 1 ) == '"' ) {
+ return substr( $opt, 1, -1 );
+ }
+
+ if ( substr( $opt, 0, 2 ) == '[[' ) {
+ return substr( $opt, 2, -2 );
+ }
+
+ return $opt;
+ }
+
+ /**
+ * Use a regex to find out the value of an option
+ * @param $key String: name of option val to retrieve
+ * @param $opts Options array to look in
+ * @param $default Mixed: default value returned if not found
+ */
+ protected static function getOptionValue( $key, $opts, $default ) {
+ $key = strtolower( $key );
+
+ if ( isset( $opts[$key] ) ) {
+ return $opts[$key];
+ } else {
+ return $default;
+ }
+ }
+}
diff --git a/tests/phpunit/includes/parser/ParserMethodsTest.php b/tests/phpunit/includes/parser/ParserMethodsTest.php
new file mode 100644
index 00000000..e5c5cb21
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserMethodsTest.php
@@ -0,0 +1,95 @@
+<?php
+
+class ParserMethodsTest extends MediaWikiLangTestCase {
+
+ public static function providePreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providePreSaveTransform
+ * @covers Parser::preSaveTransform
+ */
+ public function testPreSaveTransform( $text, $expected ) {
+ global $wgParser;
+
+ $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
+ $user = new User();
+ $user->setName( "127.0.0.1" );
+ $popts = ParserOptions::newFromUser( $user );
+ $text = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ /**
+ * @covers Parser::callParserFunction
+ */
+ public function testCallParserFunction() {
+ global $wgParser;
+
+ // Normal parses test passing PPNodes. Test passing an array.
+ $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
+ $wgParser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML );
+ $frame = $wgParser->getPreprocessor()->newFrame();
+ $ret = $wgParser->callParserFunction( $frame, '#tag',
+ array( 'pre', 'foo', 'style' => 'margin-left: 1.6em' )
+ );
+ $ret['text'] = $wgParser->mStripState->unstripBoth( $ret['text'] );
+ $this->assertSame( array(
+ 'found' => true,
+ 'text' => '<pre style="margin-left: 1.6em">foo</pre>',
+ ), $ret, 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' );
+ }
+
+ /**
+ * @covers Parser::parse
+ * @covers ParserOutput::getSections
+ */
+ public function testGetSections() {
+ global $wgParser;
+
+ $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
+ $out = $wgParser->parse( "==foo==\n<h2>bar</h2>\n==baz==\n", $title, new ParserOptions() );
+ $this->assertSame( array(
+ array(
+ 'toclevel' => 1,
+ 'level' => '2',
+ 'line' => 'foo',
+ 'number' => '1',
+ 'index' => '1',
+ 'fromtitle' => $title->getPrefixedDBkey(),
+ 'byteoffset' => 0,
+ 'anchor' => 'foo',
+ ),
+ array(
+ 'toclevel' => 1,
+ 'level' => '2',
+ 'line' => 'bar',
+ 'number' => '2',
+ 'index' => '',
+ 'fromtitle' => false,
+ 'byteoffset' => null,
+ 'anchor' => 'bar',
+ ),
+ array(
+ 'toclevel' => 1,
+ 'level' => '2',
+ 'line' => 'baz',
+ 'number' => '3',
+ 'index' => '2',
+ 'fromtitle' => $title->getPrefixedDBkey(),
+ 'byteoffset' => 21,
+ 'anchor' => 'baz',
+ ),
+ ), $out->getSections(), 'getSections() with proper value when <h2> is used' );
+ }
+ //@Todo Add tests for cleanSig() / cleanSigInSig(), getSection(), replaceSection(), getPreloadText()
+}
diff --git a/tests/phpunit/includes/parser/ParserOutputTest.php b/tests/phpunit/includes/parser/ParserOutputTest.php
new file mode 100644
index 00000000..c73666da
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserOutputTest.php
@@ -0,0 +1,59 @@
+<?php
+
+class ParserOutputTest extends MediaWikiTestCase {
+
+ public static function provideIsLinkInternal() {
+ return array(
+ // Different domains
+ array( false, 'http://example.org', 'http://mediawiki.org' ),
+ // Same domains
+ array( true, 'http://example.org', 'http://example.org' ),
+ array( true, 'https://example.org', 'https://example.org' ),
+ array( true, '//example.org', '//example.org' ),
+ // Same domain different cases
+ array( true, 'http://example.org', 'http://EXAMPLE.ORG' ),
+ // Paths, queries, and fragments are not relevant
+ array( true, 'http://example.org', 'http://example.org/wiki/Main_Page' ),
+ array( true, 'http://example.org', 'http://example.org?my=query' ),
+ array( true, 'http://example.org', 'http://example.org#its-a-fragment' ),
+ // Different protocols
+ array( false, 'http://example.org', 'https://example.org' ),
+ array( false, 'https://example.org', 'http://example.org' ),
+ // Protocol relative servers always match http and https links
+ array( true, '//example.org', 'http://example.org' ),
+ array( true, '//example.org', 'https://example.org' ),
+ // But they don't match strange things like this
+ array( false, '//example.org', 'irc://example.org' ),
+ );
+ }
+
+ /**
+ * Test to make sure ParserOutput::isLinkInternal behaves properly
+ * @dataProvider provideIsLinkInternal
+ * @covers ParserOutput::isLinkInternal
+ */
+ public function testIsLinkInternal( $shouldMatch, $server, $url ) {
+ $this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
+ }
+
+ /**
+ * @covers ParserOutput::setExtensionData
+ * @covers ParserOutput::getExtensionData
+ */
+ public function testExtensionData() {
+ $po = new ParserOutput();
+
+ $po->setExtensionData( "one", "Foo" );
+
+ $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
+ $this->assertNull( $po->getExtensionData( "spam" ) );
+
+ $po->setExtensionData( "two", "Bar" );
+ $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
+ $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
+
+ $po->setExtensionData( "one", null );
+ $this->assertNull( $po->getExtensionData( "one" ) );
+ $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
+ }
+}
diff --git a/tests/phpunit/includes/parser/ParserPreloadTest.php b/tests/phpunit/includes/parser/ParserPreloadTest.php
new file mode 100644
index 00000000..d12fee36
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserPreloadTest.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Basic tests for Parser::getPreloadText
+ * @author Antoine Musso
+ */
+class ParserPreloadTest extends MediaWikiTestCase {
+ /**
+ * @var Parser
+ */
+ private $testParser;
+ /**
+ * @var ParserOptions
+ */
+ private $testParserOptions;
+ /**
+ * @var Title
+ */
+ private $title;
+
+ protected function setUp() {
+ global $wgContLang;
+
+ parent::setUp();
+ $this->testParserOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+
+ $this->testParser = new Parser();
+ $this->testParser->Options( $this->testParserOptions );
+ $this->testParser->clearState();
+
+ $this->title = Title::newFromText( 'Preload Test' );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ unset( $this->testParser );
+ unset( $this->title );
+ }
+
+ /**
+ * @covers Parser::getPreloadText
+ */
+ public function testPreloadSimpleText() {
+ $this->assertPreloaded( 'simple', 'simple' );
+ }
+
+ /**
+ * @covers Parser::getPreloadText
+ */
+ public function testPreloadedPreIsUnstripped() {
+ $this->assertPreloaded(
+ '<pre>monospaced</pre>',
+ '<pre>monospaced</pre>',
+ '<pre> in preloaded text must be unstripped (bug 27467)'
+ );
+ }
+
+ /**
+ * @covers Parser::getPreloadText
+ */
+ public function testPreloadedNowikiIsUnstripped() {
+ $this->assertPreloaded(
+ '<nowiki>[[Dummy title]]</nowiki>',
+ '<nowiki>[[Dummy title]]</nowiki>',
+ '<nowiki> in preloaded text must be unstripped (bug 27467)'
+ );
+ }
+
+ protected function assertPreloaded( $expected, $text, $msg = '' ) {
+ $this->assertEquals(
+ $expected,
+ $this->testParser->getPreloadText(
+ $text,
+ $this->title,
+ $this->testParserOptions
+ ),
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php
new file mode 100644
index 00000000..8aee937c
--- /dev/null
+++ b/tests/phpunit/includes/parser/PreprocessorTest.php
@@ -0,0 +1,239 @@
+<?php
+
+class PreprocessorTest extends MediaWikiTestCase {
+ protected $mTitle = 'Page title';
+ protected $mPPNodeCount = 0;
+ /**
+ * @var ParserOptions
+ */
+ protected $mOptions;
+ /**
+ * @var Preprocessor
+ */
+ protected $mPreprocessor;
+
+ protected function setUp() {
+ global $wgParserConf, $wgContLang;
+ parent::setUp();
+ $this->mOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+ $name = isset( $wgParserConf['preprocessorClass'] ) ? $wgParserConf['preprocessorClass'] : 'Preprocessor_DOM';
+
+ $this->mPreprocessor = new $name( $this );
+ }
+
+ function getStripList() {
+ return array( 'gallery', 'display map' /* Used by Maps, see r80025 CR */, '/foo' );
+ }
+
+ public static function provideCases() {
+ return array(
+ array( "Foo", "<root>Foo</root>" ),
+ array( "<!-- Foo -->", "<root><comment>&lt;!-- Foo --&gt;</comment></root>" ),
+ array( "<!-- Foo --><!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment><comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> <comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> \n <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> \n <comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> \n <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> \n<comment> &lt;!-- Bar --&gt;\n</comment></root>" ),
+ array( "<!-- Foo --> <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> <comment>&lt;!-- Bar --&gt;</comment>\n</root>" ),
+ array( "<!-->Bar", "<root><comment>&lt;!--&gt;Bar</comment></root>" ),
+ array( "<!-- Comment -- comment", "<root><comment>&lt;!-- Comment -- comment</comment></root>" ),
+ array( "== Foo ==\n <!-- Bar -->\n== Baz ==\n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<comment> &lt;!-- Bar --&gt;\n</comment><h level=\"2\" i=\"2\">== Baz ==</h>\n</root>" ),
+ array( "<gallery/>", "<root><ext><name>gallery</name><attr></attr></ext></root>" ),
+ array( "Foo <gallery/> Bar", "<root>Foo <ext><name>gallery</name><attr></attr></ext> Bar</root>" ),
+ array( "<gallery></gallery>", "<root><ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<foo> <gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<foo> <gallery><gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner>&lt;gallery&gt;</inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<noinclude> Foo bar </noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore> Foo bar <ignore>&lt;/noinclude&gt;</ignore></root>" ),
+ array( "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore></root>" ),
+ array( "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore>\n</root>" ),
+ array( "<gallery>foo bar", "<root><ext><name>gallery</name><attr></attr><inner>foo bar</inner></ext></root>" ),
+ array( "<{{foo}}>", "<root>&lt;<template><title>foo</title></template>&gt;</root>" ),
+ array( "<{{{foo}}}>", "<root>&lt;<tplarg><title>foo</title></tplarg>&gt;</root>" ),
+ array( "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner>&lt;/gallery</inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "=== Foo === ", "<root><h level=\"3\" i=\"1\">=== Foo === </h></root>" ),
+ array( "==<!-- -->= Foo === ", "<root><h level=\"2\" i=\"1\">==<comment>&lt;!-- --&gt;</comment>= Foo === </h></root>" ),
+ array( "=== Foo ==<!-- -->= ", "<root><h level=\"1\" i=\"1\">=== Foo ==<comment>&lt;!-- --&gt;</comment>= </h></root>" ),
+ array( "=== Foo ===<!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment></h>\n</root>" ),
+ array( "=== Foo ===<!-- --> <!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment> <comment>&lt;!-- --&gt;</comment></h>\n</root>" ),
+ array( "== Foo ==\n== Bar == \n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<h level=\"2\" i=\"2\">== Bar == </h>\n</root>" ),
+ array( "===========", "<root><h level=\"5\" i=\"1\">===========</h></root>" ),
+ array( "Foo\n=\n==\n=\n", "<root>Foo\n=\n==\n=\n</root>" ),
+ array( "{{Foo}}", "<root><template><title>Foo</title></template></root>" ),
+ array( "\n{{Foo}}", "<root>\n<template lineStart=\"1\"><title>Foo</title></template></root>" ),
+ array( "{{Foo|bar}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar}}a", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template>a</root>" ),
+ array( "{{Foo|bar|baz}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|=bar}}", "<root><template><title>Foo</title><part><name></name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar=baz}}", "<root><template><title>Foo</title><part><name>bar</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|{{bar}}=baz}}", "<root><template><title>Foo</title><part><name><template><title>bar</title></template></name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|baz}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name index=\"1\" /><value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|2=baz}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name>2</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|bar|foo=baz}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name>foo</name>=<value>baz</value></part></template></root>" ),
+ array( "{{{1}}}", "<root><tplarg><title>1</title></tplarg></root>" ),
+ array( "{{{1|}}}", "<root><tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo}}}", "<root><tplarg><title>Foo</title></tplarg></root>" ),
+ array( "{{{Foo|}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo|bar|baz}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></tplarg></root>" ),
+ array( "{<!-- -->{Foo}}", "<root>{<comment>&lt;!-- --&gt;</comment>{Foo}}</root>" ),
+ array( "{{{{Foobar}}}}", "<root>{<tplarg><title>Foobar</title></tplarg>}</root>" ),
+ array( "{{{ {{Foo}} }}}", "<root><tplarg><title> <template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{ {{{Foo}}} }}", "<root><template><title> <tplarg><title>Foo</title></tplarg> </title></template></root>" ),
+ array( "{{{{{Foo}}}}}", "<root><template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "{{{{{Foo}} }}}", "<root><tplarg><title><template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}}", "<root><tplarg><title><tplarg><title>Foo</title></tplarg></title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}", "<root>{<template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "[[[Foo]]", "<root>[[[Foo]]</root>" ),
+ array( "{{Foo|[[[[bar]]|baz]]}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>[[[[bar]]|baz]]</value></part></template></root>" ), // This test is important, since it means the difference between having the [[ rule stacked or not
+ array( "{{Foo|[[[[bar]|baz]]}}", "<root>{{Foo|[[[[bar]|baz]]}}</root>" ),
+ array( "{{Foo|Foo [[[[bar]|baz]]}}", "<root>{{Foo|Foo [[[[bar]|baz]]}}</root>" ),
+ array( "Foo <display map>Bar</display map >Baz", "<root>Foo <ext><name>display map</name><attr></attr><inner>Bar</inner><close>&lt;/display map &gt;</close></ext>Baz</root>" ),
+ array( "Foo <display map foo>Bar</display map >Baz", "<root>Foo <ext><name>display map</name><attr> foo</attr><inner>Bar</inner><close>&lt;/display map &gt;</close></ext>Baz</root>" ),
+ array( "Foo <gallery bar=\"baz\" />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;baz&quot; </attr></ext></root>" ),
+ array( "Foo <gallery bar=\"1\" baz=2 />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;1&quot; baz=2 </attr></ext></root>" ),
+ array( "</foo>Foo<//foo>", "<root><ext><name>/foo</name><attr></attr><inner>Foo</inner><close>&lt;//foo&gt;</close></ext></root>" ), # Worth blacklisting IMHO
+ array( "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", "<root><template><title>#ifexpr: (<tplarg><title>1</title><part><name index=\"1\" /><value>1</value></part></tplarg> = 2) </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>" ),
+ array( "{{#if: {{{1|}}} | Foo | {{Bar}} }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> <template><title>Bar</title></template> </value></part></template></root>" ),
+ array( "{{#if: {{{1|}}} | Foo | [[Bar]] }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> [[Bar]] </value></part></template></root>" ),
+ array( "{{#if: {{{1|}}} | [[Foo]] | Bar }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> [[Foo]] </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>" ),
+ array( "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 1 </value></part><part><name index=\"2\" /><value> <template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 2 </value></part><part><name index=\"2\" /><value> 3 </value></part></template> </value></part></template></root>" ),
+ array( "{{ {{Foo}}", "<root>{{ <template><title>Foo</title></template></root>" ),
+ array( "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "<root>{{Foobar <template><title>Foo</title></template> <template><title>Bar</title></template> <template><title>Baz</title></template> </root>" ),
+ array( "[[Foo]] |", "<root>[[Foo]] |</root>" ),
+ array( "{{Foo|Bar|", "<root>{{Foo|Bar|</root>" ),
+ array( "[[Foo]", "<root>[[Foo]</root>" ),
+ array( "[[Foo|Bar]", "<root>[[Foo|Bar]</root>" ),
+ array( "{{Foo| [[Bar] }}", "<root>{{Foo| [[Bar] }}</root>" ),
+ array( "{{Foo| [[Bar|Baz] }}", "<root>{{Foo| [[Bar|Baz] }}</root>" ),
+ array( "{{Foo|bar=[[baz]}}", "<root>{{Foo|bar=[[baz]}}</root>" ),
+ array( "{{foo|", "<root>{{foo|</root>" ),
+ array( "{{foo|}", "<root>{{foo|}</root>" ),
+ array( "{{foo|} }}", "<root><template><title>foo</title><part><name index=\"1\" /><value>} </value></part></template></root>" ),
+ array( "{{foo|bar=|}", "<root>{{foo|bar=|}</root>" ),
+ array( "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>" ),
+ array( "{{Foo|} Bar=}}", "<root><template><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>" ),
+ /* array( file_get_contents( __DIR__ . '/QuoteQuran.txt' ), file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ), */
+ );
+ }
+
+ /**
+ * Get XML preprocessor tree from the preprocessor (which may not be the
+ * native XML-based one).
+ *
+ * @param string $wikiText
+ * @return string
+ */
+ protected function preprocessToXml( $wikiText ) {
+ if ( method_exists( $this->mPreprocessor, 'preprocessToXml' ) ) {
+ return $this->normalizeXml( $this->mPreprocessor->preprocessToXml( $wikiText ) );
+ }
+
+ $dom = $this->mPreprocessor->preprocessToObj( $wikiText );
+ if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+ return $dom->saveXML();
+ } else {
+ return $this->normalizeXml( $dom->__toString() );
+ }
+ }
+
+ /**
+ * Normalize XML string to the form that a DOMDocument saves out.
+ *
+ * @param string $xml
+ * @return string
+ */
+ protected function normalizeXml( $xml ) {
+ return preg_replace( '!<([a-z]+)/>!', '<$1></$1>', str_replace( ' />', '/>', $xml ) );
+ }
+
+ /**
+ * @dataProvider provideCases
+ * @covers Preprocessor_DOM::preprocessToXml
+ */
+ public function testPreprocessorOutput( $wikiText, $expectedXml ) {
+ $this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) );
+ }
+
+ /**
+ * These are more complex test cases taken out of wiki articles.
+ */
+ public static function provideFiles() {
+ return array(
+ array( "QuoteQuran" ), # http://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC-BY-SA by Striver
+ array( "Factorial" ), # http://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC-BY-SA by Polonium
+ array( "All_system_messages" ), # http://tl.wiktionary.org/w/index.php?title=Suleras:All_system_messages&oldid=2765 GPL text generated by MediaWiki
+ array( "Fundraising" ), # http://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC-BY-SA, copied there by Sky Harbor.
+ array( "NestedTemplates" ), # bug 27936
+ );
+ }
+
+ /**
+ * @dataProvider provideFiles
+ * @covers Preprocessor_DOM::preprocessToXml
+ */
+ public function testPreprocessorOutputFiles( $filename ) {
+ $folder = __DIR__ . "/../../../parser/preprocess";
+ $wikiText = file_get_contents( "$folder/$filename.txt" );
+ $output = $this->preprocessToXml( $wikiText );
+
+ $expectedFilename = "$folder/$filename.expected";
+ if ( file_exists( $expectedFilename ) ) {
+ $expectedXml = $this->normalizeXml( file_get_contents( $expectedFilename ) );
+ $this->assertEquals( $expectedXml, $output );
+ } else {
+ $tempFilename = tempnam( $folder, "$filename." );
+ file_put_contents( $tempFilename, $output );
+ $this->markTestIncomplete( "File $expectedFilename missing. Output stored as $tempFilename" );
+ }
+ }
+
+ /**
+ * Tests from Bug 28642 · https://bugzilla.wikimedia.org/28642
+ */
+ public static function provideHeadings() {
+ return array( /* These should become headings: */
+ array( "== h ==<!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+
+ /* These are not working: */
+ array( "== h == x <!--c1--><!--c2--><!--c3--> ", "<root>== h == x <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </root>" ),
+ array( "== h ==<!--c1--> x <!--c2--><!--c3--> ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> x <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> x </root>" ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHeadings
+ * @covers Preprocessor_DOM::preprocessToXml
+ */
+ public function testHeadings( $wikiText, $expectedXml ) {
+ $this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) );
+ }
+}
diff --git a/tests/phpunit/includes/parser/TagHooksTest.php b/tests/phpunit/includes/parser/TagHooksTest.php
new file mode 100644
index 00000000..61cbe45d
--- /dev/null
+++ b/tests/phpunit/includes/parser/TagHooksTest.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @group Parser
+ */
+class TagHookTest extends MediaWikiTestCase {
+ public static function provideValidNames() {
+ return array( array( 'foo' ), array( 'foo-bar' ), array( 'foo_bar' ), array( 'FOO-BAR' ), array( 'foo bar' ) );
+ }
+
+ public static function provideBadNames() {
+ return array( array( "foo<bar" ), array( "foo>bar" ), array( "foo\nbar" ), array( "foo\rbar" ) );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgAlwaysUseTidy', false );
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ */
+ public function testTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parserOutput = $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ */
+ public function testBadTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) );
+ $this->fail( 'Exception not thrown.' );
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ */
+ public function testFunctionTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), 0 );
+ $parserOutput = $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ */
+ public function testBadFunctionTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS );
+ $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) );
+ $this->fail( 'Exception not thrown.' );
+ }
+
+ function tagCallback( $text, $params, $parser ) {
+ return str_rot13( $text );
+ }
+
+ function functionTagCallback( &$parser, $frame, $code, $attribs ) {
+ return str_rot13( $code );
+ }
+}
diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php
new file mode 100644
index 00000000..57a88b9d
--- /dev/null
+++ b/tests/phpunit/includes/parser/TidyTest.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @group Parser
+ */
+class TidyTest extends MediaWikiTestCase {
+ public function setUp() {
+ parent::setUp();
+ $check = MWTidy::tidy( '' );
+ if ( strpos( $check, '<!--' ) !== false ) {
+ $this->markTestSkipped( 'Tidy not found' );
+ }
+ }
+
+ /**
+ * @dataProvider provideTestWrapping
+ */
+ public function testTidyWrapping( $expected, $text, $msg = '' ) {
+ $text = MWTidy::tidy( $text );
+ // We don't care about where Tidy wants to stick is <p>s
+ $text = trim( preg_replace( '#</?p>#', '', $text ) );
+ // Windows, we love you!
+ $text = str_replace( "\r", '', $text );
+ $this->assertEquals( $expected, $text, $msg );
+ }
+
+ public function provideTestWrapping() {
+ return array(
+ array(
+ '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+ '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+ '<mw:editsection> should survive tidy'
+ ),
+ array(
+ '<editsection page="foo" section="bar">foo</editsection>',
+ '<editsection page="foo" section="bar">foo</editsection>',
+ '<editsection> should survive tidy'
+ ),
+ array( '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ),
+ array( "<link foo=\"bar\" />\nfoo", '<link foo="bar"/>foo', '<link> should survive tidy' ),
+ array( "<meta foo=\"bar\" />\nfoo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ),
+ );
+ }
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/search/SearchEngineTest.php b/tests/phpunit/includes/search/SearchEngineTest.php
new file mode 100644
index 00000000..87067038
--- /dev/null
+++ b/tests/phpunit/includes/search/SearchEngineTest.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * @group Search
+ * @group Database
+ */
+class SearchEngineTest extends MediaWikiLangTestCase {
+ protected $search, $pageList;
+
+ /**
+ * Checks for database type & version.
+ * Will skip current test if DB does not support search.
+ */
+ protected function setUp() {
+ parent::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" );
+ }
+
+ $searchType = $this->db->getSearchEngine();
+ $this->search = new $searchType( $this->db );
+ }
+
+ protected function tearDown() {
+ unset( $this->search );
+
+ parent::tearDown();
+ }
+
+ function pageExists( $title ) {
+ return false;
+ }
+
+ function addDBData() {
+ if ( $this->pageExists( 'Not_Main_Page' ) ) {
+ return;
+ }
+
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ // @todo cover the case of non-wikitext content in the main namespace
+ 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 fetchIds( $results ) {
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content "
+ . "in the main namespace" );
+ }
+
+ $this->assertTrue( is_object( $results ) );
+
+ $matches = array();
+ $row = $results->next();
+ while ( $row ) {
+ $matches[] = $row->getTitle()->getPrefixedText();
+ $row = $results->next();
+ }
+ $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;
+ }
+
+ /**
+ * Insert a new page
+ *
+ * @param $pageName String: page name
+ * @param $text String: page's content
+ * @param $n Integer: unused
+ */
+ function insertPage( $pageName, $text, $ns ) {
+ $title = Title::newFromText( $pageName, $ns );
+
+ $user = User::newFromName( 'WikiSysop' );
+ $comment = 'Search Test';
+
+ // avoid memory leak...?
+ LinkCache::singleton()->clear();
+
+ $page = WikiPage::factory( $title );
+ $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
+
+ $this->pageList[] = array( $title, $page->getId() );
+
+ return true;
+ }
+
+ public 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" );
+ }
+
+ public function testTextSearch() {
+ $this->assertEquals(
+ array( 'Smithee' ),
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Plain search failed" );
+ }
+
+ public 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" );
+ }
+
+ public function testTitleSearch() {
+ $this->assertEquals(
+ array(
+ 'Alan Smithee',
+ 'Smithee',
+ ),
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title search failed" );
+ }
+
+ public 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 00000000..2f4fd501
--- /dev/null
+++ b/tests/phpunit/includes/search/SearchUpdateTest.php
@@ -0,0 +1,74 @@
+<?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
+ * @group Database
+ */
+class SearchUpdateTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgSearchType', 'MockSearch' );
+ }
+
+ function updateText( $text ) {
+ return trim( SearchUpdate::updateText( $text ) );
+ }
+
+ public 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'
+ );
+ }
+
+ public function testBug32712() {
+ $text = "text „http://example.com“ text";
+ $result = $this->updateText( $text );
+ $processed = preg_replace( '/Q/u', 'Q', $result );
+ $this->assertTrue(
+ $processed != '',
+ 'Link surrounded by unicode quotes should not fail UTF-8 validation'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/site/MediaWikiSiteTest.php b/tests/phpunit/includes/site/MediaWikiSiteTest.php
new file mode 100644
index 00000000..c5d52d33
--- /dev/null
+++ b/tests/phpunit/includes/site/MediaWikiSiteTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Tests for the MediaWikiSite class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class MediaWikiSiteTest extends SiteTest {
+
+ public function testNormalizePageTitle() {
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'enwiki' );
+
+ //NOTE: this does not actually call out to the enwiki site to perform the normalization,
+ // but uses a local Title object to do so. This is hardcoded on SiteLink::normalizePageTitle
+ // for the case that MW_PHPUNIT_TEST is set.
+ $this->assertEquals( 'Foo', $site->normalizePageName( ' foo ' ) );
+ }
+
+ public function fileUrlProvider() {
+ return array(
+ // url, filepath, path arg, expected
+ array( 'https://en.wikipedia.org', '/w/$1', 'api.php', 'https://en.wikipedia.org/w/api.php' ),
+ array( 'https://en.wikipedia.org', '/w/', 'api.php', 'https://en.wikipedia.org/w/' ),
+ array( 'https://en.wikipedia.org', '/foo/page.php?name=$1', 'api.php', 'https://en.wikipedia.org/foo/page.php?name=api.php' ),
+ array( 'https://en.wikipedia.org', '/w/$1', '', 'https://en.wikipedia.org/w/' ),
+ array( 'https://en.wikipedia.org', '/w/$1', 'foo/bar/api.php', 'https://en.wikipedia.org/w/foo/bar/api.php' ),
+ );
+ }
+
+ /**
+ * @dataProvider fileUrlProvider
+ * @covers MediaWikiSite::getFileUrl
+ */
+ public function testGetFileUrl( $url, $filePath, $pathArgument, $expected ) {
+ $site = new MediaWikiSite();
+ $site->setFilePath( $url . $filePath );
+
+ $this->assertEquals( $expected, $site->getFileUrl( $pathArgument ) );
+ }
+
+ public static function provideGetPageUrl() {
+ return array(
+ // path, page, expected substring
+ array( 'http://acme.test/wiki/$1', 'Berlin', '/wiki/Berlin' ),
+ array( 'http://acme.test/wiki/', 'Berlin', '/wiki/' ),
+ array( 'http://acme.test/w/index.php?title=$1', 'Berlin', '/w/index.php?title=Berlin' ),
+ array( 'http://acme.test/wiki/$1', '', '/wiki/' ),
+ array( 'http://acme.test/wiki/$1', 'Berlin/sub page', '/wiki/Berlin/sub_page' ),
+ array( 'http://acme.test/wiki/$1', 'Cork (city) ', '/Cork_(city)' ),
+ array( 'http://acme.test/wiki/$1', 'M&M', '/wiki/M%26M' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPageUrl
+ * @covers MediaWikiSite::getPageUrl
+ */
+ public function testGetPageUrl( $path, $page, $expected ) {
+ $site = new MediaWikiSite();
+ $site->setLinkPath( $path );
+
+ $this->assertContains( $path, $site->getPageUrl() );
+ $this->assertContains( $expected, $site->getPageUrl( $page ) );
+ }
+}
diff --git a/tests/phpunit/includes/site/SiteListTest.php b/tests/phpunit/includes/site/SiteListTest.php
new file mode 100644
index 00000000..8af2fc1b
--- /dev/null
+++ b/tests/phpunit/includes/site/SiteListTest.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * Tests for the SiteList class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteListTest extends MediaWikiTestCase {
+
+ /**
+ * Returns instances of SiteList implementing objects.
+ * @return array
+ */
+ public function siteListProvider() {
+ $sitesArrays = $this->siteArrayProvider();
+
+ $listInstances = array();
+
+ foreach ( $sitesArrays as $sitesArray ) {
+ $listInstances[] = new SiteList( $sitesArray[0] );
+ }
+
+ return $this->arrayWrap( $listInstances );
+ }
+
+ /**
+ * Returns arrays with instances of Site implementing objects.
+ * @return array
+ */
+ public function siteArrayProvider() {
+ $sites = TestSites::getSites();
+
+ $siteArrays = array();
+
+ $siteArrays[] = $sites;
+
+ $siteArrays[] = array( array_shift( $sites ) );
+
+ $siteArrays[] = array( array_shift( $sites ), array_shift( $sites ) );
+
+ return $this->arrayWrap( $siteArrays );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::isEmpty
+ */
+ public function testIsEmpty( SiteList $sites ) {
+ $this->assertEquals( count( $sites ) === 0, $sites->isEmpty() );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getSite
+ */
+ public function testGetSiteByGlobalId( SiteList $sites ) {
+ if ( $sites->isEmpty() ) {
+ $this->assertTrue( true );
+ } else {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertEquals( $site, $sites->getSite( $site->getGlobalId() ) );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getSiteByInternalId
+ */
+ public function testGetSiteByInternalId( $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ if ( is_integer( $site->getInternalId() ) ) {
+ $this->assertEquals( $site, $sites->getSiteByInternalId( $site->getInternalId() ) );
+ }
+ }
+
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::hasSite
+ */
+ public function testHasGlobalId( $sites ) {
+ $this->assertFalse( $sites->hasSite( 'non-existing-global-id' ) );
+ $this->assertFalse( $sites->hasInternalId( 720101010 ) );
+
+ if ( !$sites->isEmpty() ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::hasInternalId
+ */
+ public function testHasInternallId( $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ if ( is_integer( $site->getInternalId() ) ) {
+ $this->assertTrue( $site, $sites->hasInternalId( $site->getInternalId() ) );
+ }
+ }
+
+ $this->assertFalse( $sites->hasInternalId( -1 ) );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getGlobalIdentifiers
+ */
+ public function testGetGlobalIdentifiers( SiteList $sites ) {
+ $identifiers = $sites->getGlobalIdentifiers();
+
+ $this->assertTrue( is_array( $identifiers ) );
+
+ $expected = array();
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $expected[] = $site->getGlobalId();
+ }
+
+ $this->assertArrayEquals( $expected, $identifiers );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ *
+ * @since 1.21
+ *
+ * @param SiteList $list
+ * @covers SiteList::getSerializationData
+ * @covers SiteList::unserialize
+ */
+ public function testSerialization( SiteList $list ) {
+ $serialization = serialize( $list );
+ /**
+ * @var SiteArray $copy
+ */
+ $copy = unserialize( $serialization );
+
+ $this->assertArrayEquals( $list->getGlobalIdentifiers(), $copy->getGlobalIdentifiers() );
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $list as $site ) {
+ $this->assertTrue( $copy->hasInternalId( $site->getInternalId() ) );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/site/SiteSQLStoreTest.php b/tests/phpunit/includes/site/SiteSQLStoreTest.php
new file mode 100644
index 00000000..6002c1a1
--- /dev/null
+++ b/tests/phpunit/includes/site/SiteSQLStoreTest.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * Tests for the SiteSQLStore class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteSQLStoreTest extends MediaWikiTestCase {
+
+ /**
+ * @covers SiteSQLStore::getSites
+ */
+ public function testGetSites() {
+ $expectedSites = TestSites::getSites();
+ TestSites::insertIntoDb();
+
+ $store = SiteSQLStore::newInstance();
+
+ $sites = $store->getSites();
+
+ $this->assertInstanceOf( 'SiteList', $sites );
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertInstanceOf( 'Site', $site );
+ }
+
+ foreach ( $expectedSites as $site ) {
+ if ( $site->getGlobalId() !== null ) {
+ $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
+ }
+ }
+ }
+
+ /**
+ * @covers SiteSQLStore::saveSites
+ */
+ public function testSaveSites() {
+ $store = SiteSQLStore::newInstance();
+
+ $sites = array();
+
+ $site = new Site();
+ $site->setGlobalId( 'ertrywuutr' );
+ $site->setLanguageCode( 'en' );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'sdfhxujgkfpth' );
+ $site->setLanguageCode( 'nl' );
+ $sites[] = $site;
+
+ $this->assertTrue( $store->saveSites( $sites ) );
+
+ $site = $store->getSite( 'ertrywuutr' );
+ $this->assertInstanceOf( 'Site', $site );
+ $this->assertEquals( 'en', $site->getLanguageCode() );
+ $this->assertTrue( is_integer( $site->getInternalId() ) );
+ $this->assertTrue( $site->getInternalId() >= 0 );
+
+ $site = $store->getSite( 'sdfhxujgkfpth' );
+ $this->assertInstanceOf( 'Site', $site );
+ $this->assertEquals( 'nl', $site->getLanguageCode() );
+ $this->assertTrue( is_integer( $site->getInternalId() ) );
+ $this->assertTrue( $site->getInternalId() >= 0 );
+ }
+
+ /**
+ * @covers SiteSQLStore::reset
+ */
+ public function testReset() {
+ $store1 = SiteSQLStore::newInstance();
+ $store2 = SiteSQLStore::newInstance();
+
+ // initialize internal cache
+ $this->assertGreaterThan( 0, $store1->getSites()->count() );
+ $this->assertGreaterThan( 0, $store2->getSites()->count() );
+
+ // Clear actual data. Will purge the external cache and reset the internal
+ // cache in $store1, but not the internal cache in store2.
+ $this->assertTrue( $store1->clear() );
+
+ // sanity check: $store2 should have a stale cache now
+ $this->assertNotNull( $store2->getSite( 'enwiki' ) );
+
+ // purge cache
+ $store2->reset();
+
+ // ...now the internal cache of $store2 should be updated and thus empty.
+ $site = $store2->getSite( 'enwiki' );
+ $this->assertNull( $site );
+ }
+
+ /**
+ * @covers SiteSQLStore::clear
+ */
+ public function testClear() {
+ $store = SiteSQLStore::newInstance();
+ $this->assertTrue( $store->clear() );
+
+ $site = $store->getSite( 'enwiki' );
+ $this->assertNull( $site );
+
+ $sites = $store->getSites();
+ $this->assertEquals( 0, $sites->count() );
+ }
+}
diff --git a/tests/phpunit/includes/site/SiteTest.php b/tests/phpunit/includes/site/SiteTest.php
new file mode 100644
index 00000000..29c1ff33
--- /dev/null
+++ b/tests/phpunit/includes/site/SiteTest.php
@@ -0,0 +1,296 @@
+<?php
+
+/**
+ * Tests for the Site class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteTest extends MediaWikiTestCase {
+
+ public function instanceProvider() {
+ return $this->arrayWrap( TestSites::getSites() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getInterwikiIds
+ */
+ public function testGetInterwikiIds( Site $site ) {
+ $this->assertInternalType( 'array', $site->getInterwikiIds() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getNavigationIds
+ */
+ public function testGetNavigationIds( Site $site ) {
+ $this->assertInternalType( 'array', $site->getNavigationIds() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::addNavigationId
+ */
+ public function testAddNavigationId( Site $site ) {
+ $site->addNavigationId( 'foobar' );
+ $this->assertTrue( in_array( 'foobar', $site->getNavigationIds(), true ) );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::addInterwikiId
+ */
+ public function testAddInterwikiId( Site $site ) {
+ $site->addInterwikiId( 'foobar' );
+ $this->assertTrue( in_array( 'foobar', $site->getInterwikiIds(), true ) );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getLanguageCode
+ */
+ public function testGetLanguageCode( Site $site ) {
+ $this->assertTypeOrValue( 'string', $site->getLanguageCode(), null );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::setLanguageCode
+ */
+ public function testSetLanguageCode( Site $site ) {
+ $site->setLanguageCode( 'en' );
+ $this->assertEquals( 'en', $site->getLanguageCode() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::normalizePageName
+ */
+ public function testNormalizePageName( Site $site ) {
+ $this->assertInternalType( 'string', $site->normalizePageName( 'Foobar' ) );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getGlobalId
+ */
+ public function testGetGlobalId( Site $site ) {
+ $this->assertTypeOrValue( 'string', $site->getGlobalId(), null );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::setGlobalId
+ */
+ public function testSetGlobalId( Site $site ) {
+ $site->setGlobalId( 'foobar' );
+ $this->assertEquals( 'foobar', $site->getGlobalId() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getType
+ */
+ public function testGetType( Site $site ) {
+ $this->assertInternalType( 'string', $site->getType() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getPath
+ */
+ public function testGetPath( Site $site ) {
+ $this->assertTypeOrValue( 'string', $site->getPath( 'page_path' ), null );
+ $this->assertTypeOrValue( 'string', $site->getPath( 'file_path' ), null );
+ $this->assertTypeOrValue( 'string', $site->getPath( 'foobar' ), null );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getAllPaths
+ */
+ public function testGetAllPaths( Site $site ) {
+ $this->assertInternalType( 'array', $site->getAllPaths() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::setPath
+ * @covers Site::removePath
+ */
+ public function testSetAndRemovePath( Site $site ) {
+ $count = count( $site->getAllPaths() );
+
+ $site->setPath( 'spam', 'http://www.wikidata.org/$1' );
+ $site->setPath( 'spam', 'http://www.wikidata.org/foo/$1' );
+ $site->setPath( 'foobar', 'http://www.wikidata.org/bar/$1' );
+
+ $this->assertEquals( $count + 2, count( $site->getAllPaths() ) );
+
+ $this->assertInternalType( 'string', $site->getPath( 'foobar' ) );
+ $this->assertEquals( 'http://www.wikidata.org/foo/$1', $site->getPath( 'spam' ) );
+
+ $site->removePath( 'spam' );
+ $site->removePath( 'foobar' );
+
+ $this->assertEquals( $count, count( $site->getAllPaths() ) );
+
+ $this->assertNull( $site->getPath( 'foobar' ) );
+ $this->assertNull( $site->getPath( 'spam' ) );
+ }
+
+ /**
+ * @covers Site::setLinkPath
+ */
+ public function testSetLinkPath() {
+ $site = new Site();
+ $path = "TestPath/$1";
+
+ $site->setLinkPath( $path );
+ $this->assertEquals( $path, $site->getLinkPath() );
+ }
+
+ /**
+ * @covers Site::getLinkPathType
+ */
+ public function testGetLinkPathType() {
+ $site = new Site();
+
+ $path = 'TestPath/$1';
+ $site->setLinkPath( $path );
+ $this->assertEquals( $path, $site->getPath( $site->getLinkPathType() ) );
+
+ $path = 'AnotherPath/$1';
+ $site->setPath( $site->getLinkPathType(), $path );
+ $this->assertEquals( $path, $site->getLinkPath() );
+ }
+
+ /**
+ * @covers Site::setPath
+ */
+ public function testSetPath() {
+ $site = new Site();
+
+ $path = 'TestPath/$1';
+ $site->setPath( 'foo', $path );
+
+ $this->assertEquals( $path, $site->getPath( 'foo' ) );
+ }
+
+ /**
+ * @covers Site::setPath
+ * @covers Site::getProtocol
+ */
+ public function testProtocolRelativePath() {
+ $site = new Site();
+
+ $type = $site->getLinkPathType();
+ $path = '//acme.com/'; // protocol-relative URL
+ $site->setPath( $type, $path );
+
+ $this->assertEquals( '', $site->getProtocol() );
+ }
+
+ public static function provideGetPageUrl() {
+ //NOTE: the assumption that the URL is built by replacing $1
+ // with the urlencoded version of $page
+ // is true for Site but not guaranteed for subclasses.
+ // Subclasses need to override this provider appropriately.
+
+ return array(
+ array( #0
+ 'http://acme.test/TestPath/$1',
+ 'Foo',
+ '/TestPath/Foo',
+ ),
+ array( #1
+ 'http://acme.test/TestScript?x=$1&y=bla',
+ 'Foo',
+ 'TestScript?x=Foo&y=bla',
+ ),
+ array( #2
+ 'http://acme.test/TestPath/$1',
+ 'foo & bar/xyzzy (quux-shmoox?)',
+ '/TestPath/foo%20%26%20bar%2Fxyzzy%20%28quux-shmoox%3F%29',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPageUrl
+ * @covers Site::getPageUrl
+ */
+ public function testGetPageUrl( $path, $page, $expected ) {
+ $site = new Site();
+
+ //NOTE: the assumption that getPageUrl is based on getLinkPath
+ // is true for Site but not guaranteed for subclasses.
+ // Subclasses need to override this test case appropriately.
+ $site->setLinkPath( $path );
+ $this->assertContains( $path, $site->getPageUrl() );
+
+ $this->assertContains( $expected, $site->getPageUrl( $page ) );
+ }
+
+ protected function assertTypeOrFalse( $type, $value ) {
+ if ( $value === false ) {
+ $this->assertTrue( true );
+ } else {
+ $this->assertInternalType( $type, $value );
+ }
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::serialize
+ * @covers Site::unserialize
+ */
+ public function testSerialization( Site $site ) {
+ $this->assertInstanceOf( 'Serializable', $site );
+
+ $serialization = serialize( $site );
+ $newInstance = unserialize( $serialization );
+
+ $this->assertInstanceOf( 'Site', $newInstance );
+
+ $this->assertEquals( $serialization, serialize( $newInstance ) );
+ }
+}
diff --git a/tests/phpunit/includes/site/TestSites.php b/tests/phpunit/includes/site/TestSites.php
new file mode 100644
index 00000000..f224b7d7
--- /dev/null
+++ b/tests/phpunit/includes/site/TestSites.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * Holds sites for testing purposes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class TestSites {
+
+ /**
+ * @since 1.21
+ *
+ * @return array
+ */
+ public static function getSites() {
+ $sites = array();
+
+ $site = new Site();
+ $site->setGlobalId( 'foobar' );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'enwiktionary' );
+ $site->setGroup( 'wiktionary' );
+ $site->setLanguageCode( 'en' );
+ $site->addNavigationId( 'enwiktionary' );
+ $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
+ $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'dewiktionary' );
+ $site->setGroup( 'wiktionary' );
+ $site->setLanguageCode( 'de' );
+ $site->addInterwikiId( 'dewiktionary' );
+ $site->addInterwikiId( 'wiktionaryde' );
+ $site->setPath( MediaWikiSite::PATH_PAGE, "https://de.wiktionary.org/wiki/$1" );
+ $site->setPath( MediaWikiSite::PATH_FILE, "https://de.wiktionary.org/w/$1" );
+ $sites[] = $site;
+
+ $site = new Site();
+ $site->setGlobalId( 'spam' );
+ $site->setGroup( 'spam' );
+ $site->setLanguageCode( 'en' );
+ $site->addNavigationId( 'spam' );
+ $site->addNavigationId( 'spamz' );
+ $site->addInterwikiId( 'spamzz' );
+ $site->setLinkPath( "http://spamzz.test/testing/" );
+ $sites[] = $site;
+
+ foreach ( array( 'en', 'de', 'nl', 'sv', 'sr', 'no', 'nn' ) as $langCode ) {
+ $site = new MediaWikiSite();
+ $site->setGlobalId( $langCode . 'wiki' );
+ $site->setGroup( 'wikipedia' );
+ $site->setLanguageCode( $langCode );
+ $site->addInterwikiId( $langCode );
+ $site->addNavigationId( $langCode );
+ $site->setPath( MediaWikiSite::PATH_PAGE, "https://$langCode.wikipedia.org/wiki/$1" );
+ $site->setPath( MediaWikiSite::PATH_FILE, "https://$langCode.wikipedia.org/w/$1" );
+ $sites[] = $site;
+ }
+
+ return $sites;
+ }
+
+ /**
+ * Inserts sites into the database for the unit tests that need them.
+ *
+ * @since 0.1
+ */
+ public static function insertIntoDb() {
+ $sitesTable = SiteSQLStore::newInstance();
+ $sitesTable->clear();
+ $sitesTable->saveSites( TestSites::getSites() );
+ }
+}
diff --git a/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php
new file mode 100644
index 00000000..a806b4ac
--- /dev/null
+++ b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Test class to run the query of most of all our special pages
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ * @group Database
+ */
+
+if ( !defined( 'MEDIAWIKI' ) ) {
+ die( 1 );
+}
+
+global $IP;
+require_once "$IP/includes/QueryPage.php"; // Needed to populate $wgQueryPages
+
+class QueryAllSpecialPagesTest extends MediaWikiTestCase {
+
+ /** List query pages that can not be tested automatically */
+ protected $manualTest = array(
+ 'LinkSearchPage'
+ );
+
+ /**
+ * Pages whose query use the same DB table more than once.
+ * This is used to skip testing those pages when run against a MySQL backend
+ * which does not support reopening a temporary table. See upstream bug:
+ * http://bugs.mysql.com/bug.php?id=10327
+ */
+ protected $reopensTempTable = array(
+ 'BrokenRedirects',
+ );
+
+ /**
+ * Initialize all query page objects
+ */
+ function __construct() {
+ parent::__construct();
+
+ global $wgQueryPages;
+ foreach ( $wgQueryPages as $page ) {
+ $class = $page[0];
+ if ( !in_array( $class, $this->manualTest ) ) {
+ $this->queryPages[$class] = new $class;
+ }
+ }
+ }
+
+ /**
+ * Test SQL for each of our QueryPages objects
+ * @group Database
+ */
+ public function testQuerypageSqlQuery() {
+ global $wgDBtype;
+
+ foreach ( $this->queryPages as $page ) {
+
+ // With MySQL, skips special pages reopening a temporary table
+ // See http://bugs.mysql.com/bug.php?id=10327
+ if (
+ $wgDBtype === 'mysql'
+ && in_array( $page->getName(), $this->reopensTempTable )
+ ) {
+ $this->markTestSkipped( "SQL query for page {$page->getName()} can not be tested on MySQL backend (it reopens a temporary table)" );
+ continue;
+ }
+
+ $msg = "SQL query for page {$page->getName()} should give a result wrapper object";
+
+ $result = $page->reallyDoQuery( 50 );
+ if ( $result instanceof ResultWrapper ) {
+ $this->assertTrue( true, $msg );
+ } else {
+ $this->assertFalse( false, $msg );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialPreferencesTest.php b/tests/phpunit/includes/specials/SpecialPreferencesTest.php
new file mode 100644
index 00000000..6c637c65
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialPreferencesTest.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Test class for SpecialPreferences class.
+ *
+ * Copyright © 2013, Antoine Musso
+ * Copyright © 2013, Wikimedia Foundation Inc.
+ *
+ */
+
+class SpecialPreferencesTest extends MediaWikiTestCase {
+
+ /**
+ * Make sure a nickname which is longer than $wgMaxSigChars
+ * is not throwing a fatal error.
+ *
+ * Test specifications by Alexandre "ialex" Emsenhuber.
+ */
+ public function testBug41337() {
+
+ // Set a low limit
+ $this->setMwGlobals( 'wgMaxSigChars', 2 );
+
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )
+ ->method( 'isAnon' )
+ ->will( $this->returnValue( false ) );
+
+ # Yeah foreach requires an array, not NULL =(
+ $user->expects( $this->any() )
+ ->method( 'getEffectiveGroups' )
+ ->will( $this->returnValue( array() ) );
+
+ # The mocked user has a long nickname
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->will( $this->returnValueMap( array(
+ array( 'nickname', null, false, 'superlongnickname' ),
+ )
+ ) );
+
+ # Validate the mock (FIXME should probably be removed)
+ $this->assertFalse( $user->isAnon() );
+ $this->assertEquals( array(),
+ $user->getEffectiveGroups() );
+ $this->assertEquals( 'superlongnickname',
+ $user->getOption( 'nickname' ) );
+
+ # Forge a request to call the special page
+ $context = new RequestContext();
+ $context->setRequest( new FauxRequest() );
+ $context->setUser( $user );
+ $context->setTitle( Title::newFromText( 'Test' ) );
+
+ # Do the call, should not spurt a fatal error.
+ $special = new SpecialPreferences();
+ $special->setContext( $context );
+ $special->execute( array() );
+ }
+
+}
diff --git a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php
new file mode 100644
index 00000000..436eb2e2
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Test class for SpecialRecentchanges class
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ * @group Database
+ */
+class SpecialRecentchangesTest extends MediaWikiTestCase {
+
+ /**
+ * @var SpecialRecentChanges
+ */
+ protected $rc;
+
+ /** helper to test SpecialRecentchanges::buildMainQueryConds() */
+ private function assertConditions( $expected, $requestOptions = null, $message = '' ) {
+ $context = new RequestContext;
+ $context->setRequest( new FauxRequest( $requestOptions ) );
+
+ # setup the rc object
+ $this->rc = new SpecialRecentChanges();
+ $this->rc->setContext( $context );
+ $formOptions = $this->rc->setup( null );
+
+ # Filter out rc_timestamp conditions which depends on the test runtime
+ # This condition is not needed as of march 2, 2011 -- hashar
+ # @todo FIXME: Find a way to generate the correct rc_timestamp
+ $queryConditions = array_filter(
+ $this->rc->buildMainQueryConds( $formOptions ),
+ 'SpecialRecentchangesTest::filterOutRcTimestampCondition'
+ );
+
+ $this->assertEquals(
+ $expected,
+ $queryConditions,
+ $message
+ );
+ }
+
+ /** return false if condition begin with 'rc_timestamp ' */
+ private static function filterOutRcTimestampCondition( $var ) {
+ return ( false === strpos( $var, 'rc_timestamp ' ) );
+ }
+
+ public function testRcNsFilter() {
+ $this->assertConditions(
+ array( # expected
+ 'rc_bot' => 0,
+ #0 => "rc_timestamp >= '20110223000000'",
+ 1 => "rc_namespace = '0'",
+ ),
+ array(
+ 'namespace' => NS_MAIN,
+ ),
+ "rc conditions with no options (aka default setting)"
+ );
+ }
+
+ public function testRcNsFilterInversion() {
+ $this->assertConditions(
+ array( # expected
+ #0 => "rc_timestamp >= '20110223000000'",
+ 'rc_bot' => 0,
+ 1 => sprintf( "rc_namespace != '%s'", NS_MAIN ),
+ ),
+ array(
+ 'namespace' => NS_MAIN,
+ 'invert' => 1,
+ ),
+ "rc conditions with namespace inverted"
+ );
+ }
+
+ /**
+ * @bug 2429
+ * @dataProvider provideNamespacesAssociations
+ */
+ public function testRcNsFilterAssociation( $ns1, $ns2 ) {
+ $this->assertConditions(
+ array( # expected
+ #0 => "rc_timestamp >= '20110223000000'",
+ 'rc_bot' => 0,
+ 1 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ),
+ ),
+ array(
+ 'namespace' => $ns1,
+ 'associated' => 1,
+ ),
+ "rc conditions with namespace inverted"
+ );
+ }
+
+ /**
+ * @bug 2429
+ * @dataProvider provideNamespacesAssociations
+ */
+ public function testRcNsFilterAssociationWithInversion( $ns1, $ns2 ) {
+ $this->assertConditions(
+ array( # expected
+ #0 => "rc_timestamp >= '20110223000000'",
+ 'rc_bot' => 0,
+ 1 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ),
+ ),
+ array(
+ 'namespace' => $ns1,
+ 'associated' => 1,
+ 'invert' => 1,
+ ),
+ "rc conditions with namespace inverted"
+ );
+ }
+
+ /**
+ * Provides associated namespaces to test recent changes
+ * namespaces association filtering.
+ */
+ public static function provideNamespacesAssociations() {
+ return array( # (NS => Associated_NS)
+ array( NS_MAIN, NS_TALK ),
+ array( NS_TALK, NS_MAIN ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php
new file mode 100644
index 00000000..17e883fd
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialSearchTest.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Test class for SpecialSearch class
+ * Copyright © 2012, Antoine Musso
+ *
+ * @author Antoine Musso
+ * @group Database
+ */
+
+class SpecialSearchTest extends MediaWikiTestCase {
+ private $search;
+
+ /**
+ * @covers SpecialSearch::load
+ * @dataProvider provideSearchOptionsTests
+ * @param $requested Array Request parameters. For example array( 'ns5' => true, 'ns6' => true). NULL to use default options.
+ * @param $userOptions Array User options to test with. For example array('searchNs5' => 1 );. NULL to use default options.
+ * @param $expectedProfile An expected search profile name
+ * @param $expectedNs Array Expected namespaces
+ */
+ public function testProfileAndNamespaceLoading(
+ $requested, $userOptions, $expectedProfile, $expectedNS,
+ $message = 'Profile name and namespaces mismatches!'
+ ) {
+ $context = new RequestContext;
+ $context->setUser(
+ $this->newUserWithSearchNS( $userOptions )
+ );
+ /*
+ $context->setRequest( new FauxRequest( array(
+ 'ns5'=>true,
+ 'ns6'=>true,
+ ) ));
+ */
+ $context->setRequest( new FauxRequest( $requested ) );
+ $search = new SpecialSearch();
+ $search->setContext( $context );
+ $search->load();
+
+ /**
+ * Verify profile name and namespace in the same assertion to make
+ * sure we will be able to fully compare the above code. PHPUnit stop
+ * after an assertion fail.
+ */
+ $this->assertEquals(
+ array( /** Expected: */
+ 'ProfileName' => $expectedProfile,
+ 'Namespaces' => $expectedNS,
+ )
+ , array( /** Actual: */
+ 'ProfileName' => $search->getProfile(),
+ 'Namespaces' => $search->getNamespaces(),
+ )
+ , $message
+ );
+ }
+
+ public static function provideSearchOptionsTests() {
+ $defaultNS = SearchEngine::defaultNamespaces();
+ $EMPTY_REQUEST = array();
+ $NO_USER_PREF = null;
+
+ return array(
+ /**
+ * Parameters:
+ * <Web Request>, <User options>
+ * Followed by expected values:
+ * <ProfileName>, <NSList>
+ * Then an optional message.
+ */
+ array(
+ $EMPTY_REQUEST, $NO_USER_PREF,
+ 'default', $defaultNS,
+ 'Bug 33270: No request nor user preferences should give default profile'
+ ),
+ array(
+ array( 'ns5' => 1 ), $NO_USER_PREF,
+ 'advanced', array( 5 ),
+ 'Web request with specific NS should override user preference'
+ ),
+ array(
+ $EMPTY_REQUEST, array(
+ 'searchNs2' => 1,
+ 'searchNs14' => 1,
+ ) + array_fill_keys( array_map( function ( $ns ) {
+ return "searchNs$ns";
+ }, $defaultNS ), 0 ),
+ 'advanced', array( 2, 14 ),
+ 'Bug 33583: search with no option should honor User search preferences'
+ . ' and have all other namespace disabled'
+ ),
+ );
+ }
+
+ /**
+ * Helper to create a new User object with given options
+ * User remains anonymous though
+ */
+ function newUserWithSearchNS( $opt = null ) {
+ $u = User::newFromId( 0 );
+ if ( $opt === null ) {
+ return $u;
+ }
+ foreach ( $opt as $name => $value ) {
+ $u->setOption( $name, $value );
+ }
+
+ return $u;
+ }
+
+ /**
+ * Verify we do not expand search term in <title> on search result page
+ * https://gerrit.wikimedia.org/r/4841
+ */
+ public function testSearchTermIsNotExpanded() {
+
+ # Initialize [[Special::Search]]
+ $search = new SpecialSearch();
+ $search->getContext()->setTitle( Title::newFromText( 'Special:Search' ) );
+ $search->load();
+
+ # Simulate a user searching for a given term
+ $term = '{{SITENAME}}';
+ $search->showResults( $term );
+
+ # Lookup the HTML page title set for that page
+ $pageTitle = $search
+ ->getContext()
+ ->getOutput()
+ ->getHTMLTitle();
+
+ # Compare :-]
+ $this->assertRegExp(
+ '/' . preg_quote( $term ) . '/',
+ $pageTitle,
+ "Search term '{$term}' should not be expanded in Special:Search <title>"
+ );
+ }
+}
diff --git a/tests/phpunit/includes/upload/UploadBaseTest.php b/tests/phpunit/includes/upload/UploadBaseTest.php
new file mode 100644
index 00000000..982b46b2
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadBaseTest.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @group Upload
+ */
+class UploadBaseTest extends MediaWikiTestCase {
+
+ /** @var UploadTestHandler */
+ protected $upload;
+
+ protected function setUp() {
+ global $wgHooks;
+ parent::setUp();
+
+ $this->upload = new UploadTestHandler;
+ $this->hooks = $wgHooks;
+ $wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) {
+ return false;
+ };
+ }
+
+ protected function tearDown() {
+ global $wgHooks;
+ $wgHooks = $this->hooks;
+
+ parent::tearDown();
+ }
+
+
+ /**
+ * First checks the return code
+ * of UploadBase::getTitle() and then the actual returned title
+ *
+ * @dataProvider provideTestTitleValidation
+ * @covers UploadBase::getTitle
+ */
+ public function testTitleValidation( $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 == UploadBase::OK ) {
+ $this->assertEquals( $dstFilename,
+ $this->upload->getTitle()->getText(),
+ "$msg text" );
+ }
+ }
+
+ /**
+ * Test various forms of valid and invalid titles that can be supplied.
+ */
+ public static function provideTestTitleValidation() {
+ return array(
+ /* Test a valid title */
+ array( 'ValidTitle.jpg', 'ValidTitle.jpg', UploadBase::OK,
+ 'upload valid title' ),
+ /* A title with a slash */
+ array( 'A/B.jpg', 'B.jpg', UploadBase::OK,
+ 'upload title with slash' ),
+ /* A title with illegal char */
+ array( 'A:B.jpg', 'A-B.jpg', UploadBase::OK,
+ 'upload title with colon' ),
+ /* Stripping leading File: prefix */
+ array( 'File:C.jpg', 'C.jpg', UploadBase::OK,
+ 'upload title with File prefix' ),
+ /* Test illegal suggested title (r94601) */
+ array( '%281%29.JPG', null, UploadBase::ILLEGAL_FILENAME,
+ 'illegal title for upload' ),
+ /* A title without extension */
+ array( 'A', null, UploadBase::FILETYPE_MISSING,
+ 'upload title without extension' ),
+ /* A title with no basename */
+ array( '.jpg', null, UploadBase::MIN_LENGTH_PARTNAME,
+ 'upload title without basename' ),
+ /* A title that is longer than 255 bytes */
+ array( str_repeat( 'a', 255 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG,
+ 'upload title longer than 255 bytes' ),
+ /* A title that is longer than 240 bytes */
+ array( str_repeat( 'a', 240 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG,
+ 'upload title longer than 240 bytes' ),
+ );
+ }
+
+ /**
+ * Test the upload verification functions
+ * @covers UploadBase::verifyUpload
+ */
+ public function testVerifyUpload() {
+ /* Setup with zero file size */
+ $this->upload->initializePathInfo( '', '', 0 );
+ $result = $this->upload->verifyUpload();
+ $this->assertEquals( UploadBase::EMPTY_FILE,
+ $result['status'],
+ 'upload empty file' );
+ }
+
+ // Helper used to create an empty file of size $size.
+ private function createFileOfSize( $size ) {
+ $filename = tempnam( wfTempDir(), "mwuploadtest" );
+
+ $fh = fopen( $filename, 'w' );
+ ftruncate( $fh, $size );
+ fclose( $fh );
+
+ return $filename;
+ }
+
+ /**
+ * test uploading a 100 bytes file with $wgMaxUploadSize = 100
+ *
+ * This method should be abstracted so we can test different settings.
+ */
+ public function testMaxUploadSize() {
+ global $wgMaxUploadSize;
+ $savedGlobal = $wgMaxUploadSize; // save global
+ global $wgFileExtensions;
+ $wgFileExtensions[] = 'txt';
+
+ $wgMaxUploadSize = 100;
+
+ $filename = $this->createFileOfSize( $wgMaxUploadSize );
+ $this->upload->initializePathInfo( basename( $filename ) . '.txt', $filename, 100 );
+ $result = $this->upload->verifyUpload();
+ unlink( $filename );
+
+ $this->assertEquals(
+ array( 'status' => UploadBase::OK ), $result );
+
+ $wgMaxUploadSize = $savedGlobal; // restore global
+ }
+}
+
+class UploadTestHandler extends UploadBase {
+ public function initializeFromRequest( &$request ) {
+ }
+
+ public function testTitleValidation( $name ) {
+ $this->mTitle = false;
+ $this->mDesiredDestName = $name;
+ $this->mTitleError = UploadBase::OK;
+ $this->getTitle();
+
+ return $this->mTitleError;
+ }
+}
diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php
new file mode 100644
index 00000000..a75fba69
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php
@@ -0,0 +1,350 @@
+<?php
+
+/**
+ * @group Broken
+ * @group Upload
+ * @group Database
+ */
+class UploadFromUrlTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgEnableUploads' => true,
+ 'wgAllowCopyUploads' => true,
+ 'wgAllowAsyncCopyUploads' => true,
+ ) );
+ wfSetupSession();
+
+ if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+ }
+ }
+
+ protected function doApiRequest( array $params, array $unused = null, $appendModule = false, User $user = 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() {
+ $job = JobQueueGroup::singleton()->pop();
+ while ( $job ) {
+ $job = JobQueueGroup::singleton()->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' => $this->user->userName,
+ 'lgpassword' => $this->user->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' => $this->user->userName,
+ 'lgpassword' => $this->user->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 = $this->user->getEditToken();
+ $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" );
+
+ $this->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" );
+
+ $this->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 = JobQueueGroup::singleton()->pop();
+ $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' );
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testAsyncUpload( $data ) {
+ $token = $this->user->getEditToken();
+
+ $this->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 = $this->user->getEditToken();
+
+ $this->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 = $this->user->getEditToken();
+
+ $job = JobQueueGroup::singleton()->pop();
+ $this->assertFalse( $job, 'Starting with an empty jobqueue' );
+
+ $this->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 = JobQueueGroup::singleton()->pop();
+ $this->assertFalse( $job );
+
+ $this->assertEquals( 'Success', $data[0]['upload']['result'] );
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ public function testLeaveMessage() {
+ $token = $this->user->user->getEditToken();
+
+ $talk = $this->user->user->getTalkPage();
+ if ( $talk->exists() ) {
+ $page = WikiPage::factory( $talk );
+ $page->doDeleteArticle( '' );
+ }
+
+ $this->assertFalse( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk does not exist' );
+
+ $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 = JobQueueGroup::singleton()->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 {
+ $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 = JobQueueGroup::singleton()->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 = JobQueueGroup::singleton()->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 );
+ $page = WikiPage::factory( $t );
+ $page->doDeleteArticle( "testing" );
+ }
+ $t = Title::newFromText( $name, NS_FILE );
+
+ $this->assertFalse( $t->exists(), "File '$name' was deleted" );
+ }
+}
diff --git a/tests/phpunit/includes/upload/UploadStashTest.php b/tests/phpunit/includes/upload/UploadStashTest.php
new file mode 100644
index 00000000..7a0fea48
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadStashTest.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * @group Database
+ */
+class UploadStashTest extends MediaWikiTestCase {
+ /**
+ * @var Array of UploadStashTestUser
+ */
+ public static $users;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Setup a file for bug 29408
+ $this->bug29408File = __DIR__ . '/bug29408';
+ file_put_contents( $this->bug29408File, "\x00" );
+
+ self::$users = array(
+ 'sysop' => new TestUser(
+ 'Uploadstashtestsysop',
+ 'Upload Stash Test Sysop',
+ 'upload_stash_test_sysop@example.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new TestUser(
+ 'Uploadstashtestuser',
+ 'Upload Stash Test User',
+ 'upload_stash_test_user@example.com',
+ array()
+ )
+ );
+ }
+
+ protected function tearDown() {
+ if ( file_exists( $this->bug29408File . "." ) ) {
+ unlink( $this->bug29408File . "." );
+ }
+
+ if ( file_exists( $this->bug29408File ) ) {
+ unlink( $this->bug29408File );
+ }
+
+ parent::tearDown();
+ }
+
+ public function testBug29408() {
+ $this->setMwGlobals( 'wgUser', self::$users['uploader']->user );
+
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $stash = new UploadStash( $repo );
+
+ // Throws exception caught by PHPUnit on failure
+ $file = $stash->stashFile( $this->bug29408File );
+ // We'll never reach this point if we hit bug 29408
+ $this->assertTrue( true, 'Unrecognized file without extension' );
+
+ $stash->removeFile( $file->getFileKey() );
+ }
+
+ public function testValidRequest() {
+ $request = new FauxRequest( array( 'wpFileKey' => 'foo' ) );
+ $this->assertFalse( UploadFromStash::isValidRequest( $request ), 'Check failure on bad wpFileKey' );
+
+ $request = new FauxRequest( array( 'wpSessionKey' => 'foo' ) );
+ $this->assertFalse( UploadFromStash::isValidRequest( $request ), 'Check failure on bad wpSessionKey' );
+
+ $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) );
+ $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check good wpFileKey' );
+
+ $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) );
+ $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check good wpSessionKey' );
+
+ $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test', 'wpSessionKey' => 'foo' ) );
+ $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check key precedence' );
+ }
+}