Merge "Avoid :checkbox Sizzle selector"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sun, 30 Jun 2019 15:43:33 +0000 (15:43 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sun, 30 Jun 2019 15:43:33 +0000 (15:43 +0000)
177 files changed:
RELEASE-NOTES-1.34
autoload.php
includes/MediaWikiServices.php
includes/Permissions/PermissionManager.php
includes/Revision/MutableRevisionRecord.php
includes/Revision/RevisionArchiveRecord.php
includes/Revision/RevisionRecord.php
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionStore.php
includes/Revision/RevisionStoreCacheRecord.php
includes/Revision/RevisionStoreFactory.php
includes/Revision/RevisionStoreRecord.php
includes/ServiceWiring.php
includes/Storage/BlobStoreFactory.php
includes/Storage/SqlBlobStore.php
includes/db/DatabaseOracle.php
includes/externalstore/ExternalStore.php
includes/externalstore/ExternalStoreAccess.php [new file with mode: 0644]
includes/externalstore/ExternalStoreDB.php
includes/externalstore/ExternalStoreException.php [new file with mode: 0644]
includes/externalstore/ExternalStoreFactory.php
includes/externalstore/ExternalStoreMedium.php
includes/externalstore/ExternalStoreMemory.php [new file with mode: 0644]
includes/externalstore/ExternalStoreMwstore.php
includes/historyblob/HistoryBlobStub.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/rdbms/connectionmanager/ConnectionManager.php
includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactoryMulti.php
includes/libs/rdbms/lbfactory/LBFactorySimple.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/specials/SpecialContributions.php
includes/user/User.php
languages/Language.php
maintenance/storage/checkStorage.php
maintenance/storage/compressOld.php
maintenance/storage/moveToExternal.php
maintenance/storage/recompressTracked.php
tests/common/TestSetup.php
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiGroupValidator.php [new file with mode: 0644]
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/bootstrap.php
tests/phpunit/includes/FauxResponseTest.php [deleted file]
tests/phpunit/includes/FormOptionsInitializationTest.php [deleted file]
tests/phpunit/includes/FormOptionsTest.php [deleted file]
tests/phpunit/includes/LicensesTest.php [deleted file]
tests/phpunit/includes/Permissions/PermissionManagerTest.php
tests/phpunit/includes/Rest/HeaderContainerTest.php [deleted file]
tests/phpunit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php [deleted file]
tests/phpunit/includes/Rest/StringStreamTest.php [deleted file]
tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php [deleted file]
tests/phpunit/includes/Revision/RevisionRendererTest.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php
tests/phpunit/includes/Revision/RevisionStoreTest.php
tests/phpunit/includes/Revision/SlotRoleHandlerTest.php [deleted file]
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/ServiceWiringTest.php [deleted file]
tests/phpunit/includes/SiteConfigurationTest.php [deleted file]
tests/phpunit/includes/Storage/PreparedEditTest.php [deleted file]
tests/phpunit/includes/Storage/SqlBlobStoreTest.php
tests/phpunit/includes/TemplateCategoriesTest.php
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/XmlSelectTest.php [deleted file]
tests/phpunit/includes/actions/ActionTest.php
tests/phpunit/includes/api/ApiBlockTest.php
tests/phpunit/includes/api/ApiDeleteTest.php
tests/phpunit/includes/api/ApiEditPageTest.php
tests/phpunit/includes/api/ApiMainTest.php
tests/phpunit/includes/api/ApiMoveTest.php
tests/phpunit/includes/api/ApiParseTest.php
tests/phpunit/includes/api/ApiStashEditTest.php
tests/phpunit/includes/api/ApiUserrightsTest.php
tests/phpunit/includes/auth/AuthManagerTest.php
tests/phpunit/includes/auth/AuthenticationResponseTest.php [deleted file]
tests/phpunit/includes/changes/ChangesListFilterGroupTest.php [deleted file]
tests/phpunit/includes/config/HashConfigTest.php [deleted file]
tests/phpunit/includes/config/MultiConfigTest.php [deleted file]
tests/phpunit/includes/config/ServiceOptionsTest.php [deleted file]
tests/phpunit/includes/content/JsonContentHandlerTest.php [deleted file]
tests/phpunit/includes/debug/logger/MonologSpiTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php [deleted file]
tests/phpunit/includes/diff/ArrayDiffFormatterTest.php [deleted file]
tests/phpunit/includes/diff/DiffOpTest.php [deleted file]
tests/phpunit/includes/diff/DiffTest.php [deleted file]
tests/phpunit/includes/exception/MWExceptionHandlerTest.php [deleted file]
tests/phpunit/includes/externalstore/ExternalStoreAccessTest.php [new file with mode: 0644]
tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php
tests/phpunit/includes/externalstore/ExternalStoreTest.php
tests/phpunit/includes/installer/InstallDocFormatterTest.php [deleted file]
tests/phpunit/includes/installer/OracleInstallerTest.php [deleted file]
tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php [deleted file]
tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php [deleted file]
tests/phpunit/includes/media/IPTCTest.php [deleted file]
tests/phpunit/includes/media/MediaHandlerTest.php [deleted file]
tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php [deleted file]
tests/phpunit/includes/objectcache/RESTBagOStuffTest.php [deleted file]
tests/phpunit/includes/page/ArticleTablesTest.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/parser/ParserOutputTest.php
tests/phpunit/includes/parser/TidyTest.php [deleted file]
tests/phpunit/includes/password/PasswordTest.php [deleted file]
tests/phpunit/includes/preferences/FiltersTest.php [deleted file]
tests/phpunit/includes/registration/ExtensionProcessorTest.php [deleted file]
tests/phpunit/includes/search/SearchIndexFieldTest.php [deleted file]
tests/phpunit/includes/session/MetadataMergeExceptionTest.php [deleted file]
tests/phpunit/includes/session/SessionIdTest.php [deleted file]
tests/phpunit/includes/skins/SkinFactoryTest.php [deleted file]
tests/phpunit/includes/title/ForeignTitleTest.php [deleted file]
tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php [deleted file]
tests/phpunit/includes/title/TitleValueTest.php [deleted file]
tests/phpunit/includes/user/UserArrayFromResultTest.php [deleted file]
tests/phpunit/includes/user/UserGroupMembershipTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php [deleted file]
tests/phpunit/languages/SpecialPageAliasTest.php
tests/phpunit/unit/includes/FauxResponseTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/FormOptionsInitializationTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/FormOptionsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/LicensesTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Rest/HeaderContainerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Rest/StringStreamTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/FallbackSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/SlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/ServiceWiringTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/SiteConfigurationTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Storage/PreparedEditTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/XmlSelectTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/auth/AuthenticationResponseTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/changes/ChangesListFilterGroupTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/HashConfigTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/MultiConfigTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/ServiceOptionsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/content/JsonContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/MonologSpiTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/AvroFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/KafkaHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/LineFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/ArrayDiffFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/DiffOpTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/DiffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/installer/InstallDocFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/installer/OracleInstallerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/interwiki/InterwikiLookupAdapterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/objectcache/ReplicatedBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/IPTCTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/MediaHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/objectcache/MemcachedBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/objectcache/RESTBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/parser/TidyTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/password/PasswordTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/preferences/FiltersTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/registration/ExtensionProcessorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/search/SearchIndexFieldTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/MetadataMergeExceptionTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionIdTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/skins/SkinFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/title/ForeignTitleTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/title/NamespaceAwareForeignTitleFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/title/TitleValueTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/user/UserArrayFromResultTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php [new file with mode: 0644]

index fdc9e05..acd82d6 100644 (file)
@@ -327,6 +327,8 @@ because of Phabricator reports.
   engines.
 * Skin::escapeSearchLink() is deprecated. Use Skin::getSearchLink() or the skin
   template option 'searchaction' instead.
+* LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
+  been deprecated.
 
 === Other changes in 1.34 ===
 * …
index b01827a..6457747 100644 (file)
@@ -481,10 +481,13 @@ $wgAutoloadLocalClasses = [
        'ExtensionProcessor' => __DIR__ . '/includes/registration/ExtensionProcessor.php',
        'ExtensionRegistry' => __DIR__ . '/includes/registration/ExtensionRegistry.php',
        'ExternalStore' => __DIR__ . '/includes/externalstore/ExternalStore.php',
+       'ExternalStoreAccess' => __DIR__ . '/includes/externalstore/ExternalStoreAccess.php',
        'ExternalStoreDB' => __DIR__ . '/includes/externalstore/ExternalStoreDB.php',
+       'ExternalStoreException' => __DIR__ . '/includes/externalstore/ExternalStoreException.php',
        'ExternalStoreFactory' => __DIR__ . '/includes/externalstore/ExternalStoreFactory.php',
        'ExternalStoreHttp' => __DIR__ . '/includes/externalstore/ExternalStoreHttp.php',
        'ExternalStoreMedium' => __DIR__ . '/includes/externalstore/ExternalStoreMedium.php',
+       'ExternalStoreMemory' => __DIR__ . '/includes/externalstore/ExternalStoreMemory.php',
        'ExternalStoreMwstore' => __DIR__ . '/includes/externalstore/ExternalStoreMwstore.php',
        'ExternalUserNames' => __DIR__ . '/includes/user/ExternalUserNames.php',
        'FSFile' => __DIR__ . '/includes/libs/filebackend/fsfile/FSFile.php',
index 689477b..a37e32e 100644 (file)
@@ -571,6 +571,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'EventRelayerGroup' );
        }
 
+       /**
+        * @since 1.34
+        * @return \ExternalStoreAccess
+        */
+       public function getExternalStoreAccess() {
+               return $this->getService( 'ExternalStoreAccess' );
+       }
+
        /**
         * @since 1.31
         * @return \ExternalStoreFactory
index 202014f..defcb65 100644 (file)
@@ -21,12 +21,12 @@ namespace MediaWiki\Permissions;
 
 use Action;
 use Exception;
-use FatalError;
 use Hooks;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Session\SessionManager;
 use MediaWiki\Special\SpecialPageFactory;
+use MediaWiki\User\UserIdentity;
 use MessageSpecifier;
-use MWException;
 use NamespaceInfo;
 use RequestContext;
 use SpecialPage;
@@ -69,12 +69,121 @@ class PermissionManager {
        /** @var NamespaceInfo */
        private $nsInfo;
 
+       /** @var string[][] Access rights for groups and users in these groups */
+       private $groupPermissions;
+
+       /** @var string[][] Permission keys revoked from users in each group */
+       private $revokePermissions;
+
+       /** @var string[] A list of available rights, in addition to the ones defined by the core */
+       private $availableRights;
+
+       /** @var string[] Cached results of getAllRights() */
+       private $allRights = false;
+
+       /** @var string[][] Cached user rights */
+       private $usersRights = null;
+
+       /** @var string[] Cached rights for isEveryoneAllowed */
+       private $cachedRights = [];
+
+       /**
+        * Array of Strings Core rights.
+        * Each of these should have a corresponding message of the form
+        * "right-$right".
+        * @showinitializer
+        */
+       private $coreRights = [
+               'apihighlimits',
+               'applychangetags',
+               'autoconfirmed',
+               'autocreateaccount',
+               'autopatrol',
+               'bigdelete',
+               'block',
+               'blockemail',
+               'bot',
+               'browsearchive',
+               'changetags',
+               'createaccount',
+               'createpage',
+               'createtalk',
+               'delete',
+               'deletechangetags',
+               'deletedhistory',
+               'deletedtext',
+               'deletelogentry',
+               'deleterevision',
+               'edit',
+               'editcontentmodel',
+               'editinterface',
+               'editprotected',
+               'editmyoptions',
+               'editmyprivateinfo',
+               'editmyusercss',
+               'editmyuserjson',
+               'editmyuserjs',
+               'editmywatchlist',
+               'editsemiprotected',
+               'editsitecss',
+               'editsitejson',
+               'editsitejs',
+               'editusercss',
+               'edituserjson',
+               'edituserjs',
+               'hideuser',
+               'import',
+               'importupload',
+               'ipblock-exempt',
+               'managechangetags',
+               'markbotedits',
+               'mergehistory',
+               'minoredit',
+               'move',
+               'movefile',
+               'move-categorypages',
+               'move-rootuserpages',
+               'move-subpages',
+               'nominornewtalk',
+               'noratelimit',
+               'override-export-depth',
+               'pagelang',
+               'patrol',
+               'patrolmarks',
+               'protect',
+               'purge',
+               'read',
+               'reupload',
+               'reupload-own',
+               'reupload-shared',
+               'rollback',
+               'sendemail',
+               'siteadmin',
+               'suppressionlog',
+               'suppressredirect',
+               'suppressrevision',
+               'unblockself',
+               'undelete',
+               'unwatchedpages',
+               'upload',
+               'upload_by_url',
+               'userrights',
+               'userrights-interwiki',
+               'viewmyprivateinfo',
+               'viewmywatchlist',
+               'viewsuppressed',
+               'writeapi',
+       ];
+
        /**
         * @param SpecialPageFactory $specialPageFactory
         * @param string[] $whitelistRead
         * @param string[] $whitelistReadRegexp
         * @param bool $emailConfirmToEdit
         * @param bool $blockDisablesLogin
+        * @param string[][] $groupPermissions
+        * @param string[][] $revokePermissions
+        * @param string[] $availableRights
         * @param NamespaceInfo $nsInfo
         */
        public function __construct(
@@ -83,6 +192,9 @@ class PermissionManager {
                $whitelistReadRegexp,
                $emailConfirmToEdit,
                $blockDisablesLogin,
+               $groupPermissions,
+               $revokePermissions,
+               $availableRights,
                NamespaceInfo $nsInfo
        ) {
                $this->specialPageFactory = $specialPageFactory;
@@ -90,6 +202,9 @@ class PermissionManager {
                $this->whitelistReadRegexp = $whitelistReadRegexp;
                $this->emailConfirmToEdit = $emailConfirmToEdit;
                $this->blockDisablesLogin = $blockDisablesLogin;
+               $this->groupPermissions = $groupPermissions;
+               $this->revokePermissions = $revokePermissions;
+               $this->availableRights = $availableRights;
                $this->nsInfo = $nsInfo;
        }
 
@@ -111,7 +226,6 @@ class PermissionManager {
         *   - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
         *
         * @return bool
-        * @throws Exception
         */
        public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
                return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
@@ -133,7 +247,6 @@ class PermissionManager {
         *   whose corresponding errors may be ignored.
         *
         * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
-        * @throws Exception
         */
        public function getPermissionErrors(
                $action,
@@ -167,8 +280,6 @@ class PermissionManager {
         * @param bool $fromReplica Whether to check the replica DB instead of the master
         *
         * @return bool
-        * @throws FatalError
-        * @throws MWException
         */
        public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
                $blocked = $user->isHidden();
@@ -286,8 +397,6 @@ class PermissionManager {
         * @param LinkTarget $page
         *
         * @return array List of errors
-        * @throws FatalError
-        * @throws MWException
         */
        private function checkPermissionHooks(
                $action,
@@ -363,8 +472,6 @@ class PermissionManager {
         * @param LinkTarget $page
         *
         * @return array List of errors
-        * @throws FatalError
-        * @throws MWException
         */
        private function checkReadPermissions(
                $action,
@@ -497,7 +604,6 @@ class PermissionManager {
         * @param LinkTarget $page
         *
         * @return array List of errors
-        * @throws MWException
         */
        private function checkUserBlock(
                $action,
@@ -583,8 +689,6 @@ class PermissionManager {
         * @param LinkTarget $page
         *
         * @return array List of errors
-        * @throws FatalError
-        * @throws MWException
         */
        private function checkQuickPermissions(
                $action,
@@ -762,6 +866,7 @@ class PermissionManager {
                                        }
                                        if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
                                                $wikiPages = '';
+                                               /** @var Title $wikiPage */
                                                foreach ( $cascadingSources as $wikiPage ) {
                                                        $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
                                                }
@@ -789,7 +894,6 @@ class PermissionManager {
         * @param LinkTarget $page
         *
         * @return array List of errors
-        * @throws Exception
         */
        private function checkActionPermissions(
                $action,
@@ -1052,4 +1156,256 @@ class PermissionManager {
                return $errors;
        }
 
+       /**
+        * Testing a permission
+        *
+        * @since 1.34
+        *
+        * @param UserIdentity $user
+        * @param string $action
+        *
+        * @return bool
+        */
+       public function userHasRight( UserIdentity $user, $action = '' ) {
+               if ( $action === '' ) {
+                       return true; // In the spirit of DWIM
+               }
+               // Use strict parameter to avoid matching numeric 0 accidentally inserted
+               // by misconfiguration: 0 == 'foo'
+               return in_array( $action, $this->getUserPermissions( $user ), true );
+       }
+
+       /**
+        * Get the permissions this user has.
+        *
+        * @since 1.34
+        *
+        * @param UserIdentity $user
+        *
+        * @return string[] permission names
+        */
+       public function getUserPermissions( UserIdentity $user ) {
+               $user = User::newFromIdentity( $user );
+               if ( !isset( $this->usersRights[ $user->getId() ] ) ) {
+                       $this->usersRights[ $user->getId() ] = $this->getGroupPermissions(
+                               $user->getEffectiveGroups()
+                       );
+                       Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] );
+
+                       // Deny any rights denied by the user's session, unless this
+                       // endpoint has no sessions.
+                       if ( !defined( 'MW_NO_SESSION' ) ) {
+                               // FIXME: $user->getRequest().. need to be replaced with something else
+                               $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
+                               if ( $allowedRights !== null ) {
+                                       $this->usersRights[ $user->getId() ] = array_intersect(
+                                               $this->usersRights[ $user->getId() ],
+                                               $allowedRights
+                                       );
+                               }
+                       }
+
+                       Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] );
+                       // Force reindexation of rights when a hook has unset one of them
+                       $this->usersRights[ $user->getId() ] = array_values(
+                               array_unique( $this->usersRights[ $user->getId() ] )
+                       );
+
+                       if (
+                               $user->isLoggedIn() &&
+                               $this->blockDisablesLogin &&
+                               $user->getBlock()
+                       ) {
+                               $anon = new User;
+                               $this->usersRights[ $user->getId() ] = array_intersect(
+                                       $this->usersRights[ $user->getId() ],
+                                       $this->getUserPermissions( $anon )
+                               );
+                       }
+               }
+               return $this->usersRights[ $user->getId() ];
+       }
+
+       /**
+        * Clears users permissions cache, if specific user is provided it tries to clear
+        * permissions cache only for provided user.
+        *
+        * @since 1.34
+        *
+        * @param User|null $user
+        */
+       public function invalidateUsersRightsCache( $user = null ) {
+               if ( $user !== null ) {
+                       if ( isset( $this->usersRights[ $user->getId() ] ) ) {
+                               unset( $this->usersRights[$user->getId()] );
+                       }
+               } else {
+                       $this->usersRights = null;
+               }
+       }
+
+       /**
+        * Check, if the given group has the given permission
+        *
+        * If you're wanting to check whether all users have a permission, use
+        * PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked
+        * from anyone.
+        *
+        * @since 1.34
+        *
+        * @param string $group Group to check
+        * @param string $role Role to check
+        *
+        * @return bool
+        */
+       public function groupHasPermission( $group, $role ) {
+               return isset( $this->groupPermissions[$group][$role] ) &&
+                          $this->groupPermissions[$group][$role] &&
+                          !( isset( $this->revokePermissions[$group][$role] ) &&
+                                 $this->revokePermissions[$group][$role] );
+       }
+
+       /**
+        * Get the permissions associated with a given list of groups
+        *
+        * @since 1.34
+        *
+        * @param array $groups Array of Strings List of internal group names
+        * @return array Array of Strings List of permission key names for given groups combined
+        */
+       public function getGroupPermissions( $groups ) {
+               $rights = [];
+               // grant every granted permission first
+               foreach ( $groups as $group ) {
+                       if ( isset( $this->groupPermissions[$group] ) ) {
+                               $rights = array_merge( $rights,
+                                       // array_filter removes empty items
+                                       array_keys( array_filter( $this->groupPermissions[$group] ) ) );
+                       }
+               }
+               // now revoke the revoked permissions
+               foreach ( $groups as $group ) {
+                       if ( isset( $this->revokePermissions[$group] ) ) {
+                               $rights = array_diff( $rights,
+                                       array_keys( array_filter( $this->revokePermissions[$group] ) ) );
+                       }
+               }
+               return array_unique( $rights );
+       }
+
+       /**
+        * Get all the groups who have a given permission
+        *
+        * @since 1.34
+        *
+        * @param string $role Role to check
+        * @return array Array of Strings List of internal group names with the given permission
+        */
+       public function getGroupsWithPermission( $role ) {
+               $allowedGroups = [];
+               foreach ( array_keys( $this->groupPermissions ) as $group ) {
+                       if ( $this->groupHasPermission( $group, $role ) ) {
+                               $allowedGroups[] = $group;
+                       }
+               }
+               return $allowedGroups;
+       }
+
+       /**
+        * Check if all users may be assumed to have the given permission
+        *
+        * We generally assume so if the right is granted to '*' and isn't revoked
+        * on any group. It doesn't attempt to take grants or other extension
+        * limitations on rights into account in the general case, though, as that
+        * would require it to always return false and defeat the purpose.
+        * Specifically, session-based rights restrictions (such as OAuth or bot
+        * passwords) are applied based on the current session.
+        *
+        * @param string $right Right to check
+        *
+        * @return bool
+        * @since 1.34
+        */
+       public function isEveryoneAllowed( $right ) {
+               // Use the cached results, except in unit tests which rely on
+               // being able change the permission mid-request
+               if ( isset( $this->cachedRights[$right] ) ) {
+                       return $this->cachedRights[$right];
+               }
+
+               if ( !isset( $this->groupPermissions['*'][$right] )
+                        || !$this->groupPermissions['*'][$right] ) {
+                       $this->cachedRights[$right] = false;
+                       return false;
+               }
+
+               // If it's revoked anywhere, then everyone doesn't have it
+               foreach ( $this->revokePermissions as $rights ) {
+                       if ( isset( $rights[$right] ) && $rights[$right] ) {
+                               $this->cachedRights[$right] = false;
+                               return false;
+                       }
+               }
+
+               // Remove any rights that aren't allowed to the global-session user,
+               // unless there are no sessions for this endpoint.
+               if ( !defined( 'MW_NO_SESSION' ) ) {
+
+                       // XXX: think what could be done with the below
+                       $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
+                       if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
+                               $this->cachedRights[$right] = false;
+                               return false;
+                       }
+               }
+
+               // Allow extensions to say false
+               if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
+                       $this->cachedRights[$right] = false;
+                       return false;
+               }
+
+               $this->cachedRights[$right] = true;
+               return true;
+       }
+
+       /**
+        * Get a list of all available permissions.
+        *
+        * @since 1.34
+        *
+        * @return string[] Array of permission names
+        */
+       public function getAllPermissions() {
+               if ( $this->allRights === false ) {
+                       if ( count( $this->availableRights ) ) {
+                               $this->allRights = array_unique( array_merge(
+                                       $this->coreRights,
+                                       $this->availableRights
+                               ) );
+                       } else {
+                               $this->allRights = $this->coreRights;
+                       }
+                       Hooks::run( 'UserGetAllRights', [ &$this->allRights ] );
+               }
+               return $this->allRights;
+       }
+
+       /**
+        * Overrides user permissions cache
+        *
+        * @since 1.34
+        *
+        * @param User $user
+        * @param string[]|string $rights
+        *
+        * @throws Exception
+        */
+       public function overrideUserRightsForTesting( $user, $rights = [] ) {
+               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       throw new Exception( __METHOD__ . ' can not be called outside of tests' );
+               }
+               $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ];
+       }
+
 }
index f287c05..e9136cb 100644 (file)
@@ -70,15 +70,14 @@ class MutableRevisionRecord extends RevisionRecord {
         * in RevisionStore instead.
         *
         * @param Title $title The title of the page this Revision is associated with.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one.
         *
         * @throws MWException
         */
-       function __construct( Title $title, $wikiId = false ) {
+       function __construct( Title $title, $dbDomain = false ) {
                $slots = new MutableRevisionSlots();
 
-               parent::__construct( $title, $slots, $wikiId );
+               parent::__construct( $title, $slots, $dbDomain );
 
                $this->mSlots = $slots; // redundant, but nice for static analysis
        }
index 67dc9b2..6e8db7f 100644 (file)
@@ -54,8 +54,7 @@ class RevisionArchiveRecord extends RevisionRecord {
         * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
         *        a query that yields the required fields.
         * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one.
         */
        function __construct(
                Title $title,
@@ -63,9 +62,9 @@ class RevisionArchiveRecord extends RevisionRecord {
                CommentStoreComment $comment,
                $row,
                RevisionSlots $slots,
-               $wikiId = false
+               $dbDomain = false
        ) {
-               parent::__construct( $title, $slots, $wikiId );
+               parent::__construct( $title, $slots, $dbDomain );
                Assert::parameterType( 'object', $row, '$row' );
 
                $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
index 70a891c..0dcc35c 100644 (file)
@@ -94,17 +94,16 @@ abstract class RevisionRecord {
         *
         * @param Title $title The title of the page this Revision is associated with.
         * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one.
         *
         * @throws MWException
         */
-       function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
-               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+       function __construct( Title $title, RevisionSlots $slots, $dbDomain = false ) {
+               Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
 
                $this->mTitle = $title;
                $this->mSlots = $slots;
-               $this->mWiki = $wikiId;
+               $this->mWiki = $dbDomain;
 
                // XXX: this is a sensible default, but we may not have a Title object here in the future.
                $this->mPageId = $title->getArticleID();
@@ -515,10 +514,19 @@ abstract class RevisionRecord {
                        } else {
                                $permissions = [ 'deletedhistory' ];
                        }
+
+                       // XXX: How can we avoid global scope here?
+                       //      Perhaps the audience check should be done in a callback.
+                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
                        $permissionlist = implode( ', ', $permissions );
                        if ( $title === null ) {
                                wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
-                               return $user->isAllowedAny( ...$permissions );
+                               foreach ( $permissions as $perm ) {
+                                       if ( $permissionManager->userHasRight( $user, $perm ) ) {
+                                               return true;
+                                       }
+                               }
+                               return false;
                        } else {
                                $text = $title->getPrefixedText();
                                wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
index a63e4f1..99150c1 100644 (file)
@@ -54,22 +54,21 @@ class RevisionRenderer {
        private $roleRegistery;
 
        /** @var string|bool */
-       private $wikiId;
+       private $dbDomain;
 
        /**
         * @param ILoadBalancer $loadBalancer
         * @param SlotRoleRegistry $roleRegistry
-        * @param bool|string $wikiId
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one
         */
        public function __construct(
                ILoadBalancer $loadBalancer,
                SlotRoleRegistry $roleRegistry,
-               $wikiId = false
+               $dbDomain = false
        ) {
                $this->loadBalancer = $loadBalancer;
                $this->roleRegistery = $roleRegistry;
-               $this->wikiId = $wikiId;
-
+               $this->dbDomain = $dbDomain;
                $this->saveParseLogger = new NullLogger();
        }
 
@@ -105,7 +104,7 @@ class RevisionRenderer {
                User $forUser = null,
                array $hints = []
        ) {
-               if ( $rev->getWikiId() !== $this->wikiId ) {
+               if ( $rev->getWikiId() !== $this->dbDomain ) {
                        throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
                }
 
@@ -169,7 +168,7 @@ class RevisionRenderer {
                $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
                        ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
 
-               $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->wikiId, $flags );
+               $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
 
                return 1 + (int)$db->selectField(
                        'revision',
@@ -216,7 +215,7 @@ class RevisionRenderer {
                        $slotOutput[$role] = $out;
 
                        // XXX: should the SlotRoleHandler be able to intervene here?
-                       $combinedOutput->mergeInternalMetaDataFrom( $out, $role );
+                       $combinedOutput->mergeInternalMetaDataFrom( $out );
                        $combinedOutput->mergeTrackingMetaDataFrom( $out );
                }
 
index 56867eb..f269afe 100644 (file)
@@ -87,7 +87,7 @@ class RevisionStore
        /**
         * @var bool|string
         */
-       private $wikiId;
+       private $dbDomain;
 
        /**
         * @var boolean
@@ -142,7 +142,7 @@ class RevisionStore
         * @param ILoadBalancer $loadBalancer
         * @param SqlBlobStore $blobStore
         * @param WANObjectCache $cache A cache for caching revision rows. This can be the local
-        *        wiki's default instance even if $wikiId refers to a different wiki, since
+        *        wiki's default instance even if $dbDomain refers to a different wiki, since
         *        makeGlobalKey() is used to constructed a key that allows cached revision rows from
         *        the same database to be re-used between wikis. For example, enwiki and frwiki will
         *        use the same cache keys for revision rows from the wikidatawiki database, regardless
@@ -153,8 +153,7 @@ class RevisionStore
         * @param SlotRoleRegistry $slotRoleRegistry
         * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
         * @param ActorMigration $actorMigration
-        * @param bool|string $wikiId
-        *
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one
         */
        public function __construct(
                ILoadBalancer $loadBalancer,
@@ -166,9 +165,9 @@ class RevisionStore
                SlotRoleRegistry $slotRoleRegistry,
                $mcrMigrationStage,
                ActorMigration $actorMigration,
-               $wikiId = false
+               $dbDomain = false
        ) {
-               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+               Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
                Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
                Assert::parameter(
                        ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
@@ -207,7 +206,7 @@ class RevisionStore
                $this->slotRoleRegistry = $slotRoleRegistry;
                $this->mcrMigrationStage = $mcrMigrationStage;
                $this->actorMigration = $actorMigration;
-               $this->wikiId = $wikiId;
+               $this->dbDomain = $dbDomain;
                $this->logger = new NullLogger();
        }
 
@@ -227,7 +226,7 @@ class RevisionStore
         * @throws RevisionAccessException
         */
        private function assertCrossWikiContentLoadingIsSafe() {
-               if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+               if ( $this->dbDomain !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
                        throw new RevisionAccessException(
                                "Cross-wiki content loading is not supported by the pre-MCR schema"
                        );
@@ -285,7 +284,7 @@ class RevisionStore
         */
        private function getDBConnection( $mode, $groups = [] ) {
                $lb = $this->getDBLoadBalancer();
-               return $lb->getConnection( $mode, $groups, $this->wikiId );
+               return $lb->getConnection( $mode, $groups, $this->dbDomain );
        }
 
        /**
@@ -313,7 +312,7 @@ class RevisionStore
         */
        private function getDBConnectionRef( $mode ) {
                $lb = $this->getDBLoadBalancer();
-               return $lb->getConnectionRef( $mode, [], $this->wikiId );
+               return $lb->getConnectionRef( $mode, [], $this->dbDomain );
        }
 
        /**
@@ -341,7 +340,7 @@ class RevisionStore
                        $queryFlags = self::READ_NORMAL;
                }
 
-               $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
+               $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
                list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
                $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
 
@@ -631,7 +630,7 @@ class RevisionStore
                        $comment,
                        (object)$revisionRow,
                        new RevisionSlots( $newSlots ),
-                       $this->wikiId
+                       $this->dbDomain
                );
 
                return $rev;
@@ -813,9 +812,11 @@ class RevisionStore
                                                throw new MWException( 'Failed to get database lock for T202032' );
                                        }
                                        $fname = __METHOD__;
-                                       $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
-                                               $dbw->unlock( 'fix-for-T202032', $fname );
-                                       } );
+                                       $dbw->onTransactionResolution(
+                                               function ( $trigger, IDatabase $dbw ) use ( $fname ) {
+                                                       $dbw->unlock( 'fix-for-T202032', $fname );
+                                               }
+                                       );
 
                                        $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
 
@@ -1782,7 +1783,7 @@ class RevisionStore
                                $row->ar_user ?? null,
                                $row->ar_user_text ?? null,
                                $row->ar_actor ?? null,
-                               $this->wikiId
+                               $this->dbDomain
                        );
                } catch ( InvalidArgumentException $ex ) {
                        wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
@@ -1795,7 +1796,7 @@ class RevisionStore
 
                $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title );
 
-               return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+               return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
        }
 
        /**
@@ -1863,7 +1864,7 @@ class RevisionStore
                                $row->rev_user ?? null,
                                $row->rev_user_text ?? null,
                                $row->rev_actor ?? null,
-                               $this->wikiId
+                               $this->dbDomain
                        );
                } catch ( InvalidArgumentException $ex ) {
                        wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
@@ -1886,11 +1887,11 @@ class RevisionStore
                                                [ 'rev_id' => intval( $revId ) ]
                                        );
                                },
-                               $title, $user, $comment, $row, $slots, $this->wikiId
+                               $title, $user, $comment, $row, $slots, $this->dbDomain
                        );
                } else {
                        $rev = new RevisionStoreRecord(
-                               $title, $user, $comment, $row, $slots, $this->wikiId );
+                               $title, $user, $comment, $row, $slots, $this->dbDomain );
                }
                return $rev;
        }
@@ -1975,7 +1976,7 @@ class RevisionStore
                        }
                }
 
-               $revision = new MutableRevisionRecord( $title, $this->wikiId );
+               $revision = new MutableRevisionRecord( $title, $this->dbDomain );
                $this->initializeMutableRevisionFromArray( $revision, $fields );
 
                if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
@@ -2006,7 +2007,7 @@ class RevisionStore
                // remote wiki with unsuppressed ids, due to issues described in T222212.
                if ( isset( $fields['user'] ) &&
                        ( $fields['user'] instanceof UserIdentity ) &&
-                       ( $this->wikiId === false ||
+                       ( $this->dbDomain === false ||
                                ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
                ) {
                        $user = $fields['user'];
@@ -2016,7 +2017,7 @@ class RevisionStore
                                        $fields['user'] ?? null,
                                        $fields['user_text'] ?? null,
                                        $fields['actor'] ?? null,
-                                       $this->wikiId
+                                       $this->dbDomain
                                );
                        } catch ( InvalidArgumentException $ex ) {
                                $user = null;
@@ -2247,7 +2248,7 @@ class RevisionStore
         * @throws MWException
         */
        private function checkDatabaseWikiId( IDatabase $db ) {
-               $storeWiki = $this->wikiId;
+               $storeWiki = $this->dbDomain;
                $dbWiki = $db->getDomainID();
 
                if ( $dbWiki === $storeWiki ) {
index ef5f10e..0420d34 100644 (file)
@@ -53,8 +53,7 @@ class RevisionStoreCacheRecord extends RevisionStoreRecord {
         * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
         *        a query that yields the required fields.
         * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one.
         */
        function __construct(
                $callback,
@@ -63,9 +62,9 @@ class RevisionStoreCacheRecord extends RevisionStoreRecord {
                CommentStoreComment $comment,
                $row,
                RevisionSlots $slots,
-               $wikiId = false
+               $dbDomain = false
        ) {
-               parent::__construct( $title, $user, $comment, $row, $slots, $wikiId );
+               parent::__construct( $title, $user, $comment, $row, $slots, $dbDomain );
                $this->mCallback = $callback;
        }
 
index 6b3117f..0475557 100644 (file)
@@ -116,24 +116,24 @@ class RevisionStoreFactory {
        /**
         * @since 1.32
         *
-        * @param bool|string $wikiId false for the current domain / wikid
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one
         *
         * @return RevisionStore for the given wikiId with all necessary services and a logger
         */
-       public function getRevisionStore( $wikiId = false ) {
-               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+       public function getRevisionStore( $dbDomain = false ) {
+               Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
 
                $store = new RevisionStore(
-                       $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
-                       $this->blobStoreFactory->newSqlBlobStore( $wikiId ),
+                       $this->dbLoadBalancerFactory->getMainLB( $dbDomain ),
+                       $this->blobStoreFactory->newSqlBlobStore( $dbDomain ),
                        $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
                        $this->commentStore,
-                       $this->nameTables->getContentModels( $wikiId ),
-                       $this->nameTables->getSlotRoles( $wikiId ),
+                       $this->nameTables->getContentModels( $dbDomain ),
+                       $this->nameTables->getSlotRoles( $dbDomain ),
                        $this->slotRoleRegistry,
                        $this->mcrMigrationStage,
                        $this->actorMigration,
-                       $wikiId
+                       $dbDomain
                );
 
                $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) );
index 955cc82..469e494 100644 (file)
@@ -51,8 +51,7 @@ class RevisionStoreRecord extends RevisionRecord {
         * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
         *        a query that yields the required fields.
         * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
+        * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one.
         */
        function __construct(
                Title $title,
@@ -60,9 +59,9 @@ class RevisionStoreRecord extends RevisionRecord {
                CommentStoreComment $comment,
                $row,
                RevisionSlots $slots,
-               $wikiId = false
+               $dbDomain = false
        ) {
-               parent::__construct( $title, $slots, $wikiId );
+               parent::__construct( $title, $slots, $dbDomain );
                Assert::parameterType( 'object', $row, '$row' );
 
                $this->mId = intval( $row->rev_id );
index e371b5a..96baf14 100644 (file)
@@ -82,6 +82,7 @@ return [
        'BlobStoreFactory' => function ( MediaWikiServices $services ) : BlobStoreFactory {
                return new BlobStoreFactory(
                        $services->getDBLoadBalancerFactory(),
+                       $services->getExternalStoreAccess(),
                        $services->getMainWANObjectCache(),
                        new ServiceOptions( BlobStoreFactory::$constructorOptions,
                                $services->getMainConfig() ),
@@ -201,11 +202,22 @@ return [
                return new EventRelayerGroup( $services->getMainConfig()->get( 'EventRelayerConfig' ) );
        },
 
+       'ExternalStoreAccess' => function ( MediaWikiServices $services ) : ExternalStoreAccess {
+               return new ExternalStoreAccess(
+                       $services->getExternalStoreFactory(),
+                       LoggerFactory::getInstance( 'ExternalStore' )
+               );
+       },
+
        'ExternalStoreFactory' => function ( MediaWikiServices $services ) : ExternalStoreFactory {
                $config = $services->getMainConfig();
+               $writeStores = $config->get( 'DefaultExternalStore' );
 
                return new ExternalStoreFactory(
-                       $config->get( 'ExternalStores' )
+                       $config->get( 'ExternalStores' ),
+                       ( $writeStores !== false ) ? (array)$writeStores : [],
+                       $services->getDBLoadBalancer()->getLocalDomainID(),
+                       LoggerFactory::getInstance( 'ExternalStore' )
                );
        },
 
@@ -464,6 +476,9 @@ return [
                        $config->get( 'WhitelistReadRegexp' ),
                        $config->get( 'EmailConfirmToEdit' ),
                        $config->get( 'BlockDisablesLogin' ),
+                       $config->get( 'GroupPermissions' ),
+                       $config->get( 'RevokePermissions' ),
+                       $config->get( 'AvailableRights' ),
                        $services->getNamespaceInfo()
                );
        },
index 8262446..b59c68d 100644 (file)
@@ -24,6 +24,7 @@ use Language;
 use MediaWiki\Config\ServiceOptions;
 use WANObjectCache;
 use Wikimedia\Rdbms\ILBFactory;
+use ExternalStoreAccess;
 
 /**
  * Service for instantiating BlobStores
@@ -39,6 +40,11 @@ class BlobStoreFactory {
         */
        private $lbFactory;
 
+       /**
+        * @var ExternalStoreAccess
+        */
+       private $extStoreAccess;
+
        /**
         * @var WANObjectCache
         */
@@ -69,6 +75,7 @@ class BlobStoreFactory {
 
        public function __construct(
                ILBFactory $lbFactory,
+               ExternalStoreAccess $extStoreAccess,
                WANObjectCache $cache,
                ServiceOptions $options,
                Language $contLang
@@ -76,6 +83,7 @@ class BlobStoreFactory {
                $options->assertRequiredOptions( self::$constructorOptions );
 
                $this->lbFactory = $lbFactory;
+               $this->extStoreAccess = $extStoreAccess;
                $this->cache = $cache;
                $this->options = $options;
                $this->contLang = $contLang;
@@ -103,6 +111,7 @@ class BlobStoreFactory {
                $lb = $this->lbFactory->getMainLB( $dbDomain );
                $store = new SqlBlobStore(
                        $lb,
+                       $this->extStoreAccess,
                        $this->cache,
                        $dbDomain
                );
index 7fe5643..5260754 100644 (file)
 namespace MediaWiki\Storage;
 
 use DBAccessObjectUtils;
-use ExternalStore;
 use IDBAccessObject;
 use IExpiringStore;
 use InvalidArgumentException;
 use Language;
 use MWException;
 use WANObjectCache;
+use ExternalStoreAccess;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\ILoadBalancer;
@@ -56,13 +56,18 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
         */
        private $dbLoadBalancer;
 
+       /**
+        * @var ExternalStoreAccess
+        */
+       private $extStoreAccess;
+
        /**
         * @var WANObjectCache
         */
        private $cache;
 
        /**
-        * @var bool|string Wiki ID
+        * @var string|bool DB domain ID of a wiki or false for the local one
         */
        private $dbDomain;
 
@@ -93,6 +98,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
 
        /**
         * @param ILoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
+        * @param ExternalStoreAccess $extStoreAccess Access layer for external storage
         * @param WANObjectCache $cache A cache manager for caching blobs. This can be the local
         *        wiki's default instance even if $dbDomain refers to a different wiki, since
         *        makeGlobalKey() is used to constructed a key that allows cached blobs from the
@@ -103,10 +109,12 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
         */
        public function __construct(
                ILoadBalancer $dbLoadBalancer,
+               ExternalStoreAccess $extStoreAccess,
                WANObjectCache $cache,
                $dbDomain = false
        ) {
                $this->dbLoadBalancer = $dbLoadBalancer;
+               $this->extStoreAccess = $extStoreAccess;
                $this->cache = $cache;
                $this->dbDomain = $dbDomain;
        }
@@ -219,7 +227,10 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
                        # Write to external storage if required
                        if ( $this->useExternalStore ) {
                                // Store and get the URL
-                               $data = ExternalStore::insertToDefault( $data, [ 'wiki' => $this->dbDomain ] );
+                               $data = $this->extStoreAccess->insert( $data, [ 'domain' => $this->dbDomain ] );
+                               if ( !$data ) {
+                                       throw new BlobAccessException( "Failed to store text to external storage" );
+                               }
                                if ( $flags ) {
                                        $flags .= ',';
                                }
@@ -412,14 +423,15 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
                                        $this->getCacheTTL(),
                                        function () use ( $url, $flags ) {
                                                // Ignore $setOpts; blobs are immutable and negatives are not cached
-                                               $blob = ExternalStore::fetchFromURL( $url, [ 'wiki' => $this->dbDomain ] );
+                                               $blob = $this->extStoreAccess
+                                                       ->fetchFromURL( $url, [ 'domain' => $this->dbDomain ] );
 
                                                return $blob === false ? false : $this->decompressData( $blob, $flags );
                                        },
                                        [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ]
                                );
                        } else {
-                               $blob = ExternalStore::fetchFromURL( $url, [ 'wiki' => $this->dbDomain ] );
+                               $blob = $this->extStoreAccess->fetchFromURL( $url, [ 'domain' => $this->dbDomain ] );
                                return $blob === false ? false : $this->decompressData( $blob, $flags );
                        }
                } else {
@@ -623,7 +635,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
        }
 
        public function isReadOnly() {
-               if ( $this->useExternalStore && ExternalStore::defaultStoresAreReadOnly() ) {
+               if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
                        return true;
                }
 
index 4af62a0..c716e4d 100644 (file)
@@ -178,7 +178,7 @@ class DatabaseOracle extends Database {
        }
 
        function execFlags() {
-               return $this->trxLevel ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
+               return $this->trxLevel() ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
        }
 
        /**
@@ -548,7 +548,7 @@ class DatabaseOracle extends Database {
                        }
                }
 
-               if ( !$this->trxLevel ) {
+               if ( !$this->trxLevel() ) {
                        oci_commit( $this->conn );
                }
 
@@ -942,26 +942,24 @@ class DatabaseOracle extends Database {
        }
 
        protected function doBegin( $fname = __METHOD__ ) {
-               $this->trxLevel = 1;
-               $this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' );
+               $this->query( 'SET CONSTRAINTS ALL DEFERRED' );
        }
 
        protected function doCommit( $fname = __METHOD__ ) {
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        $ret = oci_commit( $this->conn );
                        if ( !$ret ) {
                                throw new DBUnexpectedError( $this, $this->lastError() );
                        }
-                       $this->trxLevel = 0;
-                       $this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' );
+                       $this->query( 'SET CONSTRAINTS ALL IMMEDIATE' );
                }
        }
 
        protected function doRollback( $fname = __METHOD__ ) {
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        oci_rollback( $this->conn );
-                       $this->trxLevel = 0;
-                       $this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' );
+                       $ignoreErrors = true;
+                       $this->query( 'SET CONSTRAINTS ALL IMMEDIATE', $fname, $ignoreErrors );
                }
        }
 
@@ -1338,7 +1336,7 @@ class DatabaseOracle extends Database {
                        }
                }
 
-               if ( !$this->trxLevel ) {
+               if ( !$this->trxLevel() ) {
                        oci_commit( $this->conn );
                }
 
index 76f20f0..7c90b35 100644 (file)
@@ -44,6 +44,7 @@ use MediaWiki\MediaWikiServices;
  * as the possibility to have any storage format (i.e. for archives).
  *
  * @ingroup ExternalStorage
+ * @deprecated 1.34 Use ExternalStoreFactory directly instead
  */
 class ExternalStore {
        /**
@@ -52,11 +53,16 @@ class ExternalStore {
         * @param string $proto Type of external storage, should be a value in $wgExternalStores
         * @param array $params Associative array of ExternalStoreMedium parameters
         * @return ExternalStoreMedium|bool The store class or false on error
+        * @deprecated 1.34
         */
        public static function getStoreObject( $proto, array $params = [] ) {
-               return MediaWikiServices::getInstance()
-                       ->getExternalStoreFactory()
-                       ->getStoreObject( $proto, $params );
+               try {
+                       return MediaWikiServices::getInstance()
+                               ->getExternalStoreFactory()
+                               ->getStore( $proto, $params );
+               } catch ( ExternalStoreException $e ) {
+                       return false;
+               }
        }
 
        /**
@@ -66,59 +72,16 @@ class ExternalStore {
         * @param array $params Associative array of ExternalStoreMedium parameters
         * @return string|bool The text stored or false on error
         * @throws MWException
+        * @deprecated 1.34
         */
        public static function fetchFromURL( $url, array $params = [] ) {
-               $parts = explode( '://', $url, 2 );
-               if ( count( $parts ) != 2 ) {
-                       return false; // invalid URL
-               }
-
-               list( $proto, $path ) = $parts;
-               if ( $path == '' ) { // bad URL
-                       return false;
-               }
-
-               $store = self::getStoreObject( $proto, $params );
-               if ( $store === false ) {
+               try {
+                       return MediaWikiServices::getInstance()
+                               ->getExternalStoreAccess()
+                               ->fetchFromURL( $url, $params );
+               } catch ( ExternalStoreException $e ) {
                        return false;
                }
-
-               return $store->fetchFromURL( $url );
-       }
-
-       /**
-        * Fetch data from multiple URLs with a minimum of round trips
-        *
-        * @param array $urls The URLs of the text to get
-        * @return array Map from url to its data.  Data is either string when found
-        *     or false on failure.
-        * @throws MWException
-        */
-       public static function batchFetchFromURLs( array $urls ) {
-               $batches = [];
-               foreach ( $urls as $url ) {
-                       $scheme = parse_url( $url, PHP_URL_SCHEME );
-                       if ( $scheme ) {
-                               $batches[$scheme][] = $url;
-                       }
-               }
-               $retval = [];
-               foreach ( $batches as $proto => $batchedUrls ) {
-                       $store = self::getStoreObject( $proto );
-                       if ( $store === false ) {
-                               continue;
-                       }
-                       $retval += $store->batchFetchFromURLs( $batchedUrls );
-               }
-               // invalid, not found, db dead, etc.
-               $missing = array_diff( $urls, array_keys( $retval ) );
-               if ( $missing ) {
-                       foreach ( $missing as $url ) {
-                               $retval[$url] = false;
-                       }
-               }
-
-               return $retval;
        }
 
        /**
@@ -131,24 +94,30 @@ class ExternalStore {
         * @param array $params Associative array of ExternalStoreMedium parameters
         * @return string|bool The URL of the stored data item, or false on error
         * @throws MWException
+        * @deprecated 1.34
         */
        public static function insert( $url, $data, array $params = [] ) {
-               $parts = explode( '://', $url, 2 );
-               if ( count( $parts ) != 2 ) {
-                       return false; // invalid URL
-               }
+               try {
+                       $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+                       $location = $esFactory->getStoreLocationFromUrl( $url );
 
-               list( $proto, $path ) = $parts;
-               if ( $path == '' ) { // bad URL
+                       return $esFactory->getStoreForUrl( $url, $params )->store( $location, $data );
+               } catch ( ExternalStoreException $e ) {
                        return false;
                }
+       }
 
-               $store = self::getStoreObject( $proto, $params );
-               if ( $store === false ) {
-                       return false;
-               } else {
-                       return $store->store( $path, $data );
-               }
+       /**
+        * Fetch data from multiple URLs with a minimum of round trips
+        *
+        * @param array $urls The URLs of the text to get
+        * @return array Map from url to its data.  Data is either string when found
+        *     or false on failure.
+        * @throws MWException
+        * @deprecated 1.34
+        */
+       public static function batchFetchFromURLs( array $urls ) {
+               return MediaWikiServices::getInstance()->getExternalStoreAccess()->fetchFromURLs( $urls );
        }
 
        /**
@@ -161,11 +130,10 @@ class ExternalStore {
         * @param array $params Map of ExternalStoreMedium::__construct context parameters
         * @return string The URL of the stored data item
         * @throws MWException
+        * @deprecated 1.34
         */
        public static function insertToDefault( $data, array $params = [] ) {
-               global $wgDefaultExternalStore;
-
-               return self::insertWithFallback( (array)$wgDefaultExternalStore, $data, $params );
+               return MediaWikiServices::getInstance()->getExternalStoreAccess()->insert( $data, $params );
        }
 
        /**
@@ -179,67 +147,12 @@ class ExternalStore {
         * @param array $params Map of ExternalStoreMedium::__construct context parameters
         * @return string The URL of the stored data item
         * @throws MWException
+        * @deprecated 1.34
         */
        public static function insertWithFallback( array $tryStores, $data, array $params = [] ) {
-               $error = false;
-               while ( count( $tryStores ) > 0 ) {
-                       $index = mt_rand( 0, count( $tryStores ) - 1 );
-                       $storeUrl = $tryStores[$index];
-                       wfDebug( __METHOD__ . ": trying $storeUrl\n" );
-                       list( $proto, $path ) = explode( '://', $storeUrl, 2 );
-                       $store = self::getStoreObject( $proto, $params );
-                       if ( $store === false ) {
-                               throw new MWException( "Invalid external storage protocol - $storeUrl" );
-                       }
-
-                       try {
-                               if ( $store->isReadOnly( $path ) ) {
-                                       $msg = 'read only';
-                               } else {
-                                       $url = $store->store( $path, $data );
-                                       if ( $url !== false ) {
-                                               return $url; // a store accepted the write; done!
-                                       }
-                                       $msg = 'operation failed';
-                               }
-                       } catch ( Exception $error ) {
-                               $msg = 'caught exception';
-                       }
-
-                       unset( $tryStores[$index] ); // Don't try this one again!
-                       $tryStores = array_values( $tryStores ); // Must have consecutive keys
-                       wfDebugLog( 'ExternalStorage',
-                               "Unable to store text to external storage $storeUrl ($msg)" );
-               }
-               // All stores failed
-               if ( $error ) {
-                       throw $error; // rethrow the last error
-               } else {
-                       throw new MWException( "Unable to store text to external storage" );
-               }
-       }
-
-       /**
-        * @return bool Whether all the default insertion stores are marked as read-only
-        * @since 1.31
-        */
-       public static function defaultStoresAreReadOnly() {
-               global $wgDefaultExternalStore;
-
-               $tryStores = (array)$wgDefaultExternalStore;
-               if ( !$tryStores ) {
-                       return false; // no stores exists which can be "read only"
-               }
-
-               foreach ( $tryStores as $storeUrl ) {
-                       list( $proto, $path ) = explode( '://', $storeUrl, 2 );
-                       $store = self::getStoreObject( $proto, [] );
-                       if ( !$store->isReadOnly( $path ) ) {
-                               return false; // at least one store is not read-only
-                       }
-               }
-
-               return true; // all stores are read-only
+               return MediaWikiServices::getInstance()
+                       ->getExternalStoreAccess()
+                       ->insert( $data, $params, $tryStores );
        }
 
        /**
@@ -247,8 +160,11 @@ class ExternalStore {
         * @param string $wiki
         * @return string The URL of the stored data item
         * @throws MWException
+        * @deprecated 1.34 Use insertToDefault() with 'wiki' set
         */
        public static function insertToForeignDefault( $data, $wiki ) {
-               return self::insertToDefault( $data, [ 'wiki' => $wiki ] );
+               return MediaWikiServices::getInstance()
+                       ->getExternalStoreAccess()
+                       ->insert( $data, [ 'domain' => $wiki ] );
        }
 }
diff --git a/includes/externalstore/ExternalStoreAccess.php b/includes/externalstore/ExternalStoreAccess.php
new file mode 100644 (file)
index 0000000..8603cc2
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+/**
+ * @defgroup ExternalStorage ExternalStorage
+ */
+
+use \Psr\Log\LoggerAwareInterface;
+use \Psr\Log\LoggerInterface;
+use \Psr\Log\NullLogger;
+
+/**
+ * Key/value blob storage for a collection of storage medium types (e.g. RDBMs, files)
+ *
+ * Multiple medium types can be active and each one can have multiple "locations" available.
+ * Blobs are stored under URLs of the form "<protocol>://<location>/<path>". Each type of storage
+ * medium has an associated protocol. Insertions will randomly pick mediums and locations from
+ * the provided list of writable medium-qualified locations. Insertions will also fail-over to
+ * other writable locations or mediums if one or more are not available.
+ *
+ * @ingroup ExternalStorage
+ * @since 1.34
+ */
+class ExternalStoreAccess implements LoggerAwareInterface {
+       /** @var ExternalStoreFactory */
+       private $storeFactory;
+       /** @var LoggerInterface */
+       private $logger;
+
+       /**
+        * @param ExternalStoreFactory $factory
+        * @param LoggerInterface|null $logger
+        */
+       public function __construct( ExternalStoreFactory $factory, LoggerInterface $logger = null ) {
+               $this->storeFactory = $factory;
+               $this->logger = $logger ?: new NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Fetch data from given URL
+        *
+        * @see ExternalStoreFactory::getStore()
+        *
+        * @param string $url The URL of the text to get
+        * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
+        * @return string|bool The text stored or false on error
+        * @throws ExternalStoreException
+        */
+       public function fetchFromURL( $url, array $params = [] ) {
+               return $this->storeFactory->getStoreForUrl( $url, $params )->fetchFromURL( $url );
+       }
+
+       /**
+        * Fetch data from multiple URLs with a minimum of round trips
+        *
+        * @see ExternalStoreFactory::getStore()
+        *
+        * @param array $urls The URLs of the text to get
+        * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
+        * @return array Map of (url => string or false if not found)
+        * @throws ExternalStoreException
+        */
+       public function fetchFromURLs( array $urls, array $params = [] ) {
+               $batches = $this->storeFactory->getUrlsByProtocol( $urls );
+               $retval = [];
+               foreach ( $batches as $proto => $batchedUrls ) {
+                       $store = $this->storeFactory->getStore( $proto, $params );
+                       $retval += $store->batchFetchFromURLs( $batchedUrls );
+               }
+               // invalid, not found, db dead, etc.
+               $missing = array_diff( $urls, array_keys( $retval ) );
+               foreach ( $missing as $url ) {
+                       $retval[$url] = false;
+               }
+
+               return $retval;
+       }
+
+       /**
+        * Insert data into storage and return the assigned URL
+        *
+        * This will randomly pick one of the available write storage locations to put the data.
+        * It will keep failing-over to any untried storage locations whenever one location is
+        * not usable.
+        *
+        * @see ExternalStoreFactory::getStore()
+        *
+        * @param string $data
+        * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
+        * @param string[]|null $tryStores Refer to $wgDefaultExternalStore
+        * @return string|bool The URL of the stored data item, or false on error
+        * @throws ExternalStoreException
+        */
+       public function insert( $data, array $params = [], array $tryStores = null ) {
+               $tryStores = $tryStores ?? $this->storeFactory->getWriteBaseUrls();
+               if ( !$tryStores ) {
+                       throw new ExternalStoreException( "List of external stores provided is empty." );
+               }
+
+               $error = false;
+               while ( count( $tryStores ) > 0 ) {
+                       $index = mt_rand( 0, count( $tryStores ) - 1 );
+                       $storeUrl = $tryStores[$index];
+
+                       $this->logger->debug( __METHOD__ . ": trying $storeUrl\n" );
+
+                       $store = $this->storeFactory->getStoreForUrl( $storeUrl, $params );
+                       if ( $store === false ) {
+                               throw new ExternalStoreException( "Invalid external storage protocol - $storeUrl" );
+                       }
+
+                       $location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl );
+                       try {
+                               if ( $store->isReadOnly( $location ) ) {
+                                       $msg = 'read only';
+                               } else {
+                                       $url = $store->store( $location, $data );
+                                       if ( strlen( $url ) ) {
+                                               return $url; // a store accepted the write; done!
+                                       }
+                                       $msg = 'operation failed';
+                               }
+                       } catch ( Exception $error ) {
+                               $msg = 'caught ' . get_class( $error ) . ' exception: ' . $error->getMessage();
+                       }
+
+                       unset( $tryStores[$index] ); // Don't try this one again!
+                       $tryStores = array_values( $tryStores ); // Must have consecutive keys
+                       $this->logger->error(
+                               "Unable to store text to external storage {store_path} ({failure})",
+                               [ 'store_path' => $storeUrl, 'failure' => $msg ]
+                       );
+               }
+               // All stores failed
+               if ( $error ) {
+                       throw $error; // rethrow the last error
+               } else {
+                       throw new ExternalStoreException( "Unable to store text to external storage" );
+               }
+       }
+
+       /**
+        * @return bool Whether all the default insertion stores are marked as read-only
+        * @throws ExternalStoreException
+        */
+       public function isReadOnly() {
+               $writableStores = $this->storeFactory->getWriteBaseUrls();
+               if ( !$writableStores ) {
+                       return false; // no stores exists which can be "read only"
+               }
+
+               foreach ( $writableStores as $storeUrl ) {
+                       $store = $this->storeFactory->getStoreForUrl( $storeUrl );
+                       $location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl );
+                       if ( $store !== false && !$store->isReadOnly( $location ) ) {
+                               return false; // at least one store is not read-only
+                       }
+               }
+
+               return true; // all stores are read-only
+       }
+}
index 15bc3e0..feb0614 100644 (file)
@@ -20,7 +20,7 @@
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Rdbms\ILoadBalancer;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBConnRef;
@@ -36,6 +36,22 @@ use Wikimedia\Rdbms\DatabaseDomain;
  * @ingroup ExternalStorage
  */
 class ExternalStoreDB extends ExternalStoreMedium {
+       /** @var LBFactory */
+       private $lbFactory;
+
+       /**
+        * @see ExternalStoreMedium::__construct()
+        * @param array $params Additional parameters include:
+        *   - lbFactory: an LBFactory instance
+        */
+       public function __construct( array $params ) {
+               parent::__construct( $params );
+               if ( !isset( $params['lbFactory'] ) || !( $params['lbFactory'] instanceof LBFactory ) ) {
+                       throw new InvalidArgumentException( "LBFactory required in 'lbFactory' field." );
+               }
+               $this->lbFactory = $params['lbFactory'];
+       }
+
        /**
         * The provided URL is in the form of DB://cluster/id
         * or DB://cluster/id/itemid for concatened storage.
@@ -97,9 +113,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
         */
        public function store( $location, $data ) {
                $dbw = $this->getMaster( $location );
-               $dbw->insert( $this->getTable( $dbw ),
-                       [ 'blob_text' => $data ],
-                       __METHOD__ );
+               $dbw->insert( $this->getTable( $dbw ), [ 'blob_text' => $data ], __METHOD__ );
                $id = $dbw->insertId();
                if ( !$id ) {
                        throw new MWException( __METHOD__ . ': no insert ID' );
@@ -112,8 +126,13 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * @inheritDoc
         */
        public function isReadOnly( $location ) {
+               if ( parent::isReadOnly( $location ) ) {
+                       return true;
+               }
+
                $lb = $this->getLoadBalancer( $location );
                $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
+
                return ( $lb->getReadOnlyReason( $domainId ) !== false );
        }
 
@@ -124,8 +143,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * @return ILoadBalancer
         */
        private function getLoadBalancer( $cluster ) {
-               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-               return $lbFactory->getExternalLB( $cluster );
+               return $this->lbFactory->getExternalLB( $cluster );
        }
 
        /**
@@ -135,16 +153,14 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * @return DBConnRef
         */
        public function getSlave( $cluster ) {
-               global $wgDefaultExternalStore;
-
                $lb = $this->getLoadBalancer( $cluster );
                $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
 
-               if ( !in_array( "DB://" . $cluster, (array)$wgDefaultExternalStore ) ) {
-                       wfDebug( "read only external store\n" );
+               if ( !in_array( $cluster, $this->writableLocations, true ) ) {
+                       $this->logger->debug( "read only external store\n" );
                        $lb->allowLagged( true );
                } else {
-                       wfDebug( "writable external store\n" );
+                       $this->logger->debug( "writable external store\n" );
                }
 
                $db = $lb->getConnectionRef( DB_REPLICA, [], $domainId );
@@ -174,8 +190,8 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * @return string|bool Database domain ID or false
         */
        private function getDomainId( array $server ) {
-               if ( isset( $this->params['wiki'] ) && $this->params['wiki'] !== false ) {
-                       return $this->params['wiki']; // explicit domain
+               if ( $this->isDbDomainExplicit ) {
+                       return $this->dbDomain; // explicit foreign domain
                }
 
                if ( isset( $server['dbname'] ) ) {
@@ -230,33 +246,27 @@ class ExternalStoreDB extends ExternalStoreMedium {
                static $externalBlobCache = [];
 
                $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/";
-
-               $wiki = $this->params['wiki'] ?? false;
-               $cacheID = ( $wiki === false ) ? $cacheID : "$cacheID@$wiki";
+               $cacheID = "$cacheID@{$this->dbDomain}";
 
                if ( isset( $externalBlobCache[$cacheID] ) ) {
-                       wfDebugLog( 'ExternalStoreDB-cache',
-                               "ExternalStoreDB::fetchBlob cache hit on $cacheID" );
+                       $this->logger->debug( "ExternalStoreDB::fetchBlob cache hit on $cacheID" );
 
                        return $externalBlobCache[$cacheID];
                }
 
-               wfDebugLog( 'ExternalStoreDB-cache',
-                       "ExternalStoreDB::fetchBlob cache miss on $cacheID" );
+               $this->logger->debug( "ExternalStoreDB::fetchBlob cache miss on $cacheID" );
 
                $dbr = $this->getSlave( $cluster );
                $ret = $dbr->selectField( $this->getTable( $dbr ),
                        'blob_text', [ 'blob_id' => $id ], __METHOD__ );
                if ( $ret === false ) {
-                       wfDebugLog( 'ExternalStoreDB',
-                               "ExternalStoreDB::fetchBlob master fallback on $cacheID" );
+                       $this->logger->info( "ExternalStoreDB::fetchBlob master fallback on $cacheID" );
                        // Try the master
                        $dbw = $this->getMaster( $cluster );
                        $ret = $dbw->selectField( $this->getTable( $dbw ),
                                'blob_text', [ 'blob_id' => $id ], __METHOD__ );
                        if ( $ret === false ) {
-                               wfDebugLog( 'ExternalStoreDB',
-                                       "ExternalStoreDB::fetchBlob master failed to find $cacheID" );
+                               $this->logger->error( "ExternalStoreDB::fetchBlob master failed to find $cacheID" );
                        }
                }
                if ( $itemID !== false && $ret !== false ) {
@@ -279,16 +289,22 @@ class ExternalStoreDB extends ExternalStoreMedium {
         */
        private function batchFetchBlobs( $cluster, array $ids ) {
                $dbr = $this->getSlave( $cluster );
-               $res = $dbr->select( $this->getTable( $dbr ),
-                       [ 'blob_id', 'blob_text' ], [ 'blob_id' => array_keys( $ids ) ], __METHOD__ );
+               $res = $dbr->select(
+                       $this->getTable( $dbr ),
+                       [ 'blob_id', 'blob_text' ],
+                       [ 'blob_id' => array_keys( $ids ) ],
+                       __METHOD__
+               );
+
                $ret = [];
                if ( $res !== false ) {
                        $this->mergeBatchResult( $ret, $ids, $res );
                }
                if ( $ids ) {
-                       wfDebugLog( __CLASS__, __METHOD__ .
-                               " master fallback on '$cluster' for: " .
-                               implode( ',', array_keys( $ids ) ) );
+                       $this->logger->info(
+                               __METHOD__ . ": master fallback on '$cluster' for: " .
+                               implode( ',', array_keys( $ids ) )
+                       );
                        // Try the master
                        $dbw = $this->getMaster( $cluster );
                        $res = $dbw->select( $this->getTable( $dbr ),
@@ -296,15 +312,16 @@ class ExternalStoreDB extends ExternalStoreMedium {
                                [ 'blob_id' => array_keys( $ids ) ],
                                __METHOD__ );
                        if ( $res === false ) {
-                               wfDebugLog( __CLASS__, __METHOD__ . " master failed on '$cluster'" );
+                               $this->logger->error( __METHOD__ . ": master failed on '$cluster'" );
                        } else {
                                $this->mergeBatchResult( $ret, $ids, $res );
                        }
                }
                if ( $ids ) {
-                       wfDebugLog( __CLASS__, __METHOD__ .
-                               " master on '$cluster' failed locating items: " .
-                               implode( ',', array_keys( $ids ) ) );
+                       $this->logger->error(
+                               __METHOD__ . ": master on '$cluster' failed locating items: " .
+                               implode( ',', array_keys( $ids ) )
+                       );
                }
 
                return $ret;
diff --git a/includes/externalstore/ExternalStoreException.php b/includes/externalstore/ExternalStoreException.php
new file mode 100644 (file)
index 0000000..a2ef27d
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+class ExternalStoreException extends MWException {
+
+}
index 940fb2e..3f78b8b 100644 (file)
  * @defgroup ExternalStorage ExternalStorage
  */
 
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\Assert\Assert;
+
 /**
  * @ingroup ExternalStorage
  */
-class ExternalStoreFactory {
+class ExternalStoreFactory implements LoggerAwareInterface {
+       /** @var string[] List of storage access protocols */
+       private $protocols;
+       /** @var string[] List of base storage URLs that define locations for writes */
+       private $writeBaseUrls;
+       /** @var string Default database domain to store content under */
+       private $localDomainId;
+       /** @var LoggerInterface */
+       private $logger;
 
        /**
-        * @var array
+        * @param string[] $externalStores See $wgExternalStores
+        * @param string[] $defaultStores See $wgDefaultExternalStore
+        * @param string $localDomainId Local database/wiki ID
+        * @param LoggerInterface|null $logger
         */
-       private $externalStores;
+       public function __construct(
+               array $externalStores,
+               array $defaultStores,
+               $localDomainId,
+               LoggerInterface $logger = null
+       ) {
+               Assert::parameterType( 'string', $localDomainId, '$localDomainId' );
+
+               $this->protocols = array_map( 'strtolower', $externalStores );
+               $this->writeBaseUrls = $defaultStores;
+               $this->localDomainId = $localDomainId;
+               $this->logger = $logger ?: new NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
 
        /**
-        * @param array $externalStores See $wgExternalStores
+        * @return string[] List of active store types/protocols (lowercased), e.g. [ "db" ]
+        * @since 1.34
         */
-       public function __construct( array $externalStores ) {
-               $this->externalStores = array_map( 'strtolower', $externalStores );
+       public function getProtocols() {
+               return $this->protocols;
+       }
+
+       /**
+        * @return string[] List of base URLs for writes, e.g. [ "DB://cluster1" ]
+        * @since 1.34
+        */
+       public function getWriteBaseUrls() {
+               return $this->writeBaseUrls;
        }
 
        /**
         * Get an external store object of the given type, with the given parameters
         *
+        * The 'domain' field in $params will be set to the local DB domain if it is unset
+        * or false. A special 'isDomainImplicit' flag is set when this happens, which should
+        * only be used to handle legacy DB domain configuration concerns (e.g. T200471).
+        *
         * @param string $proto Type of external storage, should be a value in $wgExternalStores
-        * @param array $params Associative array of ExternalStoreMedium parameters
-        * @return ExternalStoreMedium|bool The store class or false on error
+        * @param array $params Map of ExternalStoreMedium::__construct context parameters.
+        * @return ExternalStoreMedium The store class or false on error
+        * @throws ExternalStoreException When $proto is not recognized
         */
-       public function getStoreObject( $proto, array $params = [] ) {
-               if ( !$this->externalStores || !in_array( strtolower( $proto ), $this->externalStores ) ) {
-                       // Protocol not enabled
-                       return false;
+       public function getStore( $proto, array $params = [] ) {
+               $protoLowercase = strtolower( $proto ); // normalize
+               if ( !$this->protocols || !in_array( $protoLowercase, $this->protocols ) ) {
+                       throw new ExternalStoreException( "Protocol '$proto' is not enabled." );
                }
 
                $class = 'ExternalStore' . ucfirst( $proto );
+               if ( isset( $params['wiki'] ) ) {
+                       $params += [ 'domain' => $params['wiki'] ]; // b/c
+               }
+               if ( !isset( $params['domain'] ) || $params['domain'] === false ) {
+                       $params['domain'] = $this->localDomainId; // default
+                       $params['isDomainImplicit'] = true; // b/c for ExternalStoreDB
+               }
+               $params['writableLocations'] = [];
+               // Determine the locations for this protocol/store still receiving writes
+               foreach ( $this->writeBaseUrls as $storeUrl ) {
+                       list( $storeProto, $storePath ) = self::splitStorageUrl( $storeUrl );
+                       if ( $protoLowercase === strtolower( $storeProto ) ) {
+                               $params['writableLocations'][] = $storePath;
+                       }
+               }
+               // @TODO: ideally, this class should not hardcode what classes need what backend factory
+               // objects. For now, inject the factory instances into __construct() for those that do.
+               if ( $protoLowercase === 'db' ) {
+                       $params['lbFactory'] = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               } elseif ( $protoLowercase === 'mwstore' ) {
+                       $params['fbGroup'] = FileBackendGroup::singleton();
+               }
+               $params['logger'] = $this->logger;
+
+               if ( !class_exists( $class ) ) {
+                       throw new ExternalStoreException( "Class '$class' is not defined." );
+               }
 
                // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
-               return class_exists( $class ) ? new $class( $params ) : false;
+               return new $class( $params );
+       }
+
+       /**
+        * Get the ExternalStoreMedium for a given URL
+        *
+        * $url is either of the form:
+        *   - a) "<proto>://<location>/<path>", for retrieval, or
+        *   - b) "<proto>://<location>", for storage
+        *
+        * @param string $url
+        * @param array $params Map of ExternalStoreMedium::__construct context parameters
+        * @return ExternalStoreMedium
+        * @throws ExternalStoreException When the protocol is missing or not recognized
+        * @since 1.34
+        */
+       public function getStoreForUrl( $url, array $params = [] ) {
+               list( $proto, $path ) = self::splitStorageUrl( $url );
+               if ( $path == '' ) { // bad URL
+                       throw new ExternalStoreException( "Invalid URL '$url'" );
+               }
+
+               return $this->getStore( $proto, $params );
        }
 
+       /**
+        * Get the location within the appropriate store for a given a URL
+        *
+        * @param string $url
+        * @return string
+        * @throws ExternalStoreException
+        * @since 1.34
+        */
+       public function getStoreLocationFromUrl( $url ) {
+               list( , $location ) = self::splitStorageUrl( $url );
+               if ( $location == '' ) { // bad URL
+                       throw new ExternalStoreException( "Invalid URL '$url'" );
+               }
+
+               return $location;
+       }
+
+       /**
+        * @param string[] $urls
+        * @return array[] Map of (protocol => list of URLs)
+        * @throws ExternalStoreException
+        * @since 1.34
+        */
+       public function getUrlsByProtocol( array $urls ) {
+               $urlsByProtocol = [];
+               foreach ( $urls as $url ) {
+                       list( $proto, ) = self::splitStorageUrl( $url );
+                       $urlsByProtocol[$proto][] = $url;
+               }
+
+               return $urlsByProtocol;
+       }
+
+       /**
+        * @param string $storeUrl
+        * @return string[] (protocol, store location or location-qualified path)
+        * @throws ExternalStoreException
+        */
+       private static function splitStorageUrl( $storeUrl ) {
+               $parts = explode( '://', $storeUrl );
+               if ( count( $parts ) != 2 || $parts[0] === '' || $parts[1] === '' ) {
+                       throw new ExternalStoreException( "Invalid storage URL '$storeUrl'" );
+               }
+
+               return $parts;
+       }
 }
index da7752b..0cdcf53 100644 (file)
  * @ingroup ExternalStorage
  */
 
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
 /**
- * Accessable external objects in a particular storage medium
+ * Key/value blob storage for a particular storage medium type (e.g. RDBMs, files)
+ *
+ * There can be multiple "locations" for a storage medium type (e.g. DB clusters, filesystems).
+ * Blobs are stored under URLs of the form "<protocol>://<location>/<path>". Each type of storage
+ * medium has an associated protocol.
  *
  * @ingroup ExternalStorage
  * @since 1.21
  */
-abstract class ExternalStoreMedium {
-       /** @var array */
+abstract class ExternalStoreMedium implements LoggerAwareInterface {
+       /** @var array Usage context options for this instance */
        protected $params = [];
+       /** @var string Default database domain to store content under */
+       protected $dbDomain;
+       /** @var bool Whether this was factoried with an explicit DB domain */
+       protected $isDbDomainExplicit;
+       /** @var string[] Writable locations */
+       protected $writableLocations = [];
+
+       /** @var LoggerInterface */
+       protected $logger;
 
        /**
-        * @param array $params Usage context options:
-        *   - wiki: the domain ID of the wiki this is being used for [optional]
+        * @param array $params Usage context options for this instance:
+        *   - domain: the DB domain ID of the wiki the content is for [required]
+        *   - writableLocations: locations that are writable [required]
+        *   - logger: LoggerInterface instance [optional]
+        *   - isDomainImplicit: whether this was factoried without an explicit DB domain [optional]
         */
-       public function __construct( array $params = [] ) {
+       public function __construct( array $params ) {
                $this->params = $params;
+               if ( isset( $params['domain'] ) ) {
+                       $this->dbDomain = $params['domain'];
+                       $this->isDbDomainExplicit = empty( $params['isDomainImplicit'] );
+               } else {
+                       throw new InvalidArgumentException( 'Missing DB "domain" parameter.' );
+               }
+
+               $this->logger = $params['logger'] ?? new NullLogger();
+               $this->writableLocations = $params['writableLocations'] ?? [];
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
        }
 
        /**
@@ -52,14 +85,13 @@ abstract class ExternalStoreMedium {
         * Fetch data from given external store URLs.
         *
         * @param array $urls A list of external store URLs
-        * @return array Map from the url to the text stored. Unfound data is not represented
+        * @return string[] Map of (url => text) for the URLs where data was actually found
         */
        public function batchFetchFromURLs( array $urls ) {
                $retval = [];
                foreach ( $urls as $url ) {
                        $data = $this->fetchFromURL( $url );
-                       // Dont return when false to allow for simpler implementations.
-                       // errored urls are handled in ExternalStore::batchFetchFromURLs
+                       // Dont return when false to allow for simpler implementations
                        if ( $data !== false ) {
                                $retval[$url] = $data;
                        }
@@ -86,6 +118,6 @@ abstract class ExternalStoreMedium {
         * @since 1.31
         */
        public function isReadOnly( $location ) {
-               return false;
+               return !in_array( $location, $this->writableLocations, true );
        }
 }
diff --git a/includes/externalstore/ExternalStoreMemory.php b/includes/externalstore/ExternalStoreMemory.php
new file mode 100644 (file)
index 0000000..daad75c
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+/**
+ * External storage in PHP process memory for testing.
+ *
+ * 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
+ */
+
+/**
+ * Process memory based external objects for testing.
+ *
+ * In this system, each store "location" is separate PHP array.
+ * URLs are of the form "memory://location/id". The id/value pairs
+ * at each location are segregated by DB domain ID.
+ *
+ * @ingroup ExternalStorage
+ * @since 1.33
+ */
+class ExternalStoreMemory extends ExternalStoreMedium {
+       /** @var array[] Map of (location => DB domain => id => value) */
+       private static $data = [];
+       /** @var int */
+       private static $nextId = 0;
+
+       public function __construct( array $params ) {
+               parent::__construct( $params );
+       }
+
+       public function fetchFromURL( $url ) {
+               list( $location, $id ) = self::getURLComponents( $url );
+               if ( $id === null ) {
+                       throw new UnexpectedValueException( "Missing ID in URL component." );
+               }
+
+               return self::$data[$location][$this->dbDomain][$id] ?? false;
+       }
+
+       public function batchFetchFromURLs( array $urls ) {
+               $blobs = [];
+               foreach ( $urls as $url ) {
+                       $blob = $this->fetchFromURL( $url );
+                       if ( $blob !== false ) {
+                               $blobs[$url] = $blob;
+                       }
+               }
+
+               return $blobs;
+       }
+
+       public function store( $location, $data ) {
+               $index = ++self::$nextId;
+               self::$data[$location][$this->dbDomain][$index] = $data;
+
+               return "memory://$location/$index";
+       }
+
+       /**
+        * Remove all data from memory for this domain
+        */
+       public function clear() {
+               foreach ( self::$data as &$dataForLocation ) {
+                       unset( $dataForLocation[$this->dbDomain] );
+               }
+               unset( $dataForLocation );
+               self::$data = array_filter( self::$data, 'count' );
+               self::$nextId = 0;
+       }
+
+       /**
+        * @param string $url
+        * @return array (location, ID or null)
+        */
+       private function getURLComponents( $url ) {
+               list( $proto, $path ) = explode( '://', $url, 2 ) + [ null, null ];
+               if ( $proto !== 'memory' ) {
+                       throw new UnexpectedValueException( "Got URL of protocol '$proto', not 'memory'." );
+               } elseif ( $path === null ) {
+                       throw new UnexpectedValueException( "URL is missing path component." );
+               }
+
+               $parts = explode( '/', $path );
+               if ( count( $parts ) > 2 ) {
+                       throw new UnexpectedValueException( "Too components in URL '$path'." );
+               }
+
+               return [ $parts[0], $parts[1] ?? null ];
+       }
+}
index 7414f23..77c23ee 100644 (file)
  * @since 1.21
  */
 class ExternalStoreMwstore extends ExternalStoreMedium {
+       /** @var FileBackendGroup */
+       private $fbGroup;
+
+       /**
+        * @see ExternalStoreMedium::__construct()
+        * @param array $params Additional parameters include:
+        *   - fbGroup: a FileBackendGroup instance
+        */
+       public function __construct( array $params ) {
+               parent::__construct( $params );
+               if ( !isset( $params['fbGroup'] ) || !( $params['fbGroup'] instanceof FileBackendGroup ) ) {
+                       throw new InvalidArgumentException( "FileBackendGroup required in 'fbGroup' field." );
+               }
+               $this->fbGroup = $params['fbGroup'];
+       }
+
        /**
         * The URL returned is of the form of the form mwstore://backend/container/wiki/id
         *
@@ -39,7 +55,7 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
         * @return bool
         */
        public function fetchFromURL( $url ) {
-               $be = FileBackendGroup::singleton()->backendFromPath( $url );
+               $be = $this->fbGroup->backendFromPath( $url );
                if ( $be instanceof FileBackend ) {
                        // We don't need "latest" since objects are immutable and
                        // backends should at least have "read-after-create" consistency.
@@ -59,14 +75,14 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
        public function batchFetchFromURLs( array $urls ) {
                $pathsByBackend = [];
                foreach ( $urls as $url ) {
-                       $be = FileBackendGroup::singleton()->backendFromPath( $url );
+                       $be = $this->fbGroup->backendFromPath( $url );
                        if ( $be instanceof FileBackend ) {
                                $pathsByBackend[$be->getName()][] = $url;
                        }
                }
                $blobs = [];
                foreach ( $pathsByBackend as $backendName => $paths ) {
-                       $be = FileBackendGroup::singleton()->get( $backendName );
+                       $be = $this->fbGroup->get( $backendName );
                        $blobs += $be->getFileContentsMulti( [ 'srcs' => $paths ] );
                }
 
@@ -77,16 +93,18 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
         * @inheritDoc
         */
        public function store( $backend, $data ) {
-               $be = FileBackendGroup::singleton()->get( $backend );
+               $be = $this->fbGroup->get( $backend );
                // Get three random base 36 characters to act as shard directories
                $rand = Wikimedia\base_convert( mt_rand( 0, 46655 ), 10, 36, 3 );
                // Make sure ID is roughly lexicographically increasing for performance
                $id = str_pad( UIDGenerator::newTimestampedUID128( 32 ), 26, '0', STR_PAD_LEFT );
-               // Segregate items by wiki ID for the sake of bookkeeping
-               // @FIXME: this does not include the domain for b/c but it ideally should
-               $wiki = $this->params['wiki'] ?? wfWikiID();
-
-               $url = $be->getContainerStoragePath( 'data' ) . '/' . rawurlencode( $wiki );
+               // Segregate items by DB domain ID for the sake of bookkeeping
+               $domain = $this->isDbDomainExplicit
+                       ? $this->dbDomain
+                       // @FIXME: this does not include the schema for b/c but it ideally should
+                       : WikiMap::getWikiIdFromDbDomain( $this->dbDomain );
+               $url = $be->getContainerStoragePath( 'data' ) . '/' . rawurlencode( $domain );
+               // Use directory/container sharding
                $url .= ( $be instanceof FSFileBackend )
                        ? "/{$rand[0]}/{$rand[1]}/{$rand[2]}/{$id}" // keep directories small
                        : "/{$rand[0]}/{$rand[1]}/{$id}"; // container sharding is only 2-levels
@@ -96,13 +114,17 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
 
                if ( $status->isOK() ) {
                        return $url;
-               } else {
-                       throw new MWException( __METHOD__ . ": operation failed: $status" );
                }
+
+               throw new MWException( __METHOD__ . ": operation failed: $status" );
        }
 
        public function isReadOnly( $backend ) {
-               $be = FileBackendGroup::singleton()->get( $backend );
+               if ( parent::isReadOnly( $backend ) ) {
+                       return true;
+               }
+
+               $be = $this->fbGroup->get( $backend );
 
                return $be ? $be->isReadOnly() : false;
        }
index 4995d3b..9a4df1f 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Pointer object for an item within a CGZ blob stored in the text table.
  */
@@ -99,8 +101,9 @@ class HistoryBlobStub {
                                if ( !isset( $parts[1] ) || $parts[1] == '' ) {
                                        return false;
                                }
-                               $row->old_text = ExternalStore::fetchFromURL( $url );
-
+                               $row->old_text = MediaWikiServices::getInstance()
+                                       ->getExternalStoreAccess()
+                                       ->fetchFromURL( $url );
                        }
 
                        if ( !in_array( 'object', $flags ) ) {
index dc5aa22..a1b2460 100644 (file)
@@ -297,7 +297,7 @@ class SwiftFileBackend extends FileBackendStore {
                $method = __METHOD__;
                $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 201 ) {
+                       if ( $rcode === 201 || $rcode === 202 ) {
                                // good
                        } elseif ( $rcode === 412 ) {
                                $status->fatal( 'backend-fail-contenttype', $params['dst'] );
@@ -360,7 +360,7 @@ class SwiftFileBackend extends FileBackendStore {
                $method = __METHOD__;
                $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 201 ) {
+                       if ( $rcode === 201 || $rcode === 202 ) {
                                // good
                        } elseif ( $rcode === 412 ) {
                                $status->fatal( 'backend-fail-contenttype', $params['dst'] );
index 27e6138..50a0b0e 100644 (file)
@@ -73,7 +73,7 @@ class ConnectionManager {
         * @param int $i
         * @param string[]|null $groups
         *
-        * @return Database
+        * @return IDatabase
         */
        private function getConnection( $i, array $groups = null ) {
                $groups = $groups === null ? $this->groups : $groups;
@@ -97,7 +97,7 @@ class ConnectionManager {
         *
         * @since 1.29
         *
-        * @return Database
+        * @return IDatabase
         */
        public function getWriteConnection() {
                return $this->getConnection( DB_MASTER );
@@ -111,7 +111,7 @@ class ConnectionManager {
         *
         * @param string[]|null $groups
         *
-        * @return Database
+        * @return IDatabase
         */
        public function getReadConnection( array $groups = null ) {
                $groups = $groups === null ? $this->groups : $groups;
index aa3bea8..ccb73d7 100644 (file)
@@ -64,7 +64,7 @@ class SessionConsistentConnectionManager extends ConnectionManager {
         *
         * @param string[]|null $groups
         *
-        * @return Database
+        * @return IDatabase
         */
        public function getReadConnection( array $groups = null ) {
                if ( $this->forceWriteConnection ) {
@@ -77,7 +77,7 @@ class SessionConsistentConnectionManager extends ConnectionManager {
        /**
         * @since 1.29
         *
-        * @return Database
+        * @return IDatabase
         */
        public function getWriteConnection() {
                $this->prepareForUpdates();
index c86adac..760d137 100644 (file)
@@ -105,9 +105,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var array Map of (table name => 1) for TEMPORARY tables */
        protected $sessionTempTables = [];
 
-       /** @var int Whether there is an active transaction (1 or 0) */
-       protected $trxLevel = 0;
-       /** @var string Hexidecimal string if a transaction is active or empty string otherwise */
+       /** @var string ID of the active transaction or the empty string otherwise */
        protected $trxShortId = '';
        /** @var int Transaction status */
        protected $trxStatus = self::STATUS_TRX_NONE;
@@ -512,12 +510,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $res;
        }
 
-       public function trxLevel() {
-               return $this->trxLevel;
+       final public function trxLevel() {
+               return ( $this->trxShortId != '' ) ? 1 : 0;
        }
 
        public function trxTimestamp() {
-               return $this->trxLevel ? $this->trxTimestamp : null;
+               return $this->trxLevel() ? $this->trxTimestamp : null;
        }
 
        /**
@@ -620,11 +618,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function writesPending() {
-               return $this->trxLevel && $this->trxDoneWrites;
+               return $this->trxLevel() && $this->trxDoneWrites;
        }
 
        public function writesOrCallbacksPending() {
-               return $this->trxLevel && (
+               return $this->trxLevel() && (
                        $this->trxDoneWrites ||
                        $this->trxIdleCallbacks ||
                        $this->trxPreCommitCallbacks ||
@@ -634,7 +632,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function preCommitCallbacksPending() {
-               return $this->trxLevel && $this->trxPreCommitCallbacks;
+               return $this->trxLevel() && $this->trxPreCommitCallbacks;
        }
 
        /**
@@ -652,7 +650,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
-               if ( !$this->trxLevel ) {
+               if ( !$this->trxLevel() ) {
                        return false;
                } elseif ( !$this->trxDoneWrites ) {
                        return 0.0;
@@ -682,7 +680,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function pendingWriteCallers() {
-               return $this->trxLevel ? $this->trxWriteCallers : [];
+               return $this->trxLevel() ? $this->trxWriteCallers : [];
        }
 
        public function pendingWriteRowsAffected() {
@@ -871,7 +869,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // This should mostly do nothing if the connection is already closed
                if ( $this->conn ) {
                        // Roll back any dangling transaction first
-                       if ( $this->trxLevel ) {
+                       if ( $this->trxLevel() ) {
                                if ( $this->trxAtomicLevels ) {
                                        // Cannot let incomplete atomic sections be committed
                                        $levels = $this->flatAtomicSectionList();
@@ -1158,7 +1156,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        final protected function executeQuery( $sql, $fname, $flags ) {
                $this->assertHasConnectionHandle();
 
-               $priorTransaction = $this->trxLevel;
+               $priorTransaction = $this->trxLevel();
 
                if ( $this->isWriteQuery( $sql ) ) {
                        # In theory, non-persistent writes are allowed in read-only mode, but due to things
@@ -1248,7 +1246,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // Keep track of whether the transaction has write queries pending
                if ( $isPermWrite ) {
                        $this->lastWriteTime = microtime( true );
-                       if ( $this->trxLevel && !$this->trxDoneWrites ) {
+                       if ( $this->trxLevel() && !$this->trxDoneWrites ) {
                                $this->trxDoneWrites = true;
                                $this->trxProfiler->transactionWritingIn(
                                        $this->server, $this->getDomainID(), $this->trxShortId );
@@ -1278,7 +1276,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                if ( $ret !== false ) {
                        $this->lastPing = $startTime;
-                       if ( $isPermWrite && $this->trxLevel ) {
+                       if ( $isPermWrite && $this->trxLevel() ) {
                                $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
                                $this->trxWriteCallers[] = $fname;
                        }
@@ -1327,7 +1325,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        private function beginIfImplied( $sql, $fname ) {
                if (
-                       !$this->trxLevel &&
+                       !$this->trxLevel() &&
                        $this->getFlag( self::DBO_TRX ) &&
                        $this->isTransactableQuery( $sql )
                ) {
@@ -1457,7 +1455,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
                $this->sessionNamedLocks = [];
                // Session loss implies transaction loss
-               $this->trxLevel = 0;
+               $oldTrxShortId = $this->consumeTrxShortId();
                $this->trxAtomicCounter = 0;
                $this->trxIdleCallbacks = []; // T67263; transaction already lost
                $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
@@ -1466,7 +1464,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->trxProfiler->transactionWritingOut(
                                $this->server,
                                $this->getDomainID(),
-                               $this->trxShortId,
+                               $oldTrxShortId,
                                $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
                                $this->trxWriteAffectedRows
                        );
@@ -1492,6 +1490,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
+       /**
+        * Reset the transaction ID and return the old one
+        *
+        * @return string The old transaction ID or the empty string if there wasn't one
+        */
+       private function consumeTrxShortId() {
+               $old = $this->trxShortId;
+               $this->trxShortId = '';
+
+               return $old;
+       }
+
        /**
         * Checks whether the cause of the error is detected to be a timeout.
         *
@@ -1989,7 +1999,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        public function lockForUpdate(
                $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
-               if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) {
+               if ( !$this->trxLevel() && !$this->getFlag( self::DBO_TRX ) ) {
                        throw new DBUnexpectedError(
                                $this,
                                __METHOD__ . ': no transaction is active nor is DBO_TRX set'
@@ -3336,21 +3346,21 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
-               if ( !$this->trxLevel ) {
+               if ( !$this->trxLevel() ) {
                        throw new DBUnexpectedError( $this, "No transaction is active." );
                }
                $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
        }
 
        final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
-               if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
+               if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
                        // Start an implicit transaction similar to how query() does
                        $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                        $this->trxAutomatic = true;
                }
 
                $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
-               if ( !$this->trxLevel ) {
+               if ( !$this->trxLevel() ) {
                        $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
                }
        }
@@ -3360,13 +3370,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
-               if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
+               if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
                        // Start an implicit transaction similar to how query() does
                        $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                        $this->trxAutomatic = true;
                }
 
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
                } else {
                        // No transaction is active nor will start implicitly, so make one for this callback
@@ -3382,7 +3392,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
-               if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
+               if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
                        throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
                }
                $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
@@ -3392,7 +3402,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @return AtomicSectionIdentifier|null ID of the topmost atomic section level
         */
        private function currentAtomicSectionId() {
-               if ( $this->trxLevel && $this->trxAtomicLevels ) {
+               if ( $this->trxLevel() && $this->trxAtomicLevels ) {
                        $levelInfo = end( $this->trxAtomicLevels );
 
                        return $levelInfo[1];
@@ -3518,7 +3528,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @throws Exception
         */
        public function runOnTransactionIdleCallbacks( $trigger ) {
-               if ( $this->trxLevel ) { // sanity
+               if ( $this->trxLevel() ) { // sanity
                        throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
                }
 
@@ -3749,7 +3759,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        ) {
                $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
 
-               if ( !$this->trxLevel ) {
+               if ( !$this->trxLevel() ) {
                        $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
                        // in all changes being in one transaction to keep requests transactional.
@@ -3775,7 +3785,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        final public function endAtomic( $fname = __METHOD__ ) {
-               if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
+               if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
                        throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
                }
 
@@ -3811,7 +3821,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        final public function cancelAtomic(
                $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
        ) {
-               if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
+               if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
                        throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
                }
 
@@ -3916,7 +3926,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // Protect against mismatched atomic section, transaction nesting, and snapshot loss
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        if ( $this->trxAtomicLevels ) {
                                $levels = $this->flatAtomicSectionList();
                                $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
@@ -3936,6 +3946,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->assertHasConnectionHandle();
 
                $this->doBegin( $fname );
+               $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
                $this->trxStatus = self::STATUS_TRX_OK;
                $this->trxStatusIgnoredCause = null;
                $this->trxAtomicCounter = 0;
@@ -3944,7 +3955,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->trxDoneWrites = false;
                $this->trxAutomaticAtomic = false;
                $this->trxAtomicLevels = [];
-               $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
                $this->trxWriteDuration = 0.0;
                $this->trxWriteQueryCount = 0;
                $this->trxWriteAffectedRows = 0;
@@ -3966,10 +3976,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * @see Database::begin()
         * @param string $fname
+        * @throws DBError
         */
        protected function doBegin( $fname ) {
                $this->query( 'BEGIN', $fname );
-               $this->trxLevel = 1;
        }
 
        final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
@@ -3978,7 +3988,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
                }
 
-               if ( $this->trxLevel && $this->trxAtomicLevels ) {
+               if ( $this->trxLevel() && $this->trxAtomicLevels ) {
                        // There are still atomic sections open; this cannot be ignored
                        $levels = $this->flatAtomicSectionList();
                        throw new DBUnexpectedError(
@@ -3988,7 +3998,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
-                       if ( !$this->trxLevel ) {
+                       if ( !$this->trxLevel() ) {
                                return; // nothing to do
                        } elseif ( !$this->trxAutomatic ) {
                                throw new DBUnexpectedError(
@@ -3996,7 +4006,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                        "$fname: Flushing an explicit transaction, getting out of sync."
                                );
                        }
-               } elseif ( !$this->trxLevel ) {
+               } elseif ( !$this->trxLevel() ) {
                        $this->queryLogger->error(
                                "$fname: No transaction to commit, something got out of sync." );
                        return; // nothing to do
@@ -4013,6 +4023,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
                $this->doCommit( $fname );
+               $oldTrxShortId = $this->consumeTrxShortId();
                $this->trxStatus = self::STATUS_TRX_NONE;
 
                if ( $this->trxDoneWrites ) {
@@ -4020,7 +4031,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->trxProfiler->transactionWritingOut(
                                $this->server,
                                $this->getDomainID(),
-                               $this->trxShortId,
+                               $oldTrxShortId,
                                $writeTime,
                                $this->trxWriteAffectedRows
                        );
@@ -4038,16 +4049,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * @see Database::commit()
         * @param string $fname
+        * @throws DBError
         */
        protected function doCommit( $fname ) {
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        $this->query( 'COMMIT', $fname );
-                       $this->trxLevel = 0;
                }
        }
 
        final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
-               $trxActive = $this->trxLevel;
+               $trxActive = $this->trxLevel();
 
                if ( $flush !== self::FLUSHING_INTERNAL
                        && $flush !== self::FLUSHING_ALL_PEERS
@@ -4063,6 +4074,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->assertHasConnectionHandle();
 
                        $this->doRollback( $fname );
+                       $oldTrxShortId = $this->consumeTrxShortId();
                        $this->trxStatus = self::STATUS_TRX_NONE;
                        $this->trxAtomicLevels = [];
                        // Estimate the RTT via a query now that trxStatus is OK
@@ -4072,7 +4084,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->trxProfiler->transactionWritingOut(
                                        $this->server,
                                        $this->getDomainID(),
-                                       $this->trxShortId,
+                                       $oldTrxShortId,
                                        $writeTime,
                                        $this->trxWriteAffectedRows
                                );
@@ -4106,13 +4118,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * @see Database::rollback()
         * @param string $fname
+        * @throws DBError
         */
        protected function doRollback( $fname ) {
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        # Disconnects cause rollback anyway, so ignore those errors
                        $ignoreErrors = true;
                        $this->query( 'ROLLBACK', $fname, $ignoreErrors );
-                       $this->trxLevel = 0;
                }
        }
 
@@ -4130,7 +4142,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function explicitTrxActive() {
-               return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
+               return $this->trxLevel() && ( $this->trxAtomicLevels || !$this->trxAutomatic );
        }
 
        public function duplicateTableStructure(
@@ -4282,7 +4294,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @since 1.27
         */
        final protected function getRecordedTransactionLagStatus() {
-               return ( $this->trxLevel && $this->trxReplicaLag !== null )
+               return ( $this->trxLevel() && $this->trxReplicaLag !== null )
                        ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
                        : null;
        }
@@ -4830,7 +4842,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * Run a few simple sanity checks and close dangling connections
         */
        public function __destruct() {
-               if ( $this->trxLevel && $this->trxDoneWrites ) {
+               if ( $this->trxLevel() && $this->trxDoneWrites ) {
                        trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
                }
 
index 6c003dd..50aaff2 100644 (file)
@@ -28,6 +28,7 @@
 namespace Wikimedia\Rdbms;
 
 use Exception;
+use RuntimeException;
 use stdClass;
 use Wikimedia\AtEase\AtEase;
 
@@ -374,6 +375,17 @@ class DatabaseMssql extends Database {
                return $statementOnly;
        }
 
+       public function serverIsReadOnly() {
+               $encDatabase = $this->addQuotes( $this->getDBname() );
+               $res = $this->query(
+                       "SELECT IS_READ_ONLY FROM SYS.DATABASES WHERE NAME = $encDatabase",
+                       __METHOD__
+               );
+               $row = $this->fetchObject( $res );
+
+               return $row ? (bool)$row->IS_READ_ONLY : false;
+       }
+
        /**
         * @return int
         */
@@ -1071,13 +1083,10 @@ class DatabaseMssql extends Database {
                $this->query( 'ROLLBACK TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
        }
 
-       /**
-        * Begin a transaction, committing any previously open transaction
-        * @param string $fname
-        */
        protected function doBegin( $fname = __METHOD__ ) {
-               sqlsrv_begin_transaction( $this->conn );
-               $this->trxLevel = 1;
+               if ( !sqlsrv_begin_transaction( $this->conn ) ) {
+                       $this->reportQueryError( $this->lastError(), $this->lastErrno(), 'BEGIN', $fname );
+               }
        }
 
        /**
@@ -1085,8 +1094,9 @@ class DatabaseMssql extends Database {
         * @param string $fname
         */
        protected function doCommit( $fname = __METHOD__ ) {
-               sqlsrv_commit( $this->conn );
-               $this->trxLevel = 0;
+               if ( !sqlsrv_commit( $this->conn ) ) {
+                       $this->reportQueryError( $this->lastError(), $this->lastErrno(), 'COMMIT', $fname );
+               }
        }
 
        /**
@@ -1095,8 +1105,17 @@ class DatabaseMssql extends Database {
         * @param string $fname
         */
        protected function doRollback( $fname = __METHOD__ ) {
-               sqlsrv_rollback( $this->conn );
-               $this->trxLevel = 0;
+               if ( !sqlsrv_rollback( $this->conn ) ) {
+                       $this->queryLogger->error(
+                               "{fname}\t{db_server}\t{errno}\t{error}\t",
+                               $this->getLogContext( [
+                                       'errno' => $this->lastErrno(),
+                                       'error' => $this->lastError(),
+                                       'fname' => $fname,
+                                       'trace' => ( new RuntimeException() )->getTraceAsString()
+                               ] )
+                       );
+               }
        }
 
        /**
index a19a1a4..92eac90 100644 (file)
@@ -1072,7 +1072,7 @@ __INDEXATTR__;
         * @param string $desiredSchema
         */
        public function determineCoreSchema( $desiredSchema ) {
-               if ( $this->trxLevel ) {
+               if ( $this->trxLevel() ) {
                        // We do not want the schema selection to change on ROLLBACK or INSERT SELECT.
                        // See https://www.postgresql.org/docs/8.3/sql-set.html
                        throw new DBUnexpectedError(
index 46c34b4..17f12d3 100644 (file)
@@ -173,18 +173,8 @@ class DatabaseSqlite extends Database {
                        throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
                }
 
-               $fileName = self::generateFileName( $this->dbDir, $dbName );
-               if ( !is_readable( $fileName ) ) {
-                       $error = "SQLite database file not readable";
-                       $this->connLogger->error(
-                               "Error connecting to {db_server}: {error}",
-                               $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
-                       );
-                       throw new DBConnectionError( $this, $error );
-               }
-
                // Only $dbName is used, the other parameters are irrelevant for SQLite databases
-               $this->openFile( $fileName, $dbName, $tablePrefix );
+               $this->openFile( self::generateFileName( $this->dbDir, $dbName ), $dbName, $tablePrefix );
        }
 
        /**
@@ -196,6 +186,15 @@ class DatabaseSqlite extends Database {
         * @throws DBConnectionError
         */
        protected function openFile( $fileName, $dbName, $tablePrefix ) {
+               if ( !$this->hasMemoryPath() && !is_readable( $fileName ) ) {
+                       $error = "SQLite database file not readable";
+                       $this->connLogger->error(
+                               "Error connecting to {db_server}: {error}",
+                               $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
+                       );
+                       throw new DBConnectionError( $this, $error );
+               }
+
                $this->dbPath = $fileName;
                try {
                        $this->conn = new PDO(
@@ -772,6 +771,17 @@ class DatabaseSqlite extends Database {
                return false;
        }
 
+       public function serverIsReadOnly() {
+               return ( !$this->hasMemoryPath() && !is_writable( $this->dbPath ) );
+       }
+
+       /**
+        * @return bool
+        */
+       private function hasMemoryPath() {
+               return ( strpos( $this->dbPath, ':memory:' ) === 0 );
+       }
+
        /**
         * @return string Wikitext of a link to the server software's web site
         */
@@ -815,7 +825,6 @@ class DatabaseSqlite extends Database {
                } else {
                        $this->query( 'BEGIN', $fname );
                }
-               $this->trxLevel = 1;
        }
 
        /**
index 3709de7..2ca3d7d 100644 (file)
@@ -35,7 +35,7 @@ class FakeResultWrapper extends ResultWrapper {
 
                $this->next();
 
-               return is_object( $row ) ? (array)$row : $row;
+               return is_object( $row ) ? get_object_vars( $row ) : $row;
        }
 
        function seek( $pos ) {
index c5dbfc5..35c9539 100644 (file)
@@ -140,7 +140,7 @@ interface ILBFactory {
        /**
         * Get cached (tracked) load balancers for all main database clusters
         *
-        * @return LoadBalancer[] Map of (cluster name => LoadBalancer)
+        * @return ILoadBalancer[] Map of (cluster name => ILoadBalancer)
         * @since 1.29
         */
        public function getAllMainLBs();
@@ -148,7 +148,7 @@ interface ILBFactory {
        /**
         * Get cached (tracked) load balancers for all external database clusters
         *
-        * @return LoadBalancer[] Map of (cluster name => LoadBalancer)
+        * @return ILoadBalancer[] Map of (cluster name => ILoadBalancer)
         * @since 1.29
         */
        public function getAllExternalLBs();
index aec99f4..f675b58 100644 (file)
@@ -34,55 +34,42 @@ use InvalidArgumentException;
 class LBFactoryMulti extends LBFactory {
        /** @var array A map of database names to section names */
        private $sectionsByDB;
-
        /**
         * @var array A 2-d map. For each section, gives a map of server names to
         * load ratios
         */
        private $sectionLoads;
-
        /**
         * @var array[] Server info associative array
         * @note The host, hostName and load entries will be overridden
         */
        private $serverTemplate;
 
-       // Optional settings
-
        /** @var array A 3-d map giving server load ratios for each section and group */
        private $groupLoadsBySection = [];
-
        /** @var array A 3-d map giving server load ratios by DB name */
        private $groupLoadsByDB = [];
-
        /** @var array A map of hostname to IP address */
        private $hostsByName = [];
-
        /** @var array A map of external storage cluster name to server load map */
        private $externalLoads = [];
-
        /**
         * @var array A set of server info keys overriding serverTemplate for
         * external storage
         */
        private $externalTemplateOverrides;
-
        /**
         * @var array A 2-d map overriding serverTemplate and
         * externalTemplateOverrides on a server-by-server basis. Applies to both
         * core and external storage
         */
        private $templateOverridesByServer;
-
        /** @var array A 2-d map overriding the server info by section */
        private $templateOverridesBySection;
-
        /** @var array A 2-d map overriding the server info by external storage cluster */
        private $templateOverridesByCluster;
-
        /** @var array An override array for all master servers */
        private $masterTemplateOverrides;
-
        /**
         * @var array|bool A map of section name to read-only message. Missing or
         * false for read/write
@@ -91,16 +78,12 @@ class LBFactoryMulti extends LBFactory {
 
        /** @var LoadBalancer[] */
        private $mainLBs = [];
-
        /** @var LoadBalancer[] */
        private $extLBs = [];
-
        /** @var string */
        private $loadMonitorClass = 'LoadMonitor';
-
        /** @var string */
        private $lastDomain;
-
        /** @var string */
        private $lastSection;
 
@@ -191,22 +174,19 @@ class LBFactoryMulti extends LBFactory {
                if ( $this->lastDomain === $domain ) {
                        return $this->lastSection;
                }
-               list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
-               $section = $this->sectionsByDB[$dbName] ?? 'DEFAULT';
+
+               $database = $this->getDatabaseFromDomain( $domain );
+               $section = $this->sectionsByDB[$database] ?? 'DEFAULT';
                $this->lastSection = $section;
                $this->lastDomain = $domain;
 
                return $section;
        }
 
-       /**
-        * @param bool|string $domain
-        * @return LoadBalancer
-        */
        public function newMainLB( $domain = false ) {
-               list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
+               $database = $this->getDatabaseFromDomain( $domain );
                $section = $this->getSectionForDomain( $domain );
-               $groupLoads = $this->groupLoadsByDB[$dbName] ?? [];
+               $groupLoads = $this->groupLoadsByDB[$database] ?? [];
 
                if ( isset( $this->groupLoadsBySection[$section] ) ) {
                        $groupLoads = array_merge_recursive(
@@ -232,10 +212,6 @@ class LBFactoryMulti extends LBFactory {
                );
        }
 
-       /**
-        * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
-        * @return LoadBalancer
-        */
        public function getMainLB( $domain = false ) {
                $section = $this->getSectionForDomain( $domain );
                if ( !isset( $this->mainLBs[$section] ) ) {
@@ -379,23 +355,14 @@ class LBFactoryMulti extends LBFactory {
 
        /**
         * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
-        * @return array [database name, table prefix]
+        * @return string
         */
-       private function getDBNameAndPrefix( $domain = false ) {
-               $domain = ( $domain === false )
-                       ? $this->localDomain
-                       : DatabaseDomain::newFromId( $domain );
-
-               return [ $domain->getDatabase(), $domain->getTablePrefix() ];
+       private function getDatabaseFromDomain( $domain = false ) {
+               return ( $domain === false )
+                       ? $this->localDomain->getDatabase()
+                       : DatabaseDomain::newFromId( $domain )->getDatabase();
        }
 
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        * @param callable $callback
-        * @param array $params
-        */
        public function forEachLB( $callback, array $params = [] ) {
                foreach ( $this->mainLBs as $lb ) {
                        $callback( $lb, ...$params );
index 49054e0..fd76d88 100644 (file)
@@ -70,20 +70,12 @@ class LBFactorySimple extends LBFactory {
                $this->loadMonitorClass = $conf['loadMonitorClass'] ?? 'LoadMonitor';
        }
 
-       /**
-        * @param bool|string $domain
-        * @return LoadBalancer
-        */
        public function newMainLB( $domain = false ) {
                return $this->newLoadBalancer( $this->servers );
        }
 
-       /**
-        * @param bool|string $domain
-        * @return LoadBalancer
-        */
        public function getMainLB( $domain = false ) {
-               if ( !isset( $this->mainLB ) ) {
+               if ( !$this->mainLB ) {
                        $this->mainLB = $this->newMainLB( $domain );
                }
 
@@ -132,14 +124,6 @@ class LBFactorySimple extends LBFactory {
                return $lb;
        }
 
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        *
-        * @param callable $callback
-        * @param array $params
-        */
        public function forEachLB( $callback, array $params = [] ) {
                if ( isset( $this->mainLB ) ) {
                        $callback( $this->mainLB, ...$params );
index 4d148b4..b086beb 100644 (file)
@@ -314,22 +314,6 @@ interface ILoadBalancer {
         */
        public function getWriterIndex();
 
-       /**
-        * Returns true if the specified index is a valid server index
-        *
-        * @param int $i
-        * @return bool
-        */
-       public function haveIndex( $i );
-
-       /**
-        * Returns true if the specified index is valid and has non-zero load
-        *
-        * @param int $i
-        * @return bool
-        */
-       public function isNonZeroLoad( $i );
-
        /**
         * Get the number of servers defined in configuration
         *
index 7f12d14..44d526c 100644 (file)
@@ -1306,10 +1306,24 @@ class LoadBalancer implements ILoadBalancer {
                return 0;
        }
 
+       /**
+        * Returns true if the specified index is a valid server index
+        *
+        * @param int $i
+        * @return bool
+        * @deprecated Since 1.34
+        */
        public function haveIndex( $i ) {
                return array_key_exists( $i, $this->servers );
        }
 
+       /**
+        * Returns true if the specified index is valid and has non-zero load
+        *
+        * @param int $i
+        * @return bool
+        * @deprecated Since 1.34
+        */
        public function isNonZeroLoad( $i ) {
                return array_key_exists( $i, $this->servers ) && $this->genericLoads[$i] != 0;
        }
index d83853a..4f5c150 100644 (file)
@@ -625,8 +625,7 @@ class SpecialContributions extends IncludableSpecialPage {
                        [],
                        Xml::label(
                                $this->msg( 'namespace' )->text(),
-                               'namespace',
-                               ''
+                               'namespace'
                        ) . "\u{00A0}" .
                        Html::namespaceSelector(
                                [ 'selected' => $this->opts['namespace'], 'all' => '', 'in-user-lang' => true ],
index 6025d3c..97d4702 100644 (file)
@@ -111,95 +111,7 @@ class User implements IDBAccessObject, UserIdentity {
        ];
 
        /**
-        * Array of Strings Core rights.
-        * Each of these should have a corresponding message of the form
-        * "right-$right".
-        * @showinitializer
         * @var string[]
-        */
-       protected static $mCoreRights = [
-               'apihighlimits',
-               'applychangetags',
-               'autoconfirmed',
-               'autocreateaccount',
-               'autopatrol',
-               'bigdelete',
-               'block',
-               'blockemail',
-               'bot',
-               'browsearchive',
-               'changetags',
-               'createaccount',
-               'createpage',
-               'createtalk',
-               'delete',
-               'deletechangetags',
-               'deletedhistory',
-               'deletedtext',
-               'deletelogentry',
-               'deleterevision',
-               'edit',
-               'editcontentmodel',
-               'editinterface',
-               'editprotected',
-               'editmyoptions',
-               'editmyprivateinfo',
-               'editmyusercss',
-               'editmyuserjson',
-               'editmyuserjs',
-               'editmywatchlist',
-               'editsemiprotected',
-               'editsitecss',
-               'editsitejson',
-               'editsitejs',
-               'editusercss',
-               'edituserjson',
-               'edituserjs',
-               'hideuser',
-               'import',
-               'importupload',
-               'ipblock-exempt',
-               'managechangetags',
-               'markbotedits',
-               'mergehistory',
-               'minoredit',
-               'move',
-               'movefile',
-               'move-categorypages',
-               'move-rootuserpages',
-               'move-subpages',
-               'nominornewtalk',
-               'noratelimit',
-               'override-export-depth',
-               'pagelang',
-               'patrol',
-               'patrolmarks',
-               'protect',
-               'purge',
-               'read',
-               'reupload',
-               'reupload-own',
-               'reupload-shared',
-               'rollback',
-               'sendemail',
-               'siteadmin',
-               'suppressionlog',
-               'suppressredirect',
-               'suppressrevision',
-               'unblockself',
-               'undelete',
-               'unwatchedpages',
-               'upload',
-               'upload_by_url',
-               'userrights',
-               'userrights-interwiki',
-               'viewmyprivateinfo',
-               'viewmywatchlist',
-               'viewsuppressed',
-               'writeapi',
-       ];
-
-       /**
         * @var string[] Cached results of getAllRights()
         */
        protected static $mAllRights = false;
@@ -274,8 +186,6 @@ class User implements IDBAccessObject, UserIdentity {
        public $mBlockedby;
        /** @var string */
        protected $mHash;
-       /** @var array */
-       public $mRights;
        /** @var string */
        protected $mBlockreason;
        /** @var array */
@@ -333,6 +243,24 @@ class User implements IDBAccessObject, UserIdentity {
                return (string)$this->getName();
        }
 
+       public function __get( $name ) {
+               // A shortcut for $mRights deprecation phase
+               if ( $name === 'mRights' ) {
+                       return $this->getRights();
+               }
+       }
+
+       public function __set( $name, $value ) {
+               // A shortcut for $mRights deprecation phase, only known legitimate use was for
+               // testing purposes, other uses seem bad in principle
+               if ( $name === 'mRights' ) {
+                       MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
+                               $this,
+                               is_null( $value ) ? [] : $value
+                       );
+               }
+       }
+
        /**
         * Test if it's safe to load this User object.
         *
@@ -1699,11 +1627,12 @@ class User implements IDBAccessObject, UserIdentity {
         *   given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
         */
        public function clearInstanceCache( $reloadFrom = false ) {
+               global $wgFullyInitialised;
+
                $this->mNewtalk = -1;
                $this->mDatePreference = null;
                $this->mBlockedby = -1; # Unset
                $this->mHash = false;
-               $this->mRights = null;
                $this->mEffectiveGroups = null;
                $this->mImplicitGroups = null;
                $this->mGroupMemberships = null;
@@ -1711,6 +1640,13 @@ class User implements IDBAccessObject, UserIdentity {
                $this->mOptionsLoaded = false;
                $this->mEditCount = null;
 
+               // Replacement of former `$this->mRights = null` line
+               if ( $wgFullyInitialised && $this->mFrom ) {
+                       MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache(
+                               $this
+                       );
+               }
+
                if ( $reloadFrom ) {
                        $this->mLoadedItems = [];
                        $this->mFrom = $reloadFrom;
@@ -2149,7 +2085,6 @@ class User implements IDBAccessObject, UserIdentity {
         * @param Title $title Title to check
         * @param bool $fromReplica Whether to check the replica DB instead of the master
         * @return bool
-        * @throws MWException
         *
         * @deprecated since 1.33,
         * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..)
@@ -3395,44 +3330,13 @@ class User implements IDBAccessObject, UserIdentity {
        /**
         * Get the permissions this user has.
         * @return string[] permission names
+        *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
+        * ->getUserPermissions(..) instead
+        *
         */
        public function getRights() {
-               if ( is_null( $this->mRights ) ) {
-                       $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
-                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
-
-                       // Deny any rights denied by the user's session, unless this
-                       // endpoint has no sessions.
-                       if ( !defined( 'MW_NO_SESSION' ) ) {
-                               $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights();
-                               if ( $allowedRights !== null ) {
-                                       $this->mRights = array_intersect( $this->mRights, $allowedRights );
-                               }
-                       }
-
-                       Hooks::run( 'UserGetRightsRemove', [ $this, &$this->mRights ] );
-                       // Force reindexation of rights when a hook has unset one of them
-                       $this->mRights = array_values( array_unique( $this->mRights ) );
-
-                       // If block disables login, we should also remove any
-                       // extra rights blocked users might have, in case the
-                       // blocked user has a pre-existing session (T129738).
-                       // This is checked here for cases where people only call
-                       // $user->isAllowed(). It is also checked in Title::checkUserBlock()
-                       // to give a better error message in the common case.
-                       $config = RequestContext::getMain()->getConfig();
-                       // @TODO Partial blocks should not prevent the user from logging in.
-                       //       see: https://phabricator.wikimedia.org/T208895
-                       if (
-                               $this->isLoggedIn() &&
-                               $config->get( 'BlockDisablesLogin' ) &&
-                               $this->getBlock()
-                       ) {
-                               $anon = new User;
-                               $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
-                       }
-               }
-               return $this->mRights;
+               return MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissions( $this );
        }
 
        /**
@@ -3601,8 +3505,7 @@ class User implements IDBAccessObject, UserIdentity {
                // Refresh the groups caches, and clear the rights cache so it will be
                // refreshed on the next call to $this->getRights().
                $this->getEffectiveGroups( true );
-               $this->mRights = null;
-
+               MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
                $this->invalidateCache();
 
                return true;
@@ -3633,8 +3536,7 @@ class User implements IDBAccessObject, UserIdentity {
                // Refresh the groups caches, and clear the rights cache so it will be
                // refreshed on the next call to $this->getRights().
                $this->getEffectiveGroups( true );
-               $this->mRights = null;
-
+               MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
                $this->invalidateCache();
 
                return true;
@@ -3717,16 +3619,17 @@ class User implements IDBAccessObject, UserIdentity {
 
        /**
         * Internal mechanics of testing a permission
+        *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()
+        * ->getPermissionManager()->userHasRight(...) instead
+        *
         * @param string $action
+        *
         * @return bool
         */
        public function isAllowed( $action = '' ) {
-               if ( $action === '' ) {
-                       return true; // In the spirit of DWIM
-               }
-               // Use strict parameter to avoid matching numeric 0 accidentally inserted
-               // by misconfiguration: 0 == 'foo'
-               return in_array( $action, $this->getRights(), true );
+               return MediaWikiServices::getInstance()->getPermissionManager()
+                       ->userHasRight( $this, $action );
        }
 
        /**
@@ -4875,45 +4778,27 @@ class User implements IDBAccessObject, UserIdentity {
        /**
         * Get the permissions associated with a given list of groups
         *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
+        *             ->getGroupPermissions() instead
+        *
         * @param array $groups Array of Strings List of internal group names
         * @return array Array of Strings List of permission key names for given groups combined
         */
        public static function getGroupPermissions( $groups ) {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               $rights = [];
-               // grant every granted permission first
-               foreach ( $groups as $group ) {
-                       if ( isset( $wgGroupPermissions[$group] ) ) {
-                               $rights = array_merge( $rights,
-                                       // array_filter removes empty items
-                                       array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
-                       }
-               }
-               // now revoke the revoked permissions
-               foreach ( $groups as $group ) {
-                       if ( isset( $wgRevokePermissions[$group] ) ) {
-                               $rights = array_diff( $rights,
-                                       array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
-                       }
-               }
-               return array_unique( $rights );
+               return MediaWikiServices::getInstance()->getPermissionManager()->getGroupPermissions( $groups );
        }
 
        /**
         * Get all the groups who have a given permission
         *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
+        *             ->getGroupsWithPermission() instead
+        *
         * @param string $role Role to check
         * @return array Array of Strings List of internal group names with the given permission
         */
        public static function getGroupsWithPermission( $role ) {
-               global $wgGroupPermissions;
-               $allowedGroups = [];
-               foreach ( array_keys( $wgGroupPermissions ) as $group ) {
-                       if ( self::groupHasPermission( $group, $role ) ) {
-                               $allowedGroups[] = $group;
-                       }
-               }
-               return $allowedGroups;
+               return MediaWikiServices::getInstance()->getPermissionManager()->getGroupsWithPermission( $role );
        }
 
        /**
@@ -4923,15 +4808,17 @@ class User implements IDBAccessObject, UserIdentity {
         * User::isEveryoneAllowed() instead. That properly checks if it's revoked
         * from anyone.
         *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
+        * ->groupHasPermission(..) instead
+        *
         * @since 1.21
         * @param string $group Group to check
         * @param string $role Role to check
         * @return bool
         */
        public static function groupHasPermission( $group, $role ) {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
-                       && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
+               return MediaWikiServices::getInstance()->getPermissionManager()
+                       ->groupHasPermission( $group, $role );
        }
 
        /**
@@ -4944,51 +4831,16 @@ class User implements IDBAccessObject, UserIdentity {
         * Specifically, session-based rights restrictions (such as OAuth or bot
         * passwords) are applied based on the current session.
         *
-        * @since 1.22
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
+        *             ->isEveryoneAllowed() instead
+        *
         * @param string $right Right to check
+        *
         * @return bool
+        * @since 1.22
         */
        public static function isEveryoneAllowed( $right ) {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               static $cache = [];
-
-               // Use the cached results, except in unit tests which rely on
-               // being able change the permission mid-request
-               if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
-                       return $cache[$right];
-               }
-
-               if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
-                       $cache[$right] = false;
-                       return false;
-               }
-
-               // If it's revoked anywhere, then everyone doesn't have it
-               foreach ( $wgRevokePermissions as $rights ) {
-                       if ( isset( $rights[$right] ) && $rights[$right] ) {
-                               $cache[$right] = false;
-                               return false;
-                       }
-               }
-
-               // Remove any rights that aren't allowed to the global-session user,
-               // unless there are no sessions for this endpoint.
-               if ( !defined( 'MW_NO_SESSION' ) ) {
-                       $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
-                       if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
-                               $cache[$right] = false;
-                               return false;
-                       }
-               }
-
-               // Allow extensions to say false
-               if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
-                       $cache[$right] = false;
-                       return false;
-               }
-
-               $cache[$right] = true;
-               return true;
+               return MediaWikiServices::getInstance()->getPermissionManager()->isEveryoneAllowed( $right );
        }
 
        /**
@@ -5007,19 +4859,14 @@ class User implements IDBAccessObject, UserIdentity {
 
        /**
         * Get a list of all available permissions.
+        *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
+        *             ->getAllPermissions() instead
+        *
         * @return string[] Array of permission names
         */
        public static function getAllRights() {
-               if ( self::$mAllRights === false ) {
-                       global $wgAvailableRights;
-                       if ( count( $wgAvailableRights ) ) {
-                               self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
-                       } else {
-                               self::$mAllRights = self::$mCoreRights;
-                       }
-                       Hooks::run( 'UserGetAllRights', [ &self::$mAllRights ] );
-               }
-               return self::$mAllRights;
+               return MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
        }
 
        /**
index fd8aedf..bb256c9 100644 (file)
@@ -4863,6 +4863,7 @@ class Language {
        public function viewPrevNext( Title $title, $offset, $limit,
                array $query = [], $atend = false
        ) {
+               wfDeprecated( __METHOD__, '1.34' );
                // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
 
                # Make 'previous' link
index 173d741..c2fa687 100644 (file)
@@ -45,6 +45,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
 class CheckStorage {
        const CONCAT_HEADER = 'O:27:"concatenatedgziphistoryblob"';
        public $oldIdMap, $errors;
+       /** @var ExternalStoreDB */
        public $dbStore = null;
 
        public $errorDescriptions = [
@@ -223,7 +224,8 @@ class CheckStorage {
                        // Check external normal blobs for existence
                        if ( count( $externalNormalBlobs ) ) {
                                if ( is_null( $this->dbStore ) ) {
-                                       $this->dbStore = new ExternalStoreDB;
+                                       $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+                                       $this->dbStore = $esFactory->getStore( 'DB' );
                                }
                                foreach ( $externalConcatBlobs as $cluster => $xBlobIds ) {
                                        $blobIds = array_keys( $xBlobIds );
@@ -422,7 +424,8 @@ class CheckStorage {
                }
 
                if ( is_null( $this->dbStore ) ) {
-                       $this->dbStore = new ExternalStoreDB;
+                       $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+                       $this->dbStore = $esFactory->getStore( 'DB' );
                }
 
                foreach ( $externalConcatBlobs as $cluster => $oldIds ) {
index d3e9ce2..beb1975 100644 (file)
@@ -188,7 +188,9 @@ class CompressOld extends Maintenance {
 
                # Store in external storage if required
                if ( $extdb !== '' ) {
-                       $storeObj = new ExternalStoreDB;
+                       $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+                       /** @var ExternalStoreDB $storeObj */
+                       $storeObj = $esFactory->getStore( 'DB' );
                        $compress = $storeObj->store( $extdb, $compress );
                        if ( $compress === false ) {
                                $this->error( "Unable to store object" );
@@ -232,7 +234,9 @@ class CompressOld extends Maintenance {
 
                # Set up external storage
                if ( $extdb != '' ) {
-                       $storeObj = new ExternalStoreDB;
+                       $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+                       /** @var ExternalStoreDB $storeObj */
+                       $storeObj = $esFactory->getStore( 'DB' );
                }
 
                # Get all articles by page_id
index 0b95ba5..9554797 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Maintenance ExternalStorage
  */
 
+use MediaWiki\MediaWikiServices;
+
 define( 'REPORTING_INTERVAL', 1 );
 
 if ( !defined( 'MEDIAWIKI' ) ) {
@@ -30,21 +32,22 @@ if ( !defined( 'MEDIAWIKI' ) ) {
 
        $fname = 'moveToExternal';
 
-       if ( !isset( $args[0] ) ) {
-               print "Usage: php moveToExternal.php [-s <startid>] [-e <endid>] <cluster>\n";
+       if ( !isset( $args[1] ) ) {
+               print "Usage: php moveToExternal.php [-s <startid>] [-e <endid>] <type> <location>\n";
                exit;
        }
 
-       $cluster = $args[0];
+       $type = $args[0]; // e.g. "DB" or "mwstore"
+       $location = $args[1]; // e.g. "cluster12" or "global-swift"
        $dbw = wfGetDB( DB_MASTER );
 
        $maxID = $options['e'] ?? $dbw->selectField( 'text', 'MAX(old_id)', '', $fname );
        $minID = $options['s'] ?? 1;
 
-       moveToExternal( $cluster, $maxID, $minID );
+       moveToExternal( $type, $location, $maxID, $minID );
 }
 
-function moveToExternal( $cluster, $maxID, $minID = 1 ) {
+function moveToExternal( $type, $location, $maxID, $minID = 1 ) {
        $fname = 'moveToExternal';
        $dbw = wfGetDB( DB_MASTER );
        $dbr = wfGetDB( DB_REPLICA );
@@ -53,7 +56,9 @@ function moveToExternal( $cluster, $maxID, $minID = 1 ) {
        $blockSize = 1000;
        $numBlocks = ceil( $count / $blockSize );
        print "Moving text rows from $minID to $maxID to external storage\n";
-       $ext = new ExternalStoreDB;
+
+       $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+       $extStore = $esFactory->getStore( $type );
        $numMoved = 0;
 
        for ( $block = 0; $block < $numBlocks; $block++ ) {
@@ -108,7 +113,7 @@ function moveToExternal( $cluster, $maxID, $minID = 1 ) {
                        # print "Storing "  . strlen( $text ) . " bytes to $url\n";
                        # print "old_id=$id\n";
 
-                       $url = $ext->store( $cluster, $text );
+                       $url = $extStore->store( $location, $text );
                        if ( !$url ) {
                                print "Error writing to external storage\n";
                                exit;
index f17b00c..e6733a1 100644 (file)
@@ -69,6 +69,7 @@ class RecompressTracked {
        public $replicaId = false;
        public $noCount = false;
        public $debugLog, $infoLog, $criticalLog;
+       /** @var ExternalStoreDB */
        public $store;
 
        private static $optionsWithArgs = [
@@ -109,7 +110,8 @@ class RecompressTracked {
                foreach ( $options as $name => $value ) {
                        $this->$name = $value;
                }
-               $this->store = new ExternalStoreDB;
+               $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory();
+               $this->store = $esFactory->getStore( 'DB' );
                if ( !$this->isChild ) {
                        $GLOBALS['wgDebugLogPrefix'] = "RCT M: ";
                } elseif ( $this->replicaId !== false ) {
index e24c4c5..a42f573 100644 (file)
@@ -18,6 +18,9 @@ class TestSetup {
                global $wgSessionProviders, $wgSessionPbkdf2Iterations;
                global $wgJobTypeConf;
                global $wgAuthManagerConfig;
+               global $wgShowExceptionDetails;
+
+               $wgShowExceptionDetails = true;
 
                // wfWarn should cause tests to fail
                $wgDevelopmentWarnings = true;
index e1dde22..c35e80f 100644 (file)
@@ -54,6 +54,7 @@ $wgAutoloadClasses += [
        'HamcrestPHPUnitIntegration' => "$testDir/phpunit/HamcrestPHPUnitIntegration.php",
        'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
        'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php",
+       'MediaWikiGroupValidator' => "$testDir/phpunit/MediaWikiGroupValidator.php",
        'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php",
        'MediaWikiLoggerPHPUnitTestListener' => "$testDir/phpunit/MediaWikiLoggerPHPUnitTestListener.php",
        'MediaWikiPHPUnitCommand' => "$testDir/phpunit/MediaWikiPHPUnitCommand.php",
diff --git a/tests/phpunit/MediaWikiGroupValidator.php b/tests/phpunit/MediaWikiGroupValidator.php
new file mode 100644 (file)
index 0000000..4daff34
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+/**
+ * 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
+ * @ingroup Testing
+ */
+
+/**
+ * Trait that provides methods to check if group annotations are valid.
+ */
+trait MediaWikiGroupValidator {
+
+       /**
+        * @return bool
+        * @throws ReflectionException
+        * @since 1.34
+        */
+       public function isTestInDatabaseGroup() {
+               // If the test class says it belongs to the Database group, it needs the database.
+               // NOTE: This ONLY checks for the group in the class level doc comment.
+               $rc = new ReflectionClass( $this );
+               return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
+       }
+}
index 999ba47..3216d21 100644 (file)
@@ -24,6 +24,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
        use MediaWikiCoversValidator;
        use PHPUnit4And6Compat;
+       use MediaWikiGroupValidator;
 
        /**
         * The original service locator. This is overridden during setUp().
@@ -1220,8 +1221,23 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
        }
 
        /**
+        * Overrides specific user permissions until services are reloaded
         *
         * @since 1.34
+        *
+        * @param User $user
+        * @param string[]|string $permissions
+        *
+        * @throws Exception
+        */
+       public function overrideUserPermissions( $user, $permissions = [] ) {
+               MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
+                       $user,
+                       $permissions
+               );
+       }
+
+       /**
         * Sets the logger for a specified channel, for the duration of the test.
         * @since 1.27
         * @param string $channel
@@ -1304,17 +1320,6 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                return $this->tablesUsed || $this->isTestInDatabaseGroup();
        }
 
-       /**
-        * @return bool
-        * @since 1.32
-        */
-       protected function isTestInDatabaseGroup() {
-               // If the test class says it belongs to the Database group, it needs the database.
-               // NOTE: This ONLY checks for the group in the class level doc comment.
-               $rc = new ReflectionClass( $this );
-               return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
-       }
-
        /**
         * Insert a new page.
         *
index 06f0c9c..c1dc0f9 100644 (file)
@@ -30,4 +30,17 @@ use PHPUnit\Framework\TestCase;
 abstract class MediaWikiUnitTestCase extends TestCase {
        use PHPUnit4And6Compat;
        use MediaWikiCoversValidator;
+       use MediaWikiGroupValidator;
+
+       /**
+        * @throws ReflectionException
+        */
+       protected function setUp() {
+               parent::setUp();
+               if ( $this->isTestInDatabaseGroup() ) {
+                       throw new \Exception( get_class( $this ) .
+                         ' extends MediaWikiUnitTestCase, and may not have the @group Database annotation.' );
+               }
+       }
+
 }
index 258c822..4b1ade2 100644 (file)
@@ -65,3 +65,4 @@ require_once "$IP/tests/common/TestSetup.php";
 
 wfRequireOnceInGlobalScope( "$IP/includes/AutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/tests/common/TestsAutoLoader.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/Defines.php" );
diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php
deleted file mode 100644 (file)
index 8085bc7..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-/**
- * 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 FauxResponse */
-       protected $response;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->response = new FauxResponse;
-       }
-
-       /**
-        * @covers FauxResponse::setCookie
-        * @covers FauxResponse::getCookie
-        * @covers FauxResponse::getCookieData
-        * @covers FauxResponse::getCookies
-        */
-       public function testCookie() {
-               $expire = time() + 100;
-               $cookie = [
-                       'value' => 'val',
-                       'path' => '/path',
-                       'domain' => 'domain',
-                       'secure' => true,
-                       'httpOnly' => false,
-                       'raw' => false,
-                       'expire' => $expire,
-               ];
-
-               $this->assertEquals( null, $this->response->getCookie( 'xkey' ), 'Non-existing cookie' );
-               $this->response->setCookie( 'key', 'val', $expire, [
-                       'prefix' => 'x',
-                       'path' => '/path',
-                       'domain' => 'domain',
-                       'secure' => 1,
-                       'httpOnly' => 0,
-               ] );
-               $this->assertEquals( 'val', $this->response->getCookie( 'xkey' ), 'Existing cookie' );
-               $this->assertEquals( $cookie, $this->response->getCookieData( 'xkey' ),
-                       'Existing cookie (data)' );
-               $this->assertEquals( [ 'xkey' => $cookie ], $this->response->getCookies(),
-                       'Existing cookies' );
-       }
-
-       /**
-        * @covers FauxResponse::getheader
-        * @covers FauxResponse::header
-        */
-       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'
-               );
-       }
-
-       /**
-        * @covers FauxResponse::getStatusCode
-        */
-       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->statusHeader( 210 );
-               $this->assertEquals(
-                       210,
-                       $this->response->getStatusCode(),
-                       'Handle statusHeader method'
-               );
-
-               $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
deleted file mode 100644 (file)
index 2c78618..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * Test class for FormOptions initialization
- * Ensure the FormOptions::add() does what we want it to do.
- *
- * 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 = TestingAccessWrapper::newFromObject( new FormOptions() );
-       }
-
-       /**
-        * @covers FormOptions::add
-        */
-       public function testAddStringOption() {
-               $this->object->add( 'foo', 'string value' );
-               $this->assertEquals(
-                       [
-                               'foo' => [
-                                       'default' => 'string value',
-                                       'consumed' => false,
-                                       'type' => FormOptions::STRING,
-                                       'value' => null,
-                               ]
-                       ],
-                       $this->object->options
-               );
-       }
-
-       /**
-        * @covers FormOptions::add
-        */
-       public function testAddIntegers() {
-               $this->object->add( 'one', 1 );
-               $this->object->add( 'negone', -1 );
-               $this->assertEquals(
-                       [
-                               'negone' => [
-                                       'default' => -1,
-                                       'value' => null,
-                                       'consumed' => false,
-                                       'type' => FormOptions::INT,
-                               ],
-                               'one' => [
-                                       'default' => 1,
-                                       'value' => null,
-                                       'consumed' => false,
-                                       'type' => FormOptions::INT,
-                               ]
-                       ],
-                       $this->object->options
-               );
-       }
-}
diff --git a/tests/phpunit/includes/FormOptionsTest.php b/tests/phpunit/includes/FormOptionsTest.php
deleted file mode 100644 (file)
index da08670..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<?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.
- *
- * 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( 'float', 0.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 assertGuessFloat( $data ) {
-               $this->guess( FormOptions::FLOAT, $data );
-       }
-
-       private function assertGuessString( $data ) {
-               $this->guess( FormOptions::STRING, $data );
-       }
-
-       private function assertGuessArray( $data ) {
-               $this->guess( FormOptions::ARR, $data );
-       }
-
-       /** Generic helper */
-       private function guess( $expected, $data ) {
-               $this->assertEquals(
-                       $expected,
-                       FormOptions::guessType( $data )
-               );
-       }
-
-       /* @} */
-
-       /**
-        * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString
-        * @covers FormOptions::guessType
-        */
-       public function testGuessTypeDetection() {
-               $this->assertGuessBoolean( true );
-               $this->assertGuessBoolean( false );
-
-               $this->assertGuessInt( 0 );
-               $this->assertGuessInt( -5 );
-               $this->assertGuessInt( 5 );
-               $this->assertGuessInt( 0x0F );
-
-               $this->assertGuessFloat( 0.0 );
-               $this->assertGuessFloat( 1.5 );
-               $this->assertGuessFloat( 1e3 );
-
-               $this->assertGuessString( 'true' );
-               $this->assertGuessString( 'false' );
-               $this->assertGuessString( '5' );
-               $this->assertGuessString( '0' );
-               $this->assertGuessString( '1.5' );
-
-               $this->assertGuessArray( [ 'foo' ] );
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers FormOptions::guessType
-        */
-       public function testGuessTypeOnNullThrowException() {
-               $this->object->guessType( null );
-       }
-}
diff --git a/tests/phpunit/includes/LicensesTest.php b/tests/phpunit/includes/LicensesTest.php
deleted file mode 100644 (file)
index 0e96bf4..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * @covers Licenses
- */
-class LicensesTest extends MediaWikiTestCase {
-
-       public function testLicenses() {
-               $str = "
-* Free licenses:
-** GFDL|Debian disagrees
-";
-
-               $lc = new Licenses( [
-                       '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::class ) );
-       }
-}
index 2ce50b7..88a3f43 100644 (file)
@@ -3,8 +3,12 @@
 namespace MediaWiki\Tests\Permissions;
 
 use Action;
+use FauxRequest;
+use MediaWiki\Session\SessionId;
+use MediaWiki\Session\TestUtils;
 use MediaWikiLangTestCase;
 use RequestContext;
+use stdClass;
 use Title;
 use User;
 use MediaWiki\Block\DatabaseBlock;
@@ -13,6 +17,7 @@ use MediaWiki\Block\Restriction\PageRestriction;
 use MediaWiki\Block\SystemBlock;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Permissions\PermissionManager;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * @group Database
@@ -56,7 +61,32 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        'wgNamespaceProtection' => [
                                NS_MEDIAWIKI => 'editinterface',
                        ],
+                       'wgRevokePermissions' => [
+                               'formertesters' => [
+                                       'runtest' => true
+                               ]
+                       ],
+                       'wgAvailableRights' => [
+                               'test',
+                               'runtest',
+                               'writetest',
+                               'nukeworld',
+                               'modifytest',
+                               'editmyoptions'
+                       ]
                ] );
+
+               $this->setGroupPermissions( 'unittesters', 'test', true );
+               $this->setGroupPermissions( 'unittesters', 'runtest', true );
+               $this->setGroupPermissions( 'unittesters', 'writetest', false );
+               $this->setGroupPermissions( 'unittesters', 'nukeworld', false );
+
+               $this->setGroupPermissions( 'testwriters', 'test', true );
+               $this->setGroupPermissions( 'testwriters', 'writetest', true );
+               $this->setGroupPermissions( 'testwriters', 'modifytest', true );
+
+               $this->setGroupPermissions( '*', 'editmyoptions', true );
+
                // Without this testUserBlock will use a non-English context on non-English MediaWiki
                // installations (because of how Title::checkUserBlock is implemented) and fail.
                RequestContext::resetMain();
@@ -89,19 +119,12 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        $this->user = $this->userUser;
                }
 
-               $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
-
-               $this->overrideMwServices();
+               $this->resetServices();
        }
 
-       protected function setUserPerm( $perm ) {
-               // Setting member variables is evil!!!
-
-               if ( is_array( $perm ) ) {
-                       $this->user->mRights = $perm;
-               } else {
-                       $this->user->mRights = [ $perm ];
-               }
+       public function tearDown() {
+               parent::tearDown();
+               $this->restoreMwServices();
        }
 
        protected function setTitle( $ns, $title = "Main_Page" ) {
@@ -116,6 +139,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                } else {
                        $this->user = $this->altUser;
                }
+               $this->resetServices();
        }
 
        /**
@@ -133,163 +157,165 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
 
                $this->setUser( 'anon' );
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createtalk" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createtalk" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createpage" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createpage" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ "nocreatetext" ] ], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createpage" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createpage" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createtalk" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createtalk" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
 
                $this->setUser( $this->userName );
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createtalk" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createtalk" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createpage" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createpage" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createpage" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createpage" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createtalk" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "createtalk" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'create', $this->user, $this->title );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setUser( 'anon' );
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "move-rootuserpages" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "move-rootuserpages" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "move-rootuserpages" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "move-rootuserpages" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setUser( $this->userName );
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ], $res );
 
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "movefile" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "movefile" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
 
                $this->setUser( 'anon' );
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "movefile" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "movefile" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move', $this->user, $this->title );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setUser( $this->userName );
-               $this->setUserPerm( "move" );
-               $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
+               // $this->setUserPerm( "move" );
+               $this->runGroupPermissions( 'move', 'move', [ [ 'movenotallowedfile' ] ] );
 
-               $this->setUserPerm( "" );
+               // $this->setUserPerm( "" );
                $this->runGroupPermissions(
+                       '',
                        'move',
                        [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ]
                );
 
                $this->setUser( 'anon' );
-               $this->setUserPerm( "move" );
-               $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
+               //$this->setUserPerm( "move" );
+               $this->runGroupPermissions( 'move', 'move', [ [ 'movenotallowedfile' ] ] );
 
-               $this->setUserPerm( "" );
+               // $this->setUserPerm( "" );
                $this->runGroupPermissions(
+                       '',
                        'move',
                        [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ],
                        [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ]
@@ -301,58 +327,58 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
 
                        $this->setTitle( NS_MAIN );
                        $this->setUser( 'anon' );
-                       $this->setUserPerm( "move" );
-                       $this->runGroupPermissions( 'move', [] );
+                       // $this->setUserPerm( "move" );
+                       $this->runGroupPermissions( 'move', 'move', [] );
 
-                       $this->setUserPerm( "" );
-                       $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ],
+                       // $this->setUserPerm( "" );
+                       $this->runGroupPermissions( '', 'move', [ [ 'movenotallowed' ] ],
                                [ [ 'movenologintext' ] ] );
 
                        $this->setUser( $this->userName );
-                       $this->setUserPerm( "" );
-                       $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ] );
+                       // $this->setUserPerm( "" );
+                       $this->runGroupPermissions( '', 'move', [ [ 'movenotallowed' ] ] );
 
-                       $this->setUserPerm( "move" );
-                       $this->runGroupPermissions( 'move', [] );
+                       //$this->setUserPerm( "move" );
+                       $this->runGroupPermissions( 'move', 'move', [] );
 
                        $this->setUser( 'anon' );
-                       $this->setUserPerm( 'move' );
-                       $res = $this->permissionManager
+                       $this->overrideUserPermissions( $this->user, 'move' );
+                       $res = MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title );
                        $this->assertEquals( [], $res );
 
-                       $this->setUserPerm( '' );
-                       $res = $this->permissionManager
+                       $this->overrideUserPermissions( $this->user, '' );
+                       $res = MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title );
                        $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
                }
 
                $this->setTitle( NS_USER );
                $this->setUser( $this->userName );
-               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, [ "move", "move-rootuserpages" ] );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move-target', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
-               $this->setUserPerm( "move" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "move" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move-target', $this->user, $this->title );
                $this->assertEquals( [ [ 'cant-move-to-user-page' ] ], $res );
 
                $this->setUser( 'anon' );
-               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, [ "move", "move-rootuserpages" ] );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move-target', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_USER, "User/subpage" );
-               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, [ "move", "move-rootuserpages" ] );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move-target', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
-               $this->setUserPerm( "move" );
-               $res = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, "move" );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'move-target', $this->user, $this->title );
                $this->assertEquals( [], $res );
 
@@ -378,54 +404,58 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                ];
 
                foreach ( [ "edit", "protect", "" ] as $action ) {
-                       $this->setUserPerm( null );
+                       $this->overrideUserPermissions( $this->user );
                        $this->assertEquals( $check[$action][0],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, true ) );
                        $this->assertEquals( $check[$action][0],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
                        $this->assertEquals( $check[$action][0],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
 
                        global $wgGroupPermissions;
                        $old = $wgGroupPermissions;
                        $wgGroupPermissions = [];
+                       $this->resetServices();
 
                        $this->assertEquals( $check[$action][1],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, true ) );
                        $this->assertEquals( $check[$action][1],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
                        $this->assertEquals( $check[$action][1],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
                        $wgGroupPermissions = $old;
+                       $this->resetServices();
 
-                       $this->setUserPerm( $action );
+                       $this->overrideUserPermissions( $this->user, $action );
                        $this->assertEquals( $check[$action][2],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, true ) );
                        $this->assertEquals( $check[$action][2],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
                        $this->assertEquals( $check[$action][2],
-                               $this->permissionManager
+                               MediaWikiServices::getInstance()->getPermissionManager()
                                        ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
 
-                       $this->setUserPerm( $action );
+                       $this->overrideUserPermissions( $this->user, $action );
                        $this->assertEquals( $check[$action][3],
-                               $this->permissionManager->userCan( $action, $this->user, $this->title, true ) );
+                               MediaWikiServices::getInstance()->getPermissionManager()
+                                       ->userCan( $action, $this->user, $this->title, true ) );
                        $this->assertEquals( $check[$action][3],
-                               $this->permissionManager->userCan( $action, $this->user, $this->title,
+                               MediaWikiServices::getInstance()->getPermissionManager()
+                                       ->userCan( $action, $this->user, $this->title,
                                        PermissionManager::RIGOR_QUICK ) );
                        # count( User::getGroupsWithPermissions( $action ) ) < 1
                }
        }
 
-       protected function runGroupPermissions( $action, $result, $result2 = null ) {
+       protected function runGroupPermissions( $perm, $action, $result, $result2 = null ) {
                global $wgGroupPermissions;
 
                if ( $result2 === null ) {
@@ -434,25 +464,33 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
 
                $wgGroupPermissions['autoconfirmed']['move'] = false;
                $wgGroupPermissions['user']['move'] = false;
-               $res = $this->permissionManager
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $perm );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( $action, $this->user, $this->title );
                $this->assertEquals( $result, $res );
 
                $wgGroupPermissions['autoconfirmed']['move'] = true;
                $wgGroupPermissions['user']['move'] = false;
-               $res = $this->permissionManager
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $perm );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( $action, $this->user, $this->title );
                $this->assertEquals( $result2, $res );
 
                $wgGroupPermissions['autoconfirmed']['move'] = true;
                $wgGroupPermissions['user']['move'] = true;
-               $res = $this->permissionManager
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $perm );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( $action, $this->user, $this->title );
                $this->assertEquals( $result2, $res );
 
                $wgGroupPermissions['autoconfirmed']['move'] = false;
                $wgGroupPermissions['user']['move'] = true;
-               $res = $this->permissionManager
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $perm );
+               $res = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( $action, $this->user, $this->title );
                $this->assertEquals( $result2, $res );
        }
@@ -469,57 +507,59 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                $this->setTitle( NS_SPECIAL );
 
                $this->assertEquals( [ [ 'badaccess-group0' ], [ 'ns-specialprotected' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user, '' );
                $this->assertEquals( [ [ 'badaccess-group0' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
 
                $wgNamespaceProtection[NS_USER] = [ 'bogus' ];
 
                $this->setTitle( NS_USER );
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user, '' );
                $this->assertEquals( [ [ 'badaccess-group0' ],
                        [ 'namespaceprotected', 'User', 'bogus' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
 
                $this->setTitle( NS_MEDIAWIKI );
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
 
                $this->setTitle( NS_MEDIAWIKI );
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
 
                $wgNamespaceProtection = null;
 
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
                $this->assertEquals( true,
-                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()
+                               ->userCan( 'bogus', $this->user, $this->title ) );
 
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user, '' );
                $this->assertEquals( [ [ 'badaccess-group0' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()
+                               ->userCan( 'bogus', $this->user, $this->title ) );
        }
 
        /**
@@ -716,48 +756,48 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                $resultUserJs,
                $resultPatrol
        ) {
-               $this->setUserPerm( '' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultNone, $result );
 
-               $this->setUserPerm( 'editmyusercss' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, 'editmyusercss' );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultMyCss, $result );
 
-               $this->setUserPerm( 'editmyuserjson' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, 'editmyuserjson' );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultMyJson, $result );
 
-               $this->setUserPerm( 'editmyuserjs' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, 'editmyuserjs' );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultMyJs, $result );
 
-               $this->setUserPerm( 'editusercss' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, 'editusercss' );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultUserCss, $result );
 
-               $this->setUserPerm( 'edituserjson' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, 'edituserjson' );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultUserJson, $result );
 
-               $this->setUserPerm( 'edituserjs' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, 'edituserjs' );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( $resultUserJs, $result );
 
-               $this->setUserPerm( '' );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'patrol', $this->user, $this->title );
                $this->assertEquals( reset( $resultPatrol[0] ), reset( $result[0] ) );
 
-               $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] );
-               $result = $this->permissionManager
+               $this->overrideUserPermissions( $this->user, [ 'edituserjs', 'edituserjson', 'editusercss' ] );
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
                        ->getPermissionErrors( 'bogus', $this->user, $this->title );
                $this->assertEquals( [ [ 'badaccess-group0' ] ], $result );
        }
@@ -777,16 +817,16 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
 
                $this->setTitle( NS_MAIN );
                $this->title->mRestrictionsLoaded = true;
-               $this->setUserPerm( "edit" );
+               $this->overrideUserPermissions( $this->user, "edit" );
                $this->title->mRestrictions = [ "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
 
                $this->assertEquals( [],
-                       $this->permissionManager->getPermissionErrors( 'edit',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()
+                               ->getPermissionErrors( 'edit', $this->user, $this->title ) );
 
                $this->assertEquals( true,
-                       $this->permissionManager->userCan( 'edit', $this->user, $this->title,
-                               PermissionManager::RIGOR_QUICK ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()
+                               ->userCan( 'edit', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
 
                $this->title->mRestrictions = [ "edit" => [ 'bogus', "sysop", "protect", "" ],
                        "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
@@ -795,81 +835,81 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        [ 'protectedpagetext', 'bogus', 'bogus' ],
                        [ 'protectedpagetext', 'editprotected', 'bogus' ],
                        [ 'protectedpagetext', 'protect', 'bogus' ] ],
-                       $this->permissionManager->getPermissionErrors( 'bogus',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'bogus', $this->user, $this->title ) );
                $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
                        [ 'protectedpagetext', 'editprotected', 'edit' ],
                        [ 'protectedpagetext', 'protect', 'edit' ] ],
-                       $this->permissionManager->getPermissionErrors( 'edit',
-                               $this->user, $this->title ) );
-               $this->setUserPerm( "" );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'edit', $this->user, $this->title ) );
+               $this->overrideUserPermissions( $this->user );
                $this->assertEquals( [ [ 'badaccess-group0' ],
                        [ 'protectedpagetext', 'bogus', 'bogus' ],
                        [ 'protectedpagetext', 'editprotected', 'bogus' ],
                        [ 'protectedpagetext', 'protect', 'bogus' ] ],
-                       $this->permissionManager->getPermissionErrors( 'bogus',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'bogus', $this->user, $this->title ) );
                $this->assertEquals( [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ],
                        [ 'protectedpagetext', 'bogus', 'edit' ],
                        [ 'protectedpagetext', 'editprotected', 'edit' ],
                        [ 'protectedpagetext', 'protect', 'edit' ] ],
-                       $this->permissionManager->getPermissionErrors( 'edit',
-                               $this->user, $this->title ) );
-               $this->setUserPerm( [ "edit", "editprotected" ] );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'edit', $this->user, $this->title ) );
+               $this->overrideUserPermissions( $this->user, [ "edit", "editprotected" ] );
                $this->assertEquals( [ [ 'badaccess-group0' ],
                        [ 'protectedpagetext', 'bogus', 'bogus' ],
                        [ 'protectedpagetext', 'protect', 'bogus' ] ],
-                       $this->permissionManager->getPermissionErrors( 'bogus',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'bogus', $this->user, $this->title ) );
                $this->assertEquals( [
                        [ 'protectedpagetext', 'bogus', 'edit' ],
                        [ 'protectedpagetext', 'protect', 'edit' ] ],
-                       $this->permissionManager->getPermissionErrors( 'edit',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'edit', $this->user, $this->title ) );
 
                $this->title->mCascadeRestriction = true;
-               $this->setUserPerm( "edit" );
+               $this->overrideUserPermissions( $this->user, "edit" );
 
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title,
-                               PermissionManager::RIGOR_QUICK ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()
+                               ->userCan( 'bogus', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
 
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'edit', $this->user, $this->title,
-                               PermissionManager::RIGOR_QUICK ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'edit', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
 
                $this->assertEquals( [ [ 'badaccess-group0' ],
                        [ 'protectedpagetext', 'bogus', 'bogus' ],
                        [ 'protectedpagetext', 'editprotected', 'bogus' ],
                        [ 'protectedpagetext', 'protect', 'bogus' ] ],
-                       $this->permissionManager->getPermissionErrors( 'bogus',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'bogus', $this->user, $this->title ) );
                $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
                        [ 'protectedpagetext', 'editprotected', 'edit' ],
                        [ 'protectedpagetext', 'protect', 'edit' ] ],
-                       $this->permissionManager->getPermissionErrors( 'edit',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'edit', $this->user, $this->title ) );
 
-               $this->setUserPerm( [ "edit", "editprotected" ] );
+               $this->overrideUserPermissions( $this->user, [ "edit", "editprotected" ] );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title,
-                               PermissionManager::RIGOR_QUICK ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'bogus', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
 
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'edit', $this->user, $this->title,
-                               PermissionManager::RIGOR_QUICK ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'edit', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
 
                $this->assertEquals( [ [ 'badaccess-group0' ],
                        [ 'protectedpagetext', 'bogus', 'bogus' ],
                        [ 'protectedpagetext', 'protect', 'bogus' ],
                        [ 'protectedpagetext', 'protect', 'bogus' ] ],
-                       $this->permissionManager->getPermissionErrors( 'bogus',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'bogus', $this->user, $this->title ) );
                $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
                        [ 'protectedpagetext', 'protect', 'edit' ],
                        [ 'protectedpagetext', 'protect', 'edit' ] ],
-                       $this->permissionManager->getPermissionErrors( 'edit',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'edit', $this->user, $this->title ) );
        }
 
        /**
@@ -877,7 +917,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
         */
        public function testCascadingSourcesRestrictions() {
                $this->setTitle( NS_MAIN, "test page" );
-               $this->setUserPerm( [ "edit", "bogus" ] );
+               $this->overrideUserPermissions( $this->user, [ "edit", "bogus" ] );
 
                $this->title->mCascadeSources = [
                        Title::makeTitle( NS_MAIN, "Bogus" ),
@@ -888,17 +928,21 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                ];
 
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'bogus', $this->user, $this->title ) );
                $this->assertEquals( [
                        [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
                        [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
                        [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ] ],
-                       $this->permissionManager->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'bogus', $this->user, $this->title ) );
 
                $this->assertEquals( true,
-                       $this->permissionManager->userCan( 'edit', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'edit', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager->getPermissionErrors( 'edit', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'edit', $this->user, $this->title ) );
        }
 
        /**
@@ -907,7 +951,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
         * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
         */
        public function testActionPermissions() {
-               $this->setUserPerm( [ "createpage" ] );
+               $this->overrideUserPermissions( $this->user, [ "createpage" ] );
                $this->setTitle( NS_MAIN, "test page" );
                $this->title->mTitleProtection['permission'] = '';
                $this->title->mTitleProtection['user'] = $this->user->getId();
@@ -916,75 +960,85 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                $this->title->mCascadeRestriction = false;
 
                $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'create', $this->user, $this->title ) );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'create', $this->user, $this->title ) );
 
                $this->title->mTitleProtection['permission'] = 'editprotected';
-               $this->setUserPerm( [ 'createpage', 'protect' ] );
+               $this->overrideUserPermissions( $this->user, [ 'createpage', 'protect' ] );
                $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'create', $this->user, $this->title ) );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'create', $this->user, $this->title ) );
 
-               $this->setUserPerm( [ 'createpage', 'editprotected' ] );
+               $this->overrideUserPermissions( $this->user, [ 'createpage', 'editprotected' ] );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'create', $this->user, $this->title ) );
                $this->assertEquals( true,
-                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'create', $this->user, $this->title ) );
 
-               $this->setUserPerm( [ 'createpage' ] );
+               $this->overrideUserPermissions( $this->user, [ 'createpage' ] );
                $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'create', $this->user, $this->title ) );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'create', $this->user, $this->title ) );
 
                $this->setTitle( NS_MEDIA, "test page" );
-               $this->setUserPerm( [ "move" ] );
+               $this->overrideUserPermissions( $this->user, [ "move" ] );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'move', $this->user, $this->title ) );
                $this->assertEquals( [ [ 'immobile-source-namespace', 'Media' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move', $this->user, $this->title ) );
 
                $this->setTitle( NS_HELP, "test page" );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move', $this->user, $this->title ) );
                $this->assertEquals( true,
-                       $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'move', $this->user, $this->title ) );
 
                $this->title->mInterwiki = "no";
                $this->assertEquals( [ [ 'immobile-source-page' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move', $this->user, $this->title ) );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'move', $this->user, $this->title ) );
 
                $this->setTitle( NS_MEDIA, "test page" );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'move-target', $this->user, $this->title ) );
                $this->assertEquals( [ [ 'immobile-target-namespace', 'Media' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
 
                $this->setTitle( NS_HELP, "test page" );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
                $this->assertEquals( true,
-                       $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'move-target', $this->user, $this->title ) );
 
                $this->title->mInterwiki = "no";
                $this->assertEquals( [ [ 'immobile-target-page' ] ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
                $this->assertEquals( false,
-                       $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->userCan(
+                               'move-target', $this->user, $this->title ) );
        }
 
        /**
@@ -997,10 +1051,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        'wgBlockDisablesLogin' => false,
                ] );
 
-               $this->overrideMwServices();
-               $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
-
-               $this->setUserPerm( [
+               $this->overrideUserPermissions( $this->user, [
                        'createpage',
                        'edit',
                        'move',
@@ -1013,24 +1064,32 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
 
                # $wgEmailConfirmToEdit only applies to 'edit' action
                $this->assertEquals( [],
-                       $this->permissionManager->getPermissionErrors( 'move-target',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'move-target', $this->user, $this->title ) );
                $this->assertContains( [ 'confirmedittext' ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'edit', $this->user, $this->title ) );
 
                $this->setMwGlobals( 'wgEmailConfirmToEdit', false );
-               $this->overrideMwServices();
-               $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, [
+                       'createpage',
+                       'edit',
+                       'move',
+                       'rollback',
+                       'patrol',
+                       'upload',
+                       'purge'
+               ] );
 
                $this->assertNotContains( [ 'confirmedittext' ],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'edit', $this->user, $this->title ) );
 
                # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
                $this->assertEquals( [],
-                       $this->permissionManager->getPermissionErrors( 'move-target',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'move-target', $this->user, $this->title ) );
 
                global $wgLang;
                $prev = time();
@@ -1049,13 +1108,13 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        '[[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->permissionManager->getPermissionErrors( 'move-target',
-                               $this->user, $this->title ) );
+                       MediaWikiServices::getInstance()->getPermissionManager()->getPermissionErrors(
+                               'move-target', $this->user, $this->title ) );
 
-               $this->assertEquals( false, $this->permissionManager
+               $this->assertEquals( false, MediaWikiServices::getInstance()->getPermissionManager()
                        ->userCan( 'move-target', $this->user, $this->title ) );
                // quickUserCan should ignore user blocks
-               $this->assertEquals( true, $this->permissionManager
+               $this->assertEquals( true, MediaWikiServices::getInstance()->getPermissionManager()
                        ->userCan( 'move-target', $this->user, $this->title,
                                PermissionManager::RIGOR_QUICK ) );
 
@@ -1074,7 +1133,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        '[[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->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
                # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this )
                #   $user->blockedFor() == ''
@@ -1096,22 +1155,22 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
 
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'edit', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'upload', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'purge', $this->user, $this->title ) );
 
                // partial block message test
@@ -1126,22 +1185,22 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                ] );
 
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'edit', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'upload', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'purge', $this->user, $this->title ) );
 
                $this->user->mBlock->setRestrictions( [
@@ -1154,22 +1213,22 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
 
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'edit', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'upload', $this->user, $this->title ) );
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'purge', $this->user, $this->title ) );
 
                // Test no block.
@@ -1177,7 +1236,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                $this->user->mBlock = null;
 
                $this->assertEquals( [],
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'edit', $this->user, $this->title ) );
        }
 
@@ -1228,7 +1287,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                        $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
 
                $this->assertEquals( $errors,
-                       $this->permissionManager
+                       MediaWikiServices::getInstance()->getPermissionManager()
                                ->getPermissionErrors( 'tester', $this->user, $this->title ) );
        }
 
@@ -1243,7 +1302,7 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                //$this->assertSame( '', $user->blockedBy(), 'sanity check' );
                //$this->assertSame( '', $user->blockedFor(), 'sanity check' );
                //$this->assertFalse( (bool)$user->isHidden(), 'sanity check' );
-               $this->assertFalse( $this->permissionManager
+               $this->assertFalse( MediaWikiServices::getInstance()->getPermissionManager()
                        ->isBlockedFrom( $user, $ut ), 'sanity check' );
 
                // Block the user
@@ -1264,7 +1323,8 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                //$this->assertSame( $blocker->getName(), $user->blockedBy() );
                //$this->assertSame( 'Because', $user->blockedFor() );
                //$this->assertTrue( (bool)$user->isHidden() );
-               $this->assertTrue( $this->permissionManager->isBlockedFrom( $user, $ut ) );
+               $this->assertTrue( MediaWikiServices::getInstance()->getPermissionManager()
+                       ->isBlockedFrom( $user, $ut ) );
 
                // Unblock
                $block->delete();
@@ -1275,7 +1335,8 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                //$this->assertSame( '', $user->blockedBy() );
                //$this->assertSame( '', $user->blockedFor() );
                //$this->assertFalse( (bool)$user->isHidden() );
-               $this->assertFalse( $this->permissionManager->isBlockedFrom( $user, $ut ) );
+               $this->assertFalse( MediaWikiServices::getInstance()->getPermissionManager()
+                       ->isBlockedFrom( $user, $ut ) );
        }
 
        /**
@@ -1325,7 +1386,8 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                $block->insert();
 
                try {
-                       $this->assertSame( $expect, $this->permissionManager->isBlockedFrom( $user, $title ) );
+                       $this->assertSame( $expect, MediaWikiServices::getInstance()->getPermissionManager()
+                               ->isBlockedFrom( $user, $title ) );
                } finally {
                        $block->delete();
                }
@@ -1408,4 +1470,187 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::getUserPermissions
+        */
+       public function testGetUserPermissions() {
+               $user = $this->getTestUser( [ 'unittesters' ] )->getUser();
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getUserPermissions( $user );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::getUserPermissions
+        */
+       public function testGetUserPermissionsHooks() {
+               $user = $this->getTestUser( [ 'unittesters', 'testwriters' ] )->getUser();
+               $userWrapper = TestingAccessWrapper::newFromObject( $user );
+
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getUserPermissions( $user );
+               $this->assertContains( 'test', $rights, 'sanity check' );
+               $this->assertContains( 'runtest', $rights, 'sanity check' );
+               $this->assertContains( 'writetest', $rights, 'sanity check' );
+               $this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
+
+               // Add a hook manipluating the rights
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserGetRights' => [ function ( $user, &$rights ) {
+                       $rights[] = 'nukeworld';
+                       $rights = array_diff( $rights, [ 'writetest' ] );
+               } ] ] );
+
+               $this->resetServices();
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getUserPermissions( $user );
+               $this->assertContains( 'test', $rights );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertContains( 'nukeworld', $rights );
+
+               // Add a Session that limits rights
+               $mock = $this->getMockBuilder( stdClass::class )
+                       ->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
+                       ->getMock();
+               $mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] );
+               $mock->method( 'getSessionId' )->willReturn(
+                       new SessionId( str_repeat( 'X', 32 ) )
+               );
+               $session = TestUtils::getDummySession( $mock );
+               $mockRequest = $this->getMockBuilder( FauxRequest::class )
+                       ->setMethods( [ 'getSession' ] )
+                       ->getMock();
+               $mockRequest->method( 'getSession' )->willReturn( $session );
+               $userWrapper->mRequest = $mockRequest;
+
+               $this->resetServices();
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getUserPermissions( $user );
+               $this->assertContains( 'test', $rights );
+               $this->assertNotContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::getGroupPermissions
+        */
+       public function testGroupPermissions() {
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getGroupPermissions( [ 'unittesters' ] );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getGroupPermissions( [ 'unittesters', 'testwriters' ] );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertContains( 'writetest', $rights );
+               $this->assertContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::getGroupPermissions
+        */
+       public function testRevokePermissions() {
+               $rights = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getGroupPermissions( [ 'unittesters', 'formertesters' ] );
+               $this->assertNotContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @dataProvider provideGetGroupsWithPermission
+        * @covers \MediaWiki\Permissions\PermissionManager::getGroupsWithPermission
+        */
+       public function testGetGroupsWithPermission( $expected, $right ) {
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getGroupsWithPermission( $right );
+               sort( $result );
+               sort( $expected );
+
+               $this->assertEquals( $expected, $result, "Groups with permission $right" );
+       }
+
+       public static function provideGetGroupsWithPermission() {
+               return [
+                       [
+                               [ 'unittesters', 'testwriters' ],
+                               'test'
+                       ],
+                       [
+                               [ 'unittesters' ],
+                               'runtest'
+                       ],
+                       [
+                               [ 'testwriters' ],
+                               'writetest'
+                       ],
+                       [
+                               [ 'testwriters' ],
+                               'modifytest'
+                       ],
+               ];
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::userHasRight
+        */
+       public function testUserHasRight() {
+               $result = MediaWikiServices::getInstance()->getPermissionManager()->userHasRight(
+                       $this->getTestUser( 'unittesters' )->getUser(),
+                       'test'
+               );
+               $this->assertTrue( $result );
+
+               $result = MediaWikiServices::getInstance()->getPermissionManager()->userHasRight(
+                       $this->getTestUser( 'formertesters' )->getUser(),
+                       'runtest'
+               );
+               $this->assertFalse( $result );
+
+               $result = MediaWikiServices::getInstance()->getPermissionManager()->userHasRight(
+                       $this->getTestUser( 'formertesters' )->getUser(),
+                       ''
+               );
+               $this->assertTrue( $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::groupHasPermission
+        */
+       public function testGroupHasPermission() {
+               $result = MediaWikiServices::getInstance()->getPermissionManager()->groupHasPermission(
+                       'unittesters',
+                       'test'
+               );
+               $this->assertTrue( $result );
+
+               $result = MediaWikiServices::getInstance()->getPermissionManager()->groupHasPermission(
+                       'formertesters',
+                       'runtest'
+               );
+               $this->assertFalse( $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Permissions\PermissionManager::isEveryoneAllowed
+        */
+       public function testIsEveryoneAllowed() {
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
+                                                                  ->isEveryoneAllowed( 'editmyoptions' );
+               $this->assertTrue( $result );
+
+               $result = MediaWikiServices::getInstance()->getPermissionManager()
+                                                                  ->isEveryoneAllowed( 'test' );
+               $this->assertFalse( $result );
+       }
+
 }
diff --git a/tests/phpunit/includes/Rest/HeaderContainerTest.php b/tests/phpunit/includes/Rest/HeaderContainerTest.php
deleted file mode 100644 (file)
index e0dbfdf..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest;
-
-use MediaWikiTestCase;
-use MediaWiki\Rest\HeaderContainer;
-
-/**
- * @covers \MediaWiki\Rest\HeaderContainer
- */
-class HeaderContainerTest extends MediaWikiTestCase {
-       public static function provideSetHeader() {
-               return [
-                       'simple' => [
-                               [
-                                       [ 'Test', 'foo' ]
-                               ],
-                               [ 'Test' => [ 'foo' ] ],
-                               [ 'Test' => 'foo' ]
-                       ],
-                       'replace' => [
-                               [
-                                       [ 'Test', 'foo' ],
-                                       [ 'Test', 'bar' ],
-                               ],
-                               [ 'Test' => [ 'bar' ] ],
-                               [ 'Test' => 'bar' ],
-                       ],
-                       'array value' => [
-                               [
-                                       [ 'Test', [ '1', '2' ] ],
-                                       [ 'Test', [ '3', '4' ] ],
-                               ],
-                               [ 'Test' => [ '3', '4' ] ],
-                               [ 'Test' => '3, 4' ]
-                       ],
-                       'preserve most recent case' => [
-                               [
-                                       [ 'test', 'foo' ],
-                                       [ 'tesT', 'bar' ],
-                               ],
-                               [ 'tesT' => [ 'bar' ] ],
-                               [ 'tesT' => 'bar' ]
-                       ],
-                       'empty' => [ [], [], [] ],
-               ];
-       }
-
-       /** @dataProvider provideSetHeader */
-       public function testSetHeader( $setOps, $headers, $lines ) {
-               $hc = new HeaderContainer;
-               foreach ( $setOps as list( $name, $value ) ) {
-                       $hc->setHeader( $name, $value );
-               }
-               $this->assertSame( $headers, $hc->getHeaders() );
-               $this->assertSame( $lines, $hc->getHeaderLines() );
-       }
-
-       public static function provideAddHeader() {
-               return [
-                       'simple' => [
-                               [
-                                       [ 'Test', 'foo' ]
-                               ],
-                               [ 'Test' => [ 'foo' ] ],
-                               [ 'Test' => 'foo' ]
-                       ],
-                       'add' => [
-                               [
-                                       [ 'Test', 'foo' ],
-                                       [ 'Test', 'bar' ],
-                               ],
-                               [ 'Test' => [ 'foo', 'bar' ] ],
-                               [ 'Test' => 'foo, bar' ],
-                       ],
-                       'array value' => [
-                               [
-                                       [ 'Test', [ '1', '2' ] ],
-                                       [ 'Test', [ '3', '4' ] ],
-                               ],
-                               [ 'Test' => [ '1', '2', '3', '4' ] ],
-                               [ 'Test' => '1, 2, 3, 4' ]
-                       ],
-                       'preserve original case' => [
-                               [
-                                       [ 'Test', 'foo' ],
-                                       [ 'tesT', 'bar' ],
-                               ],
-                               [ 'Test' => [ 'foo', 'bar' ] ],
-                               [ 'Test' => 'foo, bar' ]
-                       ],
-               ];
-       }
-
-       /** @dataProvider provideAddHeader */
-       public function testAddHeader( $addOps, $headers, $lines ) {
-               $hc = new HeaderContainer;
-               foreach ( $addOps as list( $name, $value ) ) {
-                       $hc->addHeader( $name, $value );
-               }
-               $this->assertSame( $headers, $hc->getHeaders() );
-               $this->assertSame( $lines, $hc->getHeaderLines() );
-       }
-
-       public static function provideRemoveHeader() {
-               return [
-                       'simple' => [
-                               [ [ 'Test', 'foo' ] ],
-                               [ 'Test' ],
-                               [],
-                               []
-                       ],
-                       'case mismatch' => [
-                               [ [ 'Test', 'foo' ] ],
-                               [ 'tesT' ],
-                               [],
-                               []
-                       ],
-                       'remove nonexistent' => [
-                               [ [ 'A', '1' ] ],
-                               [ 'B' ],
-                               [ 'A' => [ '1' ] ],
-                               [ 'A' => '1' ]
-                       ],
-               ];
-       }
-
-       /** @dataProvider provideRemoveHeader */
-       public function testRemoveHeader( $addOps, $removeOps, $headers, $lines ) {
-               $hc = new HeaderContainer;
-               foreach ( $addOps as list( $name, $value ) ) {
-                       $hc->addHeader( $name, $value );
-               }
-               foreach ( $removeOps as $name ) {
-                       $hc->removeHeader( $name );
-               }
-               $this->assertSame( $headers, $hc->getHeaders() );
-               $this->assertSame( $lines, $hc->getHeaderLines() );
-       }
-
-       public function testHasHeader() {
-               $hc = new HeaderContainer;
-               $hc->addHeader( 'A', '1' );
-               $hc->addHeader( 'B', '2' );
-               $hc->addHeader( 'C', '3' );
-               $hc->removeHeader( 'B' );
-               $hc->removeHeader( 'c' );
-               $this->assertTrue( $hc->hasHeader( 'A' ) );
-               $this->assertTrue( $hc->hasHeader( 'a' ) );
-               $this->assertFalse( $hc->hasHeader( 'B' ) );
-               $this->assertFalse( $hc->hasHeader( 'c' ) );
-               $this->assertFalse( $hc->hasHeader( 'C' ) );
-       }
-
-       public function testGetRawHeaderLines() {
-               $hc = new HeaderContainer;
-               $hc->addHeader( 'A', '1' );
-               $hc->addHeader( 'a', '2' );
-               $hc->addHeader( 'b', '3' );
-               $hc->addHeader( 'Set-Cookie', 'x' );
-               $hc->addHeader( 'SET-cookie', 'y' );
-               $this->assertSame(
-                       [
-                               'A: 1, 2',
-                               'b: 3',
-                               'Set-Cookie: x',
-                               'Set-Cookie: y',
-                       ],
-                       $hc->getRawHeaderLines()
-               );
-       }
-}
diff --git a/tests/phpunit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php b/tests/phpunit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php
deleted file mode 100644 (file)
index 935cec1..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest\PathTemplateMatcher;
-
-use MediaWiki\Rest\PathTemplateMatcher\PathConflict;
-use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
-use MediaWikiTestCase;
-
-/**
- * @covers \MediaWiki\Rest\PathTemplateMatcher\PathMatcher
- * @covers \MediaWiki\Rest\PathTemplateMatcher\PathConflict
- */
-class PathMatcherTest extends MediaWikiTestCase {
-       private static $normalRoutes = [
-               '/a/b',
-               '/b/{x}',
-               '/c/{x}/d',
-               '/c/{x}/e',
-               '/c/{x}/{y}/d',
-       ];
-
-       public static function provideConflictingRoutes() {
-               return [
-                       [ '/a/b', 0, '/a/b' ],
-                       [ '/a/{x}', 0, '/a/b' ],
-                       [ '/{x}/c', 1, '/b/{x}' ],
-                       [ '/b/a', 1, '/b/{x}' ],
-                       [ '/b/{x}', 1, '/b/{x}' ],
-                       [ '/{x}/{y}/d', 2, '/c/{x}/d' ],
-               ];
-       }
-
-       public static function provideMatch() {
-               return [
-                       [ '', false ],
-                       [ '/a/b', [ 'params' => [], 'userData' => 0 ] ],
-                       [ '/b', false ],
-                       [ '/b/1', [ 'params' => [ 'x' => '1' ], 'userData' => 1 ] ],
-                       [ '/c/1/d', [ 'params' => [ 'x' => '1' ], 'userData' => 2 ] ],
-                       [ '/c/1/e', [ 'params' => [ 'x' => '1' ], 'userData' => 3 ] ],
-                       [ '/c/000/e', [ 'params' => [ 'x' => '000' ], 'userData' => 3 ] ],
-                       [ '/c/1/f', false ],
-                       [ '/c//e', [ 'params' => [ 'x' => '' ], 'userData' => 3 ] ],
-                       [ '/c///e', false ],
-               ];
-       }
-
-       public function createNormalRouter() {
-               $pm = new PathMatcher;
-               foreach ( self::$normalRoutes as $i => $route ) {
-                       $pm->add( $route, $i );
-               }
-               return $pm;
-       }
-
-       /** @dataProvider provideConflictingRoutes */
-       public function testAddConflict( $attempt, $expectedUserData, $expectedTemplate ) {
-               $pm = $this->createNormalRouter();
-               $actualTemplate = null;
-               $actualUserData = null;
-               try {
-                       $pm->add( $attempt, 'conflict' );
-               } catch ( PathConflict $pc ) {
-                       $actualTemplate = $pc->existingTemplate;
-                       $actualUserData = $pc->existingUserData;
-               }
-               $this->assertSame( $expectedUserData, $actualUserData );
-               $this->assertSame( $expectedTemplate, $actualTemplate );
-       }
-
-       /** @dataProvider provideMatch */
-       public function testMatch( $path, $expectedResult ) {
-               $pm = $this->createNormalRouter();
-               $result = $pm->match( $path );
-               $this->assertSame( $expectedResult, $result );
-       }
-}
diff --git a/tests/phpunit/includes/Rest/StringStreamTest.php b/tests/phpunit/includes/Rest/StringStreamTest.php
deleted file mode 100644 (file)
index f474643..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest;
-
-use MediaWiki\Rest\StringStream;
-use MediaWikiTestCase;
-
-/** @covers \MediaWiki\Rest\StringStream */
-class StringStreamTest extends MediaWikiTestCase {
-       public static function provideSeekGetContents() {
-               return [
-                       [ 'abcde', 0, SEEK_SET, 'abcde' ],
-                       [ 'abcde', 1, SEEK_SET, 'bcde' ],
-                       [ 'abcde', 5, SEEK_SET, '' ],
-                       [ 'abcde', 1, SEEK_CUR, 'cde' ],
-                       [ 'abcde', 0, SEEK_END, '' ],
-               ];
-       }
-
-       /** @dataProvider provideSeekGetContents */
-       public function testCopyToStream( $input, $offset, $whence, $expected ) {
-               $ss = new StringStream;
-               $ss->write( $input );
-               $ss->seek( 1 );
-               $ss->seek( $offset, $whence );
-               $destStream = fopen( 'php://memory', 'w+' );
-               $ss->copyToStream( $destStream );
-               fseek( $destStream, 0 );
-               $result = stream_get_contents( $destStream );
-               $this->assertSame( $expected, $result );
-       }
-
-       public function testGetSize() {
-               $ss = new StringStream;
-               $this->assertSame( 0, $ss->getSize() );
-               $ss->write( "hello" );
-               $this->assertSame( 5, $ss->getSize() );
-               $ss->rewind();
-               $this->assertSame( 5, $ss->getSize() );
-       }
-
-       public function testTell() {
-               $ss = new StringStream;
-               $this->assertSame( $ss->tell(), 0 );
-               $ss->write( "abc" );
-               $this->assertSame( $ss->tell(), 3 );
-               $ss->seek( 0 );
-               $ss->read( 1 );
-               $this->assertSame( $ss->tell(), 1 );
-       }
-
-       public function testEof() {
-               $ss = new StringStream( 'abc' );
-               $this->assertFalse( $ss->eof() );
-               $ss->read( 1 );
-               $this->assertFalse( $ss->eof() );
-               $ss->read( 1 );
-               $this->assertFalse( $ss->eof() );
-               $ss->read( 1 );
-               $this->assertTrue( $ss->eof() );
-               $ss->rewind();
-               $this->assertFalse( $ss->eof() );
-       }
-
-       public function testIsSeekable() {
-               $ss = new StringStream;
-               $this->assertTrue( $ss->isSeekable() );
-       }
-
-       public function testIsReadable() {
-               $ss = new StringStream;
-               $this->assertTrue( $ss->isReadable() );
-       }
-
-       public function testIsWritable() {
-               $ss = new StringStream;
-               $this->assertTrue( $ss->isWritable() );
-       }
-
-       public function testSeekWrite() {
-               $ss = new StringStream;
-               $this->assertSame( '', (string)$ss );
-               $ss->write( 'a' );
-               $this->assertSame( 'a', (string)$ss );
-               $ss->write( 'b' );
-               $this->assertSame( 'ab', (string)$ss );
-               $ss->seek( 1 );
-               $ss->write( 'c' );
-               $this->assertSame( 'ac', (string)$ss );
-       }
-
-       /** @dataProvider provideSeekGetContents */
-       public function testSeekGetContents( $input, $offset, $whence, $expected ) {
-               $ss = new StringStream( $input );
-               $ss->seek( 1 );
-               $ss->seek( $offset, $whence );
-               $this->assertSame( $expected, $ss->getContents() );
-       }
-
-       public static function provideSeekRead() {
-               return [
-                       [ 'abcde', 0, SEEK_SET, 1, 'a' ],
-                       [ 'abcde', 0, SEEK_SET, 2, 'ab' ],
-                       [ 'abcde', 4, SEEK_SET, 2, 'e' ],
-                       [ 'abcde', 5, SEEK_SET, 1, '' ],
-                       [ 'abcde', 1, SEEK_CUR, 1, 'c' ],
-                       [ 'abcde', 0, SEEK_END, 1, '' ],
-                       [ 'abcde', -1, SEEK_END, 1, 'e' ],
-               ];
-       }
-
-       /** @dataProvider provideSeekRead */
-       public function testSeekRead( $input, $offset, $whence, $length, $expected ) {
-               $ss = new StringStream( $input );
-               $ss->seek( 1 );
-               $ss->seek( $offset, $whence );
-               $this->assertSame( $expected, $ss->read( $length ) );
-       }
-
-       /** @expectedException \InvalidArgumentException */
-       public function testReadBeyondEnd() {
-               $ss = new StringStream( 'abc' );
-               $ss->seek( 1, SEEK_END );
-       }
-
-       /** @expectedException \InvalidArgumentException */
-       public function testReadBeforeStart() {
-               $ss = new StringStream( 'abc' );
-               $ss->seek( -1 );
-       }
-}
diff --git a/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 898a35f..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\FallbackSlotRoleHandler;
-use MediaWikiTestCase;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\FallbackSlotRoleHandler
- */
-class FallbackSlotRoleHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @return Title
-        */
-       private function makeBlankTitleObject() {
-               return $this->createMock( Title::class );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getDefaultModel()
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new FallbackSlotRoleHandler( 'foo' );
-               $this->assertSame( 'foo', $handler->getRole() );
-               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title ) );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
-
-               // For the fallback handler, no models are allowed
-               $title = $this->makeBlankTitleObject();
-               $this->assertFalse( $handler->isAllowedModel( 'FooModel', $title ) );
-               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedOn() {
-               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertFalse( $handler->isAllowedOn( $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
-
-               $this->assertFalse( $handler->supportsArticleCount() );
-       }
-
-}
index d57625b..d1418c2 100644 (file)
@@ -6,7 +6,6 @@ use CommentStoreComment;
 use Content;
 use Language;
 use LogicException;
-use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\MainSlotRoleHandler;
 use MediaWiki\Revision\RevisionRecord;
@@ -20,7 +19,6 @@ use ParserOptions;
 use ParserOutput;
 use PHPUnit\Framework\MockObject\MockObject;
 use Title;
-use User;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\ILoadBalancer;
 use WikitextContent;
@@ -30,20 +28,6 @@ use WikitextContent;
  */
 class RevisionRendererTest extends MediaWikiTestCase {
 
-       /** @var PermissionManager|\PHPUnit_Framework_MockObject_MockObject $permissionManagerMock */
-       private $permissionManagerMock;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->permissionManagerMock = $this->createMock( PermissionManager::class );
-               $this->overrideMwServices( null, [
-                       'PermissionManager' => function (): PermissionManager {
-                               return $this->permissionManagerMock;
-                       }
-               ] );
-       }
-
        /**
         * @param int $articleId
         * @param int $revisionId
@@ -88,13 +72,9 @@ class RevisionRendererTest extends MediaWikiTestCase {
                                        return $mock->getArticleID() === $other->getArticleID();
                                }
                        );
-               $this->permissionManagerMock->expects( $this->any() )
-                       ->method( 'userCan' )
-                       ->willReturnCallback(
-                               function ( $perm, User $user ) {
-                                       return $user->isAllowed( $perm );
-                               }
-                       );
+               $mock->expects( $this->any() )
+                       ->method( 'getRestrictions' )
+                       ->willReturn( [] );
 
                return $mock;
        }
@@ -383,6 +363,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
                $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
 
+               $this->assertNotNull( $rr, 'getRenderedRevision' );
                $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
 
                $this->assertSame( $rev, $rr->getRevision() );
index 7b017ab..033e2fe 100644 (file)
@@ -1753,8 +1753,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
                $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-               $blobStore = new SqlBlobStore( $lb, $cache );
+               $services = MediaWikiServices::getInstance();
+               $lb = $services->getDBLoadBalancer();
+               $access = $services->getExternalStoreAccess();
+               $blobStore = new SqlBlobStore( $lb, $access, $cache );
                $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
 
                $factory = $this->getMockBuilder( BlobStoreFactory::class )
index 4f06ee2..84c815d 100644 (file)
@@ -55,7 +55,7 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore
         */
        public function testGetRevisionStore(
-               $wikiId,
+               $dbDomain,
                $mcrMigrationStage = MIGRATION_OLD,
                $contentHandlerUseDb = true
        ) {
@@ -81,14 +81,14 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
                        $contentHandlerUseDb
                );
 
-               $store = $factory->getRevisionStore( $wikiId );
+               $store = $factory->getRevisionStore( $dbDomain );
                $wrapper = TestingAccessWrapper::newFromObject( $store );
 
                // ensure the correct object type is returned
                $this->assertInstanceOf( RevisionStore::class, $store );
 
                // ensure the RevisionStore is for the given wikiId
-               $this->assertSame( $wikiId, $wrapper->wikiId );
+               $this->assertSame( $dbDomain, $wrapper->dbDomain );
 
                // ensure all other required services are correctly set
                $this->assertSame( $cache, $wrapper->cache );
index a8c8581..0648bfc 100644 (file)
@@ -450,9 +450,12 @@ class RevisionStoreTest extends MediaWikiTestCase {
                }
 
                $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               $services = MediaWikiServices::getInstance();
+               $lb = $services->getDBLoadBalancer();
+               $access = $services->getExternalStoreAccess();
+
+               $blobStore = new SqlBlobStore( $lb, $access, $cache );
 
-               $blobStore = new SqlBlobStore( $lb, $cache );
                $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
 
                $store = $this->getRevisionStore( $lb, $blobStore, $cache );
@@ -480,9 +483,11 @@ class RevisionStoreTest extends MediaWikiTestCase {
                ];
 
                $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               $services = MediaWikiServices::getInstance();
+               $lb = $services->getDBLoadBalancer();
+               $access = $services->getExternalStoreAccess();
 
-               $blobStore = new SqlBlobStore( $lb, $cache );
+               $blobStore = new SqlBlobStore( $lb, $access, $cache );
                $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
 
                $store = $this->getRevisionStore( $lb, $blobStore, $cache );
diff --git a/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 372a879..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\SlotRoleHandler;
-use MediaWikiTestCase;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\SlotRoleHandler
- */
-class SlotRoleHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @return Title
-        */
-       private function makeBlankTitleObject() {
-               return $this->createMock( Title::class );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getDefaultModel()
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ] );
-               $this->assertSame( 'foo', $handler->getRole() );
-               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'frob', $hints );
-               $this->assertSame( 'niz', $hints['frob'] );
-
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertTrue( $handler->isAllowedModel( 'FooModel', $title ) );
-               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
-
-               $this->assertFalse( $handler->supportsArticleCount() );
-       }
-
-}
index bbd034a..2d141e6 100644 (file)
@@ -1539,8 +1539,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
        public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
                $title = Title::newFromText( $title );
 
-               $this->setMwGlobals(
-                       'wgGroupPermissions',
+               $this->setGroupPermissions(
                        [
                                'sysop' => [
                                        'deletedtext' => true,
@@ -1592,8 +1591,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
         * @covers Revision::userCan
         */
        public function testUserCan( $bitField, $field, $userGroups, $expected ) {
-               $this->setMwGlobals(
-                       'wgGroupPermissions',
+               $this->setGroupPermissions(
                        [
                                'sysop' => [
                                        'deletedtext' => true,
index 98f2980..d62e4c7 100644 (file)
@@ -438,10 +438,11 @@ class RevisionTest extends MediaWikiTestCase {
                $lb = $this->getMockBuilder( LoadBalancer::class )
                        ->disableOriginalConstructor()
                        ->getMock();
-
+               $access = MediaWikiServices::getInstance()->getExternalStoreAccess();
                $cache = $this->getWANObjectCache();
 
-               $blobStore = new SqlBlobStore( $lb, $cache );
+               $blobStore = new SqlBlobStore( $lb, $access, $cache );
+
                return $blobStore;
        }
 
@@ -807,7 +808,7 @@ class RevisionTest extends MediaWikiTestCase {
        public function testGetRevisionText_external_noOldId() {
                $this->setService(
                        'ExternalStoreFactory',
-                       new ExternalStoreFactory( [ 'ForTesting' ] )
+                       new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' )
                );
                $this->assertSame(
                        'AAAABBAAA',
@@ -829,14 +830,15 @@ class RevisionTest extends MediaWikiTestCase {
 
                $this->setService(
                        'ExternalStoreFactory',
-                       new ExternalStoreFactory( [ 'ForTesting' ] )
+                       new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' )
                );
 
                $lb = $this->getMockBuilder( LoadBalancer::class )
                        ->disableOriginalConstructor()
                        ->getMock();
+               $access = MediaWikiServices::getInstance()->getExternalStoreAccess();
 
-               $blobStore = new SqlBlobStore( $lb, $cache );
+               $blobStore = new SqlBlobStore( $lb, $access, $cache );
                $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) );
 
                $this->assertSame(
diff --git a/tests/phpunit/includes/ServiceWiringTest.php b/tests/phpunit/includes/ServiceWiringTest.php
deleted file mode 100644 (file)
index 02e06f8..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @coversNothing
- */
-class ServiceWiringTest extends MediaWikiTestCase {
-       public function testServicesAreSorted() {
-               global $IP;
-               $services = array_keys( require "$IP/includes/ServiceWiring.php" );
-               $sortedServices = $services;
-               natcasesort( $sortedServices );
-
-               $this->assertSame( $sortedServices, $services,
-                       'Please keep services sorted alphabetically' );
-       }
-}
diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php
deleted file mode 100644 (file)
index 3b72262..0000000
+++ /dev/null
@@ -1,379 +0,0 @@
-<?php
-
-class SiteConfigurationTest extends MediaWikiTestCase {
-
-       /**
-        * @var SiteConfiguration
-        */
-       protected $mConf;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->mConf = new SiteConfiguration;
-
-               $this->mConf->suffixes = [ 'wikipedia' => 'wiki' ];
-               $this->mConf->wikis = [ 'enwiki', 'dewiki', 'frwiki' ];
-               $this->mConf->settings = [
-                       'SimpleKey' => [
-                               'wiki' => 'wiki',
-                               'tag' => 'tag',
-                               'enwiki' => 'enwiki',
-                               'dewiki' => 'dewiki',
-                               'frwiki' => 'frwiki',
-                       ],
-
-                       'Fallback' => [
-                               'default' => 'default',
-                               'wiki' => 'wiki',
-                               'tag' => 'tag',
-                               'frwiki' => 'frwiki',
-                               'null_wiki' => null,
-                       ],
-
-                       'WithParams' => [
-                               'default' => '$lang $site $wiki',
-                       ],
-
-                       '+SomeGlobal' => [
-                               'wiki' => [
-                                       'wiki' => 'wiki',
-                               ],
-                               'tag' => [
-                                       'tag' => 'tag',
-                               ],
-                               'enwiki' => [
-                                       'enwiki' => 'enwiki',
-                               ],
-                               'dewiki' => [
-                                       'dewiki' => 'dewiki',
-                               ],
-                               'frwiki' => [
-                                       'frwiki' => 'frwiki',
-                               ],
-                       ],
-
-                       'MergeIt' => [
-                               '+wiki' => [
-                                       'wiki' => 'wiki',
-                               ],
-                               '+tag' => [
-                                       'tag' => 'tag',
-                               ],
-                               'default' => [
-                                       'default' => 'default',
-                               ],
-                               '+enwiki' => [
-                                       'enwiki' => 'enwiki',
-                               ],
-                               '+dewiki' => [
-                                       'dewiki' => 'dewiki',
-                               ],
-                               '+frwiki' => [
-                                       'frwiki' => 'frwiki',
-                               ],
-                       ],
-               ];
-
-               $GLOBALS['SomeGlobal'] = [ 'SomeGlobal' => 'SomeGlobal' ];
-       }
-
-       /**
-        * This function is used as a callback within the tests below
-        */
-       public static function getSiteParamsCallback( $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 [
-                       'suffix' => $site,
-                       'lang' => $lang,
-                       'params' => [
-                               'lang' => $lang,
-                               'site' => $site,
-                               'wiki' => $wiki,
-                       ],
-                       'tags' => [ 'tag' ],
-               ];
-       }
-
-       /**
-        * @covers SiteConfiguration::siteFromDB
-        */
-       public function testSiteFromDb() {
-               $this->assertEquals(
-                       [ 'wikipedia', 'en' ],
-                       $this->mConf->siteFromDB( 'enwiki' ),
-                       'siteFromDB()'
-               );
-               $this->assertEquals(
-                       [ 'wikipedia', '' ],
-                       $this->mConf->siteFromDB( 'wiki' ),
-                       'siteFromDB() on a suffix'
-               );
-               $this->assertEquals(
-                       [ null, null ],
-                       $this->mConf->siteFromDB( 'wikien' ),
-                       'siteFromDB() on a non-existing wiki'
-               );
-
-               $this->mConf->suffixes = [ 'wiki', '' ];
-               $this->assertEquals(
-                       [ '', 'wikien' ],
-                       $this->mConf->siteFromDB( 'wikien' ),
-                       'siteFromDB() on a non-existing wiki (2)'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::getLocalDatabases
-        */
-       public function testGetLocalDatabases() {
-               $this->assertEquals(
-                       [ 'enwiki', 'dewiki', 'frwiki' ],
-                       $this->mConf->getLocalDatabases(),
-                       'getLocalDatabases()'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::get
-        */
-       public function testGetConfVariables() {
-               // Simple
-               $this->assertEquals(
-                       'enwiki',
-                       $this->mConf->get( 'SimpleKey', 'enwiki', 'wiki' ),
-                       'get(): simple setting on an existing wiki'
-               );
-               $this->assertEquals(
-                       'dewiki',
-                       $this->mConf->get( 'SimpleKey', 'dewiki', 'wiki' ),
-                       'get(): simple setting on an existing wiki (2)'
-               );
-               $this->assertEquals(
-                       'frwiki',
-                       $this->mConf->get( 'SimpleKey', 'frwiki', 'wiki' ),
-                       'get(): simple setting on an existing wiki (3)'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'SimpleKey', 'wiki', 'wiki' ),
-                       'get(): simple setting on an suffix'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'SimpleKey', 'eswiki', 'wiki' ),
-                       'get(): simple setting on an non-existing wiki'
-               );
-
-               // Fallback
-               $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', [], [ 'tag' ] ),
-                       'get(): fallback setting on an existing wiki (with wiki tag)'
-               );
-               $this->assertEquals(
-                       'frwiki',
-                       $this->mConf->get( 'Fallback', 'frwiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): no fallback if wiki has its own setting (matching tag)'
-               );
-               $this->assertSame(
-                       // Potential regression test for T192855
-                       null,
-                       $this->mConf->get( 'Fallback', 'null_wiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): no fallback if wiki has its own setting (matching tag and uses null)'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'Fallback', 'wiki', 'wiki' ),
-                       'get(): fallback setting on an suffix'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'Fallback', 'wiki', 'wiki', [], [ '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', [], [ 'tag' ] ),
-                       'get(): fallback setting on an non-existing wiki (with wiki tag)'
-               );
-
-               // Merging
-               $common = [ 'wiki' => 'wiki', 'default' => 'default' ];
-               $commonTag = [ 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ];
-               $this->assertEquals(
-                       [ 'enwiki' => 'enwiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki' ),
-                       'get(): merging setting on an existing wiki'
-               );
-               $this->assertEquals(
-                       [ 'enwiki' => 'enwiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an existing wiki (with tag)'
-               );
-               $this->assertEquals(
-                       [ 'dewiki' => 'dewiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki' ),
-                       'get(): merging setting on an existing wiki (2)'
-               );
-               $this->assertEquals(
-                       [ 'dewiki' => 'dewiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an existing wiki (2) (with tag)'
-               );
-               $this->assertEquals(
-                       [ 'frwiki' => 'frwiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki' ),
-                       'get(): merging setting on an existing wiki (3)'
-               );
-               $this->assertEquals(
-                       [ 'frwiki' => 'frwiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an existing wiki (3) (with tag)'
-               );
-               $this->assertEquals(
-                       [ 'wiki' => 'wiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki' ),
-                       'get(): merging setting on an suffix'
-               );
-               $this->assertEquals(
-                       [ 'wiki' => 'wiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an suffix (with tag)'
-               );
-               $this->assertEquals(
-                       $common,
-                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki' ),
-                       'get(): merging setting on an non-existing wiki'
-               );
-               $this->assertEquals(
-                       $commonTag,
-                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an non-existing wiki (with tag)'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::siteFromDB
-        */
-       public function testSiteFromDbWithCallback() {
-               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
-
-               $this->assertEquals(
-                       [ 'wiki', 'en' ],
-                       $this->mConf->siteFromDB( 'enwiki' ),
-                       'siteFromDB() with callback'
-               );
-               $this->assertEquals(
-                       [ 'wiki', '' ],
-                       $this->mConf->siteFromDB( 'wiki' ),
-                       'siteFromDB() with callback on a suffix'
-               );
-               $this->assertEquals(
-                       [ null, null ],
-                       $this->mConf->siteFromDB( 'wikien' ),
-                       'siteFromDB() with callback on a non-existing wiki'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::get
-        */
-       public function testParameterReplacement() {
-               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
-
-               $this->assertEquals(
-                       'en wiki enwiki',
-                       $this->mConf->get( 'WithParams', 'enwiki', 'wiki' ),
-                       'get(): parameter replacement on an existing wiki'
-               );
-               $this->assertEquals(
-                       'de wiki dewiki',
-                       $this->mConf->get( 'WithParams', 'dewiki', 'wiki' ),
-                       'get(): parameter replacement on an existing wiki (2)'
-               );
-               $this->assertEquals(
-                       'fr wiki frwiki',
-                       $this->mConf->get( 'WithParams', 'frwiki', 'wiki' ),
-                       'get(): parameter replacement on an existing wiki (3)'
-               );
-               $this->assertEquals(
-                       ' wiki wiki',
-                       $this->mConf->get( 'WithParams', 'wiki', 'wiki' ),
-                       'get(): parameter replacement on an suffix'
-               );
-               $this->assertEquals(
-                       'es wiki eswiki',
-                       $this->mConf->get( 'WithParams', 'eswiki', 'wiki' ),
-                       'get(): parameter replacement on an non-existing wiki'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::getAll
-        */
-       public function testGetAllGlobals() {
-               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
-
-               $getall = [
-                       'SimpleKey' => 'enwiki',
-                       'Fallback' => 'tag',
-                       'WithParams' => 'en wiki enwiki',
-                       'SomeGlobal' => [ 'enwiki' => 'enwiki' ] + $GLOBALS['SomeGlobal'],
-                       'MergeIt' => [
-                               'enwiki' => 'enwiki',
-                               'tag' => 'tag',
-                               'wiki' => 'wiki',
-                               'default' => 'default'
-                       ],
-               ];
-               $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' );
-
-               $this->mConf->extractAllGlobals( 'enwiki', 'wiki' );
-
-               $this->assertEquals(
-                       $getall['SimpleKey'],
-                       $GLOBALS['SimpleKey'],
-                       'extractAllGlobals(): simple setting'
-               );
-               $this->assertEquals(
-                       $getall['Fallback'],
-                       $GLOBALS['Fallback'],
-                       'extractAllGlobals(): fallback setting'
-               );
-               $this->assertEquals(
-                       $getall['WithParams'],
-                       $GLOBALS['WithParams'],
-                       'extractAllGlobals(): parameter replacement'
-               );
-               $this->assertEquals(
-                       $getall['SomeGlobal'],
-                       $GLOBALS['SomeGlobal'],
-                       'extractAllGlobals(): merging with global'
-               );
-               $this->assertEquals(
-                       $getall['MergeIt'],
-                       $GLOBALS['MergeIt'],
-                       'extractAllGlobals(): merging setting'
-               );
-       }
-}
diff --git a/tests/phpunit/includes/Storage/PreparedEditTest.php b/tests/phpunit/includes/Storage/PreparedEditTest.php
deleted file mode 100644 (file)
index 29999ee..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace MediaWiki\Edit;
-
-use ParserOutput;
-use MediaWikiTestCase;
-
-/**
- * @covers \MediaWiki\Edit\PreparedEdit
- */
-class PreparedEditTest extends MediaWikiTestCase {
-       function testCallback() {
-               $output = new ParserOutput();
-               $edit = new PreparedEdit();
-               $edit->parserOutputCallback = function () {
-                       return new ParserOutput();
-               };
-
-               $this->assertEquals( $output, $edit->getOutput() );
-               $this->assertEquals( $output, $edit->output );
-       }
-}
index 5506940..ac39b48 100644 (file)
@@ -24,6 +24,7 @@ class SqlBlobStoreTest extends MediaWikiTestCase {
 
                $store = new SqlBlobStore(
                        $services->getDBLoadBalancer(),
+                       $services->getExternalStoreAccess(),
                        $services->getMainWANObjectCache()
                );
 
index ebd8dbd..04addab 100644 (file)
@@ -15,7 +15,7 @@ class TemplateCategoriesTest extends MediaWikiLangTestCase {
         */
        public function testTemplateCategories() {
                $user = new User();
-               $user->mRights = [ 'createpage', 'edit', 'purge', 'delete' ];
+               $this->overrideUserPermissions( $user, [ 'createpage', 'edit', 'purge', 'delete' ] );
 
                $title = Title::newFromText( "Categorized from template" );
                $page = WikiPage::factory( $title );
index e09546e..150e2ed 100644 (file)
@@ -72,17 +72,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
 
                        $this->user = $this->userUser;
                }
-               $this->overrideMwServices();
-       }
-
-       protected function setUserPerm( $perm ) {
-               // Setting member variables is evil!!!
-
-               if ( is_array( $perm ) ) {
-                       $this->user->mRights = $perm;
-               } else {
-                       $this->user->mRights = [ $perm ];
-               }
+               $this->resetServices();
        }
 
        protected function setTitle( $ns, $title = "Main_Page" ) {
@@ -114,139 +104,139 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
 
                $this->setUser( 'anon' );
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createtalk" );
+               $this->overrideUserPermissions( $this->user, "createtalk" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createpage" );
+               $this->overrideUserPermissions( $this->user, "createpage" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ "nocreatetext" ] ], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user, "" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createpage" );
+               $this->overrideUserPermissions( $this->user, "createpage" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createtalk" );
+               $this->overrideUserPermissions( $this->user, "createtalk" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
 
                $this->setUser( $this->userName );
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createtalk" );
+               $this->overrideUserPermissions( $this->user, "createtalk" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "createpage" );
+               $this->overrideUserPermissions( $this->user, "createpage" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setTitle( NS_TALK );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createpage" );
+               $this->overrideUserPermissions( $this->user, "createpage" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "createtalk" );
+               $this->overrideUserPermissions( $this->user, "createtalk" );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
                $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
 
                $this->setUser( 'anon' );
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "move-rootuserpages" );
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "move-rootuserpages" );
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user, "" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user, "" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '' );
-               $this->setUserPerm( "move-rootuserpages" );
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_USER, $this->userName . '/subpage' );
-               $this->setUserPerm( "move-rootuserpages" );
+               $this->overrideUserPermissions( $this->user, "move-rootuserpages" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setUser( $this->userName );
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ], $res );
 
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "movefile" );
+               $this->overrideUserPermissions( $this->user, "movefile" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
 
                $this->setUser( 'anon' );
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ], $res );
 
                $this->setTitle( NS_FILE, "img.png" );
-               $this->setUserPerm( "movefile" );
+               $this->overrideUserPermissions( $this->user, "movefile" );
                $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
                $this->assertEquals( [ [ 'movenologintext' ] ], $res );
 
                $this->setUser( $this->userName );
-               $this->setUserPerm( "move" );
+               $this->overrideUserPermissions( $this->user, "move" );
                $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
 
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $this->runGroupPermissions(
                        'move',
                        [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ]
                );
 
                $this->setUser( 'anon' );
-               $this->setUserPerm( "move" );
+               $this->overrideUserPermissions( $this->user, "move" );
                $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
 
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $this->runGroupPermissions(
                        'move',
                        [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ],
@@ -259,51 +249,51 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
 
                        $this->setTitle( NS_MAIN );
                        $this->setUser( 'anon' );
-                       $this->setUserPerm( "move" );
+                       $this->overrideUserPermissions( $this->user, "move" );
                        $this->runGroupPermissions( 'move', [] );
 
-                       $this->setUserPerm( "" );
+                       $this->overrideUserPermissions( $this->user, "" );
                        $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ],
                                [ [ 'movenologintext' ] ] );
 
                        $this->setUser( $this->userName );
-                       $this->setUserPerm( "" );
+                       $this->overrideUserPermissions( $this->user, "" );
                        $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ] );
 
-                       $this->setUserPerm( "move" );
+                       $this->overrideUserPermissions( $this->user, "move" );
                        $this->runGroupPermissions( 'move', [] );
 
                        $this->setUser( 'anon' );
-                       $this->setUserPerm( 'move' );
+                       $this->overrideUserPermissions( $this->user, 'move' );
                        $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                        $this->assertEquals( [], $res );
 
-                       $this->setUserPerm( '' );
+                       $this->overrideUserPermissions( $this->user );
                        $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                        $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
                }
 
                $this->setTitle( NS_USER );
                $this->setUser( $this->userName );
-               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+               $this->overrideUserPermissions( $this->user, [ "move", "move-rootuserpages" ] );
                $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                $this->assertEquals( [], $res );
 
-               $this->setUserPerm( "move" );
+               $this->overrideUserPermissions( $this->user, "move" );
                $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                $this->assertEquals( [ [ 'cant-move-to-user-page' ] ], $res );
 
                $this->setUser( 'anon' );
-               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+               $this->overrideUserPermissions( $this->user, [ "move", "move-rootuserpages" ] );
                $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                $this->assertEquals( [], $res );
 
                $this->setTitle( NS_USER, "User/subpage" );
-               $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+               $this->overrideUserPermissions( $this->user, [ "move", "move-rootuserpages" ] );
                $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                $this->assertEquals( [], $res );
 
-               $this->setUserPerm( "move" );
+               $this->overrideUserPermissions( $this->user, "move" );
                $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
                $this->assertEquals( [], $res );
 
@@ -329,7 +319,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                ];
 
                foreach ( [ "edit", "protect", "" ] as $action ) {
-                       $this->setUserPerm( null );
+                       $this->overrideUserPermissions( $this->user );
                        $this->assertEquals( $check[$action][0],
                                $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
                        $this->assertEquals( $check[$action][0],
@@ -341,15 +331,19 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $old = $wgGroupPermissions;
                        $wgGroupPermissions = [];
 
+                       $this->resetServices();
+
                        $this->assertEquals( $check[$action][1],
                                $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
                        $this->assertEquals( $check[$action][1],
                                $this->title->getUserPermissionsErrors( $action, $this->user, 'full' ) );
                        $this->assertEquals( $check[$action][1],
                                $this->title->getUserPermissionsErrors( $action, $this->user, 'secure' ) );
+
                        $wgGroupPermissions = $old;
+                       $this->resetServices();
 
-                       $this->setUserPerm( $action );
+                       $this->overrideUserPermissions( $this->user, $action );
                        $this->assertEquals( $check[$action][2],
                                $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
                        $this->assertEquals( $check[$action][2],
@@ -357,7 +351,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $this->assertEquals( $check[$action][2],
                                $this->title->getUserPermissionsErrors( $action, $this->user, 'secure' ) );
 
-                       $this->setUserPerm( $action );
+                       $this->overrideUserPermissions( $this->user, $action );
                        $this->assertEquals( $check[$action][3],
                                $this->title->userCan( $action, $this->user, true ) );
                        $this->assertEquals( $check[$action][3],
@@ -373,23 +367,39 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $result2 = $result;
                }
 
+               // XXX: there could be a better way to handle this, but since we need to
+               // override PermissionManager service each time globals are changed
+               // and in the same time we need to keep user permissions overrides from the outside
+               // the best we can do inside this method is to save & restore faked user perms
+
+               $userPermsOverrides = MediaWikiServices::getInstance()->getPermissionManager()
+                       ->getUserPermissions( $this->user );
+
                $wgGroupPermissions['autoconfirmed']['move'] = false;
                $wgGroupPermissions['user']['move'] = false;
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $userPermsOverrides );
                $res = $this->title->getUserPermissionsErrors( $action, $this->user );
                $this->assertEquals( $result, $res );
 
                $wgGroupPermissions['autoconfirmed']['move'] = true;
                $wgGroupPermissions['user']['move'] = false;
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $userPermsOverrides );
                $res = $this->title->getUserPermissionsErrors( $action, $this->user );
                $this->assertEquals( $result2, $res );
 
                $wgGroupPermissions['autoconfirmed']['move'] = true;
                $wgGroupPermissions['user']['move'] = true;
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $userPermsOverrides );
                $res = $this->title->getUserPermissionsErrors( $action, $this->user );
                $this->assertEquals( $result2, $res );
 
                $wgGroupPermissions['autoconfirmed']['move'] = false;
                $wgGroupPermissions['user']['move'] = true;
+               $this->resetServices();
+               $this->overrideUserPermissions( $this->user, $userPermsOverrides );
                $res = $this->title->getUserPermissionsErrors( $action, $this->user );
                $this->assertEquals( $result2, $res );
        }
@@ -409,42 +419,42 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
 
                $this->setTitle( NS_MAIN );
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user );
                $this->assertEquals( [ [ 'badaccess-group0' ] ],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
 
                $wgNamespaceProtection[NS_USER] = [ 'bogus' ];
 
                $this->setTitle( NS_USER );
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user );
                $this->assertEquals( [ [ 'badaccess-group0' ],
                                [ 'namespaceprotected', 'User', 'bogus' ] ],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
 
                $this->setTitle( NS_MEDIAWIKI );
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
 
                $this->setTitle( NS_MEDIAWIKI );
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
 
                $wgNamespaceProtection = null;
 
-               $this->setUserPerm( 'bogus' );
+               $this->overrideUserPermissions( $this->user, 'bogus' );
                $this->assertEquals( [],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
                $this->assertEquals( true,
                        $this->title->userCan( 'bogus', $this->user ) );
 
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user );
                $this->assertEquals( [ [ 'badaccess-group0' ] ],
                        $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
                $this->assertEquals( false,
@@ -645,39 +655,39 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                $resultUserJs,
                $resultPatrol
        ) {
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultNone, $result );
 
-               $this->setUserPerm( 'editmyusercss' );
+               $this->overrideUserPermissions( $this->user, 'editmyusercss' );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultMyCss, $result );
 
-               $this->setUserPerm( 'editmyuserjson' );
+               $this->overrideUserPermissions( $this->user, 'editmyuserjson' );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultMyJson, $result );
 
-               $this->setUserPerm( 'editmyuserjs' );
+               $this->overrideUserPermissions( $this->user, 'editmyuserjs' );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultMyJs, $result );
 
-               $this->setUserPerm( 'editusercss' );
+               $this->overrideUserPermissions( $this->user, 'editusercss' );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultUserCss, $result );
 
-               $this->setUserPerm( 'edituserjson' );
+               $this->overrideUserPermissions( $this->user, 'edituserjson' );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultUserJson, $result );
 
-               $this->setUserPerm( 'edituserjs' );
+               $this->overrideUserPermissions( $this->user, 'edituserjs' );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( $resultUserJs, $result );
 
-               $this->setUserPerm( '' );
+               $this->overrideUserPermissions( $this->user );
                $result = $this->title->getUserPermissionsErrors( 'patrol', $this->user );
                $this->assertEquals( reset( $resultPatrol[0] ), reset( $result[0] ) );
 
-               $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] );
+               $this->overrideUserPermissions( $this->user, [ 'edituserjs', 'edituserjson', 'editusercss' ] );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
                $this->assertEquals( [ [ 'badaccess-group0' ] ], $result );
        }
@@ -697,7 +707,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
 
                $this->setTitle( NS_MAIN );
                $this->title->mRestrictionsLoaded = true;
-               $this->setUserPerm( "edit" );
+               $this->overrideUserPermissions( $this->user, "edit" );
                $this->title->mRestrictions = [ "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
 
                $this->assertEquals( [],
@@ -720,7 +730,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                                [ 'protectedpagetext', 'protect', 'edit' ] ],
                        $this->title->getUserPermissionsErrors( 'edit',
                                $this->user ) );
-               $this->setUserPerm( "" );
+               $this->overrideUserPermissions( $this->user );
                $this->assertEquals( [ [ 'badaccess-group0' ],
                                [ 'protectedpagetext', 'bogus', 'bogus' ],
                                [ 'protectedpagetext', 'editprotected', 'bogus' ],
@@ -733,7 +743,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                                [ 'protectedpagetext', 'protect', 'edit' ] ],
                        $this->title->getUserPermissionsErrors( 'edit',
                                $this->user ) );
-               $this->setUserPerm( [ "edit", "editprotected" ] );
+               $this->overrideUserPermissions( $this->user, [ "edit", "editprotected" ] );
                $this->assertEquals( [ [ 'badaccess-group0' ],
                                [ 'protectedpagetext', 'bogus', 'bogus' ],
                                [ 'protectedpagetext', 'protect', 'bogus' ] ],
@@ -746,7 +756,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                                $this->user ) );
 
                $this->title->mCascadeRestriction = true;
-               $this->setUserPerm( "edit" );
+               $this->overrideUserPermissions( $this->user, "edit" );
                $this->assertEquals( false,
                        $this->title->quickUserCan( 'bogus', $this->user ) );
                $this->assertEquals( false,
@@ -763,7 +773,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $this->title->getUserPermissionsErrors( 'edit',
                                $this->user ) );
 
-               $this->setUserPerm( [ "edit", "editprotected" ] );
+               $this->overrideUserPermissions( $this->user, [ "edit", "editprotected" ] );
                $this->assertEquals( false,
                        $this->title->quickUserCan( 'bogus', $this->user ) );
                $this->assertEquals( false,
@@ -786,7 +796,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         */
        public function testCascadingSourcesRestrictions() {
                $this->setTitle( NS_MAIN, "test page" );
-               $this->setUserPerm( [ "edit", "bogus" ] );
+               $this->overrideUserPermissions( $this->user, [ "edit", "bogus" ] );
 
                $this->title->mCascadeSources = [
                        Title::makeTitle( NS_MAIN, "Bogus" ),
@@ -816,7 +826,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
         */
        public function testActionPermissions() {
-               $this->setUserPerm( [ "createpage" ] );
+               $this->overrideUserPermissions( $this->user, [ "createpage" ] );
                $this->setTitle( NS_MAIN, "test page" );
                $this->title->mTitleProtection['permission'] = '';
                $this->title->mTitleProtection['user'] = $this->user->getId();
@@ -830,26 +840,26 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $this->title->userCan( 'create', $this->user ) );
 
                $this->title->mTitleProtection['permission'] = 'editprotected';
-               $this->setUserPerm( [ 'createpage', 'protect' ] );
+               $this->overrideUserPermissions( $this->user, [ 'createpage', 'protect' ] );
                $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
                        $this->title->getUserPermissionsErrors( 'create', $this->user ) );
                $this->assertEquals( false,
                        $this->title->userCan( 'create', $this->user ) );
 
-               $this->setUserPerm( [ 'createpage', 'editprotected' ] );
+               $this->overrideUserPermissions( $this->user, [ 'createpage', 'editprotected' ] );
                $this->assertEquals( [],
                        $this->title->getUserPermissionsErrors( 'create', $this->user ) );
                $this->assertEquals( true,
                        $this->title->userCan( 'create', $this->user ) );
 
-               $this->setUserPerm( [ 'createpage' ] );
+               $this->overrideUserPermissions( $this->user, [ 'createpage' ] );
                $this->assertEquals( [ [ '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( [ "move" ] );
+               $this->overrideUserPermissions( $this->user, [ "move" ] );
                $this->assertEquals( false,
                        $this->title->userCan( 'move', $this->user ) );
                $this->assertEquals( [ [ 'immobile-source-namespace', 'Media' ] ],
@@ -895,9 +905,12 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        'wgEmailAuthentication' => true,
                        'wgBlockDisablesLogin' => false,
                ] );
-               $this->overrideMwServices();
+               $this->resetServices();
 
-               $this->setUserPerm( [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ] );
+               $this->overrideUserPermissions(
+                       $this->user,
+                       [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ]
+               );
                $this->setTitle( NS_HELP, "test page" );
 
                # $wgEmailConfirmToEdit only applies to 'edit' action
@@ -907,7 +920,11 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
 
                $this->setMwGlobals( 'wgEmailConfirmToEdit', false );
-               $this->overrideMwServices();
+               $this->resetServices();
+               $this->overrideUserPermissions(
+                       $this->user,
+                       [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ]
+               );
 
                $this->assertNotContains( [ 'confirmedittext' ],
                        $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
@@ -1071,6 +1088,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                                ],
                        ],
                ] );
+               $this->resetServices();
 
                $now = time();
                $this->user->mBlockedby = $this->user->getName();
index 529d9fb..225a786 100644 (file)
@@ -355,7 +355,7 @@ class TitleTest extends MediaWikiTestCase {
 
                // New anonymous user with no rights
                $user = new User;
-               $user->mRights = [];
+               $this->overrideUserPermissions( $user, [] );
                $errors = $title->userCan( $action, $user );
 
                if ( is_bool( $expected ) ) {
diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php
deleted file mode 100644 (file)
index 52e20bd..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-<?php
-
-/**
- * @group Xml
- */
-class XmlSelectTest extends MediaWikiTestCase {
-
-       /**
-        * @var XmlSelect
-        */
-       protected $select;
-
-       protected function setUp() {
-               parent::setUp();
-               $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 [
-                       /**
-                        * Values are set following a 3-bit Gray code where two successive
-                        * values differ by only one value.
-                        * See https://en.wikipedia.org/wiki/Gray_code
-                        */
-                       #      $name   $id    $default
-                       [ false, false, false, '<select></select>' ],
-                       [ false, false, 'foo', '<select></select>' ],
-                       [ false, 'id', 'foo', '<select id="id"></select>' ],
-                       [ false, 'id', false, '<select id="id"></select>' ],
-                       [ 'name', 'id', false, '<select name="name" id="id"></select>' ],
-                       [ 'name', 'id', 'foo', '<select name="name" id="id"></select>' ],
-                       [ 'name', false, 'foo', '<select name="name"></select>' ],
-                       [ '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'
-               );
-
-               # inexistent 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
-               );
-       }
-}
index 5ad7736..4d977cb 100644 (file)
@@ -190,14 +190,14 @@ class ActionTest extends MediaWikiTestCase {
 
        public function testCanExecute() {
                $user = $this->getTestUser()->getUser();
-               $user->mRights = [ 'access' ];
+               $this->overrideUserPermissions( $user, 'access' );
                $action = Action::factory( 'access', $this->getPage(), $this->getContext() );
                $this->assertNull( $action->canExecute( $user ) );
        }
 
        public function testCanExecuteNoRight() {
                $user = $this->getTestUser()->getUser();
-               $user->mRights = [];
+               $this->overrideUserPermissions( $user, [] );
                $action = Action::factory( 'access', $this->getPage(), $this->getContext() );
 
                try {
@@ -209,7 +209,7 @@ class ActionTest extends MediaWikiTestCase {
 
        public function testCanExecuteRequiresUnblock() {
                $user = $this->getTestUser()->getUser();
-               $user->mRights = [];
+               $this->overrideUserPermissions( $user, [] );
 
                $page = $this->getExistingTestPage();
                $action = Action::factory( 'unblock', $page, $this->getContext() );
index 43da9a9..b29d333 100644 (file)
@@ -150,6 +150,8 @@ class ApiBlockTest extends ApiTestCase {
                $this->setMwGlobals( 'wgRevokePermissions',
                        [ 'user' => [ 'applychangetags' => true ] ] );
 
+               $this->resetServices();
+
                $this->doBlock( [ 'tags' => 'custom tag' ] );
        }
 
@@ -160,6 +162,7 @@ class ApiBlockTest extends ApiTestCase {
                $this->mergeMwGlobalArrayValue( 'wgGroupPermissions',
                        [ 'sysop' => $newPermissions ] );
 
+               $this->resetServices();
                $res = $this->doBlock( [ 'hidename' => '' ] );
 
                $dbw = wfGetDB( DB_MASTER );
@@ -209,6 +212,8 @@ class ApiBlockTest extends ApiTestCase {
                $this->setMwGlobals( 'wgRevokePermissions',
                        [ 'sysop' => [ 'blockemail' => true ] ] );
 
+               $this->resetServices();
+
                $this->doBlock( [ 'noemail' => '' ] );
        }
 
index c68954c..cc5dada 100644 (file)
@@ -143,6 +143,7 @@ class ApiDeleteTest extends ApiTestCase {
                ChangeTags::defineTag( 'custom tag' );
                $this->setMwGlobals( 'wgRevokePermissions',
                        [ 'user' => [ 'applychangetags' => true ] ] );
+               $this->resetServices();
 
                $this->editPage( $name, 'Some text' );
 
index d2762e0..3badd28 100644 (file)
@@ -39,6 +39,7 @@ class ApiEditPageTest extends ApiTestCase {
                        $this->tablesUsed,
                        [ 'change_tag', 'change_tag_def', 'logging' ]
                );
+               $this->resetServices();
        }
 
        public function testEdit() {
@@ -1367,6 +1368,9 @@ class ApiEditPageTest extends ApiTestCase {
                ChangeTags::defineTag( 'custom tag' );
                $this->setMwGlobals( 'wgRevokePermissions',
                        [ 'user' => [ 'applychangetags' => true ] ] );
+               // Supply services with updated globals
+               $this->resetServices();
+
                try {
                        $this->doApiRequestWithToken( [
                                'action' => 'edit',
@@ -1545,6 +1549,8 @@ class ApiEditPageTest extends ApiTestCase {
 
                $this->setMwGlobals( 'wgRevokePermissions',
                        [ 'user' => [ 'upload' => true ] ] );
+               // Supply services with updated globals
+               $this->resetServices();
 
                $this->doApiRequestWithToken( [
                        'action' => 'edit',
@@ -1560,6 +1566,8 @@ class ApiEditPageTest extends ApiTestCase {
                        'The content you supplied exceeds the article size limit of 1 kilobyte.' );
 
                $this->setMwGlobals( 'wgMaxArticleSize', 1 );
+               // Supply services with updated globals
+               $this->resetServices();
 
                $text = str_repeat( '!', 1025 );
 
@@ -1577,6 +1585,8 @@ class ApiEditPageTest extends ApiTestCase {
                        'The action you have requested is limited to users in the group: ' );
 
                $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
+               // Supply services with updated globals
+               $this->resetServices();
 
                $this->doApiRequestWithToken( [
                        'action' => 'edit',
@@ -1593,6 +1603,8 @@ class ApiEditPageTest extends ApiTestCase {
 
                $this->setMwGlobals( 'wgRevokePermissions',
                        [ 'user' => [ 'editcontentmodel' => true ] ] );
+               // Supply services with updated globals
+               $this->resetServices();
 
                $this->doApiRequestWithToken( [
                        'action' => 'edit',
index a5518a1..580efcd 100644 (file)
@@ -141,6 +141,7 @@ class ApiMainTest extends ApiTestCase {
        public function testSetCacheModeUnrecognized() {
                $api = new ApiMain();
                $api->setCacheMode( 'unrecognized' );
+               $this->resetServices();
                $this->assertSame(
                        'private',
                        TestingAccessWrapper::newFromObject( $api )->mCacheMode,
@@ -150,7 +151,6 @@ class ApiMainTest extends ApiTestCase {
 
        public function testSetCacheModePrivateWiki() {
                $this->setGroupPermissions( '*', 'read', false );
-
                $wrappedApi = TestingAccessWrapper::newFromObject( new ApiMain() );
                $wrappedApi->setCacheMode( 'public' );
                $this->assertSame( 'private', $wrappedApi->mCacheMode );
@@ -401,7 +401,7 @@ class ApiMainTest extends ApiTestCase {
                } else {
                        $user = new User();
                }
-               $user->mRights = $rights;
+               $this->overrideUserPermissions( $user, $rights );
                try {
                        $this->doApiRequest( [
                                'action' => 'query',
index d880923..c98308c 100644 (file)
@@ -294,6 +294,7 @@ class ApiMoveTest extends ApiTestCase {
                $name = ucfirst( __FUNCTION__ );
 
                $this->mergeMwGlobalArrayValue( 'wgNamespacesWithSubpages', [ NS_MAIN => true ] );
+               $this->resetServices();
 
                $pages = [ $name, "$name/1", "$name/2", "Talk:$name", "Talk:$name/1", "Talk:$name/3" ];
                $ids = [];
@@ -379,7 +380,6 @@ class ApiMoveTest extends ApiTestCase {
                $name = ucfirst( __FUNCTION__ );
 
                $this->setGroupPermissions( 'sysop', 'suppressredirect', false );
-
                $id = $this->createPage( $name );
 
                $res = $this->doApiRequestWithToken( [
index 0011d7a..a87160a 100644 (file)
@@ -121,7 +121,7 @@ class ApiParseTest extends ApiTestCase {
 
                $this->setMwGlobals( 'wgExtraInterlanguageLinkPrefixes', [ 'madeuplanguage' ] );
                $this->tablesUsed[] = 'interwiki';
-               $this->overrideMwServices();
+               $this->resetServices();
        }
 
        /**
index 47a6d81..ecb7e1e 100644 (file)
@@ -307,6 +307,7 @@ class ApiStashEditTest extends ApiTestCase {
 
                // Nor does the original one if they become a bot
                $user->addGroup( 'bot' );
+               MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
                $this->assertFalse(
                        $this->doCheckCache( $user ),
                        "We assume bots don't have cache entries"
@@ -315,6 +316,7 @@ class ApiStashEditTest extends ApiTestCase {
                // But other groups are okay
                $user->removeGroup( 'bot' );
                $user->addGroup( 'sysop' );
+               MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
                $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
        }
 
index a1bafed..0d7ad0c 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\MediaWikiServices;
 
 /**
  * @group API
@@ -36,6 +37,8 @@ class ApiUserrightsTest extends ApiTestCase {
                if ( $remove ) {
                        $this->mergeMwGlobalArrayValue( 'wgRemoveGroups', [ 'bureaucrat' => $remove ] );
                }
+
+               $this->resetServices();
        }
 
        /**
@@ -75,6 +78,7 @@ class ApiUserrightsTest extends ApiTestCase {
                $res = $this->doApiRequestWithToken( $params );
 
                $user->clearInstanceCache();
+               MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
                $this->assertSame( $expectedGroups, $user->getGroups() );
 
                $this->assertArrayNotHasKey( 'warnings', $res[0] );
@@ -217,6 +221,7 @@ class ApiUserrightsTest extends ApiTestCase {
                ChangeTags::defineTag( 'custom tag' );
 
                $this->setGroupPermissions( 'user', 'applychangetags', false );
+               $this->resetServices();
 
                $this->doFailedRightsChange(
                        'You do not have permission to apply change tags along with your changes.',
index e6a1d38..fc6f688 100644 (file)
@@ -1446,6 +1446,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
                ];
                $block = new DatabaseBlock( $blockOptions );
                $block->insert();
+               $this->resetServices();
                $status = $this->manager->checkAccountCreatePermissions( $user );
                $this->assertFalse( $status->isOK() );
                $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
@@ -1472,12 +1473,12 @@ class AuthManagerTest extends \MediaWikiTestCase {
                        ],
                        'wgProxyWhitelist' => [],
                ] );
-               $this->overrideMwServices();
+               $this->resetServices();
                $status = $this->manager->checkAccountCreatePermissions( new \User );
                $this->assertFalse( $status->isOK() );
                $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
                $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
-               $this->overrideMwServices();
+               $this->resetServices();
                $status = $this->manager->checkAccountCreatePermissions( new \User );
                $this->assertTrue( $status->isGood() );
        }
@@ -2365,6 +2366,8 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $this->mergeMwGlobalArrayValue( 'wgObjectCaches',
                        [ __METHOD__ => [ 'class' => 'HashBagOStuff' ] ] );
                $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
+               // Supply services with updated globals
+               $this->resetServices();
 
                // Set up lots of mocks...
                $mocks = [];
diff --git a/tests/phpunit/includes/auth/AuthenticationResponseTest.php b/tests/phpunit/includes/auth/AuthenticationResponseTest.php
deleted file mode 100644 (file)
index c796822..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace MediaWiki\Auth;
-
-/**
- * @group AuthManager
- * @covers \MediaWiki\Auth\AuthenticationResponse
- */
-class AuthenticationResponseTest extends \MediaWikiTestCase {
-       /**
-        * @dataProvider provideConstructors
-        * @param string $constructor
-        * @param array $args
-        * @param array|Exception $expect
-        */
-       public function testConstructors( $constructor, $args, $expect ) {
-               if ( is_array( $expect ) ) {
-                       $res = new AuthenticationResponse();
-                       $res->messageType = 'warning';
-                       foreach ( $expect as $field => $value ) {
-                               $res->$field = $value;
-                       }
-                       $ret = call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
-                       $this->assertEquals( $res, $ret );
-               } else {
-                       try {
-                               call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
-                               $this->fail( 'Expected exception not thrown' );
-                       } catch ( \Exception $ex ) {
-                               $this->assertEquals( $expect, $ex );
-                       }
-               }
-       }
-
-       public function provideConstructors() {
-               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
-               $msg = new \Message( 'mainpage' );
-
-               return [
-                       [ 'newPass', [], [
-                               'status' => AuthenticationResponse::PASS,
-                       ] ],
-                       [ 'newPass', [ 'name' ], [
-                               'status' => AuthenticationResponse::PASS,
-                               'username' => 'name',
-                       ] ],
-                       [ 'newPass', [ 'name', null ], [
-                               'status' => AuthenticationResponse::PASS,
-                               'username' => 'name',
-                       ] ],
-
-                       [ 'newFail', [ $msg ], [
-                               'status' => AuthenticationResponse::FAIL,
-                               'message' => $msg,
-                               'messageType' => 'error',
-                       ] ],
-
-                       [ 'newRestart', [ $msg ], [
-                               'status' => AuthenticationResponse::RESTART,
-                               'message' => $msg,
-                       ] ],
-
-                       [ 'newAbstain', [], [
-                               'status' => AuthenticationResponse::ABSTAIN,
-                       ] ],
-
-                       [ 'newUI', [ [ $req ], $msg ], [
-                               'status' => AuthenticationResponse::UI,
-                               'neededRequests' => [ $req ],
-                               'message' => $msg,
-                               'messageType' => 'warning',
-                       ] ],
-
-                       [ 'newUI', [ [ $req ], $msg, 'warning' ], [
-                               'status' => AuthenticationResponse::UI,
-                               'neededRequests' => [ $req ],
-                               'message' => $msg,
-                               'messageType' => 'warning',
-                       ] ],
-
-                       [ 'newUI', [ [ $req ], $msg, 'error' ], [
-                               'status' => AuthenticationResponse::UI,
-                               'neededRequests' => [ $req ],
-                               'message' => $msg,
-                               'messageType' => 'error',
-                       ] ],
-                       [ 'newUI', [ [], $msg ],
-                               new \InvalidArgumentException( '$reqs may not be empty' )
-                       ],
-
-                       [ 'newRedirect', [ [ $req ], 'http://example.org/redir' ], [
-                               'status' => AuthenticationResponse::REDIRECT,
-                               'neededRequests' => [ $req ],
-                               'redirectTarget' => 'http://example.org/redir',
-                       ] ],
-                       [
-                               'newRedirect',
-                               [ [ $req ], 'http://example.org/redir', [ 'foo' => 'bar' ] ],
-                               [
-                                       'status' => AuthenticationResponse::REDIRECT,
-                                       'neededRequests' => [ $req ],
-                                       'redirectTarget' => 'http://example.org/redir',
-                                       'redirectApiData' => [ 'foo' => 'bar' ],
-                               ]
-                       ],
-                       [ 'newRedirect', [ [], 'http://example.org/redir' ],
-                               new \InvalidArgumentException( '$reqs may not be empty' )
-                       ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php b/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php
deleted file mode 100644 (file)
index 6190516..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @covers ChangesListFilterGroup
- */
-class ChangesListFilterGroupTest extends MediaWikiTestCase {
-       /**
-        * phpcs:disable Generic.Files.LineLength
-        * @expectedException MWException
-        * @expectedExceptionMessage Group names may not contain '_'.  Use the naming convention: 'camelCase'
-        * phpcs:enable
-        */
-       public function testReservedCharacter() {
-               new MockChangesListFilterGroup(
-                       [
-                               'type' => 'some_type',
-                               'name' => 'group_name',
-                               'priority' => 1,
-                               'filters' => [],
-                       ]
-               );
-       }
-
-       public function testAutoPriorities() {
-               $group = new MockChangesListFilterGroup(
-                       [
-                               'type' => 'some_type',
-                               'name' => 'groupName',
-                               'isFullCoverage' => true,
-                               'priority' => 1,
-                               'filters' => [
-                                       [ 'name' => 'hidefoo' ],
-                                       [ 'name' => 'hidebar' ],
-                                       [ 'name' => 'hidebaz' ],
-                               ],
-                       ]
-               );
-
-               $filters = $group->getFilters();
-               $this->assertEquals(
-                       [
-                               -2,
-                               -3,
-                               -4,
-                       ],
-                       array_map(
-                               function ( $f ) {
-                                       return $f->getPriority();
-                               },
-                               array_values( $filters )
-                       )
-               );
-       }
-
-       // Get without warnings
-       public function testGetFilter() {
-               $group = new MockChangesListFilterGroup(
-                       [
-                               'type' => 'some_type',
-                               'name' => 'groupName',
-                               'isFullCoverage' => true,
-                               'priority' => 1,
-                               'filters' => [
-                                       [ 'name' => 'foo' ],
-                               ],
-                       ]
-               );
-
-               $this->assertEquals(
-                       'foo',
-                       $group->getFilter( 'foo' )->getName()
-               );
-
-               $this->assertEquals(
-                       null,
-                       $group->getFilter( 'bar' )
-               );
-       }
-}
diff --git a/tests/phpunit/includes/config/HashConfigTest.php b/tests/phpunit/includes/config/HashConfigTest.php
deleted file mode 100644 (file)
index bac8311..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-class HashConfigTest extends MediaWikiTestCase {
-
-       /**
-        * @covers HashConfig::newInstance
-        */
-       public function testNewInstance() {
-               $conf = HashConfig::newInstance();
-               $this->assertInstanceOf( HashConfig::class, $conf );
-       }
-
-       /**
-        * @covers HashConfig::__construct
-        */
-       public function testConstructor() {
-               $conf = new HashConfig();
-               $this->assertInstanceOf( HashConfig::class, $conf );
-
-               // Test passing arguments to the constructor
-               $conf2 = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $this->assertEquals( '1', $conf2->get( 'one' ) );
-       }
-
-       /**
-        * @covers HashConfig::get
-        */
-       public function testGet() {
-               $conf = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $this->assertEquals( '1', $conf->get( 'one' ) );
-               $this->setExpectedException( ConfigException::class, 'HashConfig::get: undefined option' );
-               $conf->get( 'two' );
-       }
-
-       /**
-        * @covers HashConfig::has
-        */
-       public function testHas() {
-               $conf = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $this->assertTrue( $conf->has( 'one' ) );
-               $this->assertFalse( $conf->has( 'two' ) );
-       }
-
-       /**
-        * @covers HashConfig::set
-        */
-       public function testSet() {
-               $conf = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $conf->set( 'two', '2' );
-               $this->assertEquals( '2', $conf->get( 'two' ) );
-               // Check that set overwrites
-               $conf->set( 'one', '3' );
-               $this->assertEquals( '3', $conf->get( 'one' ) );
-       }
-}
diff --git a/tests/phpunit/includes/config/MultiConfigTest.php b/tests/phpunit/includes/config/MultiConfigTest.php
deleted file mode 100644 (file)
index fc28395..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-class MultiConfigTest extends MediaWikiTestCase {
-
-       /**
-        * Tests that settings are fetched in the right order
-        *
-        * @covers MultiConfig::__construct
-        * @covers MultiConfig::get
-        */
-       public function testGet() {
-               $multi = new MultiConfig( [
-                       new HashConfig( [ 'foo' => 'bar' ] ),
-                       new HashConfig( [ 'foo' => 'baz', 'bar' => 'foo' ] ),
-                       new HashConfig( [ 'bar' => 'baz' ] ),
-               ] );
-
-               $this->assertEquals( 'bar', $multi->get( 'foo' ) );
-               $this->assertEquals( 'foo', $multi->get( 'bar' ) );
-               $this->setExpectedException( ConfigException::class, 'MultiConfig::get: undefined option:' );
-               $multi->get( 'notset' );
-       }
-
-       /**
-        * @covers MultiConfig::has
-        */
-       public function testHas() {
-               $conf = new MultiConfig( [
-                       new HashConfig( [ 'foo' => 'foo' ] ),
-                       new HashConfig( [ 'something' => 'bleh' ] ),
-                       new HashConfig( [ 'meh' => 'eh' ] ),
-               ] );
-
-               $this->assertTrue( $conf->has( 'foo' ) );
-               $this->assertTrue( $conf->has( 'something' ) );
-               $this->assertTrue( $conf->has( 'meh' ) );
-               $this->assertFalse( $conf->has( 'what' ) );
-       }
-}
diff --git a/tests/phpunit/includes/config/ServiceOptionsTest.php b/tests/phpunit/includes/config/ServiceOptionsTest.php
deleted file mode 100644 (file)
index 966cf41..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-
-use MediaWiki\Config\ServiceOptions;
-
-/**
- * @coversDefaultClass \MediaWiki\Config\ServiceOptions
- */
-class ServiceOptionsTest extends MediaWikiTestCase {
-       public static $testObj;
-
-       public static function setUpBeforeClass() {
-               parent::setUpBeforeClass();
-
-               self::$testObj = new stdclass();
-       }
-
-       /**
-        * @dataProvider provideConstructor
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        * @covers ::get
-        */
-       public function testConstructor( $expected, $keys, ...$sources ) {
-               $options = new ServiceOptions( $keys, ...$sources );
-
-               foreach ( $expected as $key => $val ) {
-                       $this->assertSame( $val, $options->get( $key ) );
-               }
-
-               // This is lumped in the same test because there's no support for depending on a test that
-               // has a data provider.
-               $options->assertRequiredOptions( array_keys( $expected ) );
-
-               // Suppress warning if no assertions were run. This is expected for empty arguments.
-               $this->assertTrue( true );
-       }
-
-       public function provideConstructor() {
-               return [
-                       'No keys' => [ [], [], [ 'a' => 'aval' ] ],
-                       'Simple array source' => [
-                               [ 'a' => 'aval', 'b' => 'bval' ],
-                               [ 'a', 'b' ],
-                               [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ],
-                       ],
-                       'Simple HashConfig source' => [
-                               [ 'a' => 'aval', 'b' => 'bval' ],
-                               [ 'a', 'b' ],
-                               new HashConfig( [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ] ),
-                       ],
-                       'Three different sources' => [
-                               [ 'a' => 'aval', 'b' => 'bval' ],
-                               [ 'a', 'b' ],
-                               [ 'z' => 'zval' ],
-                               new HashConfig( [ 'a' => 'aval', 'c' => 'cval' ] ),
-                               [ 'b' => 'bval', 'd' => 'dval' ],
-                       ],
-                       'null key' => [
-                               [ 'a' => null ],
-                               [ 'a' ],
-                               [ 'a' => null ],
-                       ],
-                       'Numeric option name' => [
-                               [ '0' => 'nothing' ],
-                               [ '0' ],
-                               [ '0' => 'nothing' ],
-                       ],
-                       'Multiple sources for one key' => [
-                               [ 'a' => 'winner' ],
-                               [ 'a' ],
-                               [ 'a' => 'winner' ],
-                               [ 'a' => 'second place' ],
-                       ],
-                       'Object value is passed by reference' => [
-                               [ 'a' => self::$testObj ],
-                               [ 'a' ],
-                               [ 'a' => self::$testObj ],
-                       ],
-               ];
-       }
-
-       /**
-        * @covers ::__construct
-        */
-       public function testKeyNotFound() {
-               $this->setExpectedException( InvalidArgumentException::class,
-                       'Key "a" not found in input sources' );
-
-               new ServiceOptions( [ 'a' ], [ 'b' => 'bval' ], [ 'c' => 'cval' ] );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testOutOfOrderAssertRequiredOptions() {
-               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
-               $options->assertRequiredOptions( [ 'b', 'a' ] );
-               $this->assertTrue( true, 'No exception thrown' );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::get
-        */
-       public function testGetUnrecognized() {
-               $this->setExpectedException( InvalidArgumentException::class,
-                       'Unrecognized option "b"' );
-
-               $options = new ServiceOptions( [ 'a' ], [ 'a' => '' ] );
-               $options->get( 'b' );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testExtraKeys() {
-               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
-                       'Precondition failed: Unsupported options passed: b, c!' );
-
-               $options = new ServiceOptions( [ 'a', 'b', 'c' ], [ 'a' => '', 'b' => '', 'c' => '' ] );
-               $options->assertRequiredOptions( [ 'a' ] );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testMissingKeys() {
-               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
-                       'Precondition failed: Required options missing: a, b!' );
-
-               $options = new ServiceOptions( [ 'c' ], [ 'c' => '' ] );
-               $options->assertRequiredOptions( [ 'a', 'b', 'c' ] );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testExtraAndMissingKeys() {
-               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
-                       'Precondition failed: Unsupported options passed: b! Required options missing: c!' );
-
-               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
-               $options->assertRequiredOptions( [ 'a', 'c' ] );
-       }
-}
diff --git a/tests/phpunit/includes/content/JsonContentHandlerTest.php b/tests/phpunit/includes/content/JsonContentHandlerTest.php
deleted file mode 100644 (file)
index abfb673..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-class JsonContentHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @covers JsonContentHandler::makeEmptyContent
-        */
-       public function testMakeEmptyContent() {
-               $handler = new JsonContentHandler();
-               $content = $handler->makeEmptyContent();
-               $this->assertInstanceOf( JsonContent::class, $content );
-               $this->assertTrue( $content->isValid() );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/MonologSpiTest.php b/tests/phpunit/includes/debug/logger/MonologSpiTest.php
deleted file mode 100644 (file)
index fda3ac6..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-<?php
-/**
- * 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
- */
-
-namespace MediaWiki\Logger;
-
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-class MonologSpiTest extends MediaWikiTestCase {
-
-       /**
-        * @covers MediaWiki\Logger\MonologSpi::mergeConfig
-        */
-       public function testMergeConfig() {
-               $base = [
-                       'loggers' => [
-                               '@default' => [
-                                       'processors' => [ 'constructor' ],
-                                       'handlers' => [ 'constructor' ],
-                               ],
-                       ],
-                       'processors' => [
-                               'constructor' => [
-                                       'class' => 'constructor',
-                               ],
-                       ],
-                       'handlers' => [
-                               'constructor' => [
-                                       'class' => 'constructor',
-                                       'formatter' => 'constructor',
-                               ],
-                       ],
-                       'formatters' => [
-                               'constructor' => [
-                                       'class' => 'constructor',
-                               ],
-                       ],
-               ];
-
-               $fixture = new MonologSpi( $base );
-               $this->assertSame(
-                       $base,
-                       TestingAccessWrapper::newFromObject( $fixture )->config
-               );
-
-               $fixture->mergeConfig( [
-                       'loggers' => [
-                               'merged' => [
-                                       'processors' => [ 'merged' ],
-                                       'handlers' => [ 'merged' ],
-                               ],
-                       ],
-                       'processors' => [
-                               'merged' => [
-                                       'class' => 'merged',
-                               ],
-                       ],
-                       'magic' => [
-                               'idkfa' => [ 'xyzzy' ],
-                       ],
-                       'handlers' => [
-                               'merged' => [
-                                       'class' => 'merged',
-                                       'formatter' => 'merged',
-                               ],
-                       ],
-                       'formatters' => [
-                               'merged' => [
-                                       'class' => 'merged',
-                               ],
-                       ],
-               ] );
-               $this->assertSame(
-                       [
-                               'loggers' => [
-                                       '@default' => [
-                                               'processors' => [ 'constructor' ],
-                                               'handlers' => [ 'constructor' ],
-                                       ],
-                                       'merged' => [
-                                               'processors' => [ 'merged' ],
-                                               'handlers' => [ 'merged' ],
-                                       ],
-                               ],
-                               'processors' => [
-                                       'constructor' => [
-                                               'class' => 'constructor',
-                                       ],
-                                       'merged' => [
-                                               'class' => 'merged',
-                                       ],
-                               ],
-                               'handlers' => [
-                                       'constructor' => [
-                                               'class' => 'constructor',
-                                               'formatter' => 'constructor',
-                                       ],
-                                       'merged' => [
-                                               'class' => 'merged',
-                                               'formatter' => 'merged',
-                                       ],
-                               ],
-                               'formatters' => [
-                                       'constructor' => [
-                                               'class' => 'constructor',
-                                       ],
-                                       'merged' => [
-                                               'class' => 'merged',
-                                       ],
-                               ],
-                               'magic' => [
-                                       'idkfa' => [ 'xyzzy' ],
-                               ],
-                       ],
-                       TestingAccessWrapper::newFromObject( $fixture )->config
-               );
-       }
-
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php
deleted file mode 100644 (file)
index baa4df7..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-/**
- * 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
- */
-
-namespace MediaWiki\Logger\Monolog;
-
-use MediaWikiTestCase;
-use PHPUnit_Framework_Error_Notice;
-
-/**
- * @covers \MediaWiki\Logger\Monolog\AvroFormatter
- */
-class AvroFormatterTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               if ( !class_exists( 'AvroStringIO' ) ) {
-                       $this->markTestSkipped( 'Avro is required for the AvroFormatterTest' );
-               }
-               parent::setUp();
-       }
-
-       public function testSchemaNotAvailable() {
-               $formatter = new AvroFormatter( [] );
-               $this->setExpectedException(
-                       'PHPUnit_Framework_Error_Notice',
-                       "The schema for channel 'marty' is not available"
-               );
-               $formatter->format( [ 'channel' => 'marty' ] );
-       }
-
-       public function testSchemaNotAvailableReturnValue() {
-               $formatter = new AvroFormatter( [] );
-               $noticeEnabled = PHPUnit_Framework_Error_Notice::$enabled;
-               // disable conversion of notices
-               PHPUnit_Framework_Error_Notice::$enabled = false;
-               // have to keep the user notice from being output
-               \Wikimedia\suppressWarnings();
-               $res = $formatter->format( [ 'channel' => 'marty' ] );
-               \Wikimedia\restoreWarnings();
-               PHPUnit_Framework_Error_Notice::$enabled = $noticeEnabled;
-               $this->assertNull( $res );
-       }
-
-       public function testDoesSomethingWhenSchemaAvailable() {
-               $formatter = new AvroFormatter( [
-                       'string' => [
-                               'schema' => [ 'type' => 'string' ],
-                               'revision' => 1010101,
-                       ]
-               ] );
-               $res = $formatter->format( [
-                       'channel' => 'string',
-                       'context' => 'better to be',
-               ] );
-               $this->assertNotNull( $res );
-               // basically just tell us if avro changes its string encoding, or if
-               // we completely fail to generate a log message.
-               $this->assertEquals( 'AAAAAAAAD2m1GGJldHRlciB0byBiZQ==', base64_encode( $res ) );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php b/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php
deleted file mode 100644 (file)
index 4c0ca04..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-<?php
-/**
- * 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
- */
-
-namespace MediaWiki\Logger\Monolog;
-
-use MediaWikiTestCase;
-use Monolog\Logger;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers \MediaWiki\Logger\Monolog\KafkaHandler
- */
-class KafkaHandlerTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               if ( !class_exists( 'Monolog\Handler\AbstractProcessingHandler' )
-                       || !class_exists( 'Kafka\Produce' )
-               ) {
-                       $this->markTestSkipped( 'Monolog and Kafka are required for the KafkaHandlerTest' );
-               }
-
-               parent::setUp();
-       }
-
-       public function topicNamingProvider() {
-               return [
-                       [ [], 'monolog_foo' ],
-                       [ [ 'alias' => [ 'foo' => 'bar' ] ], 'bar' ]
-               ];
-       }
-
-       /**
-        * @dataProvider topicNamingProvider
-        */
-       public function testTopicNaming( $options, $expect ) {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $produce->expects( $this->once() )
-                       ->method( 'setMessages' )
-                       ->with( $expect, $this->anything(), $this->anything() );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-
-               $handler = new KafkaHandler( $produce, $options );
-               $handler->handle( [
-                       'channel' => 'foo',
-                       'level' => Logger::EMERGENCY,
-                       'extra' => [],
-                       'context' => [],
-               ] );
-       }
-
-       public function swallowsExceptionsWhenRequested() {
-               return [
-                       // defaults to false
-                       [ [], true ],
-                       // also try false explicitly
-                       [ [ 'swallowExceptions' => false ], true ],
-                       // turn it on
-                       [ [ 'swallowExceptions' => true ], false ],
-               ];
-       }
-
-       /**
-        * @dataProvider swallowsExceptionsWhenRequested
-        */
-       public function testGetAvailablePartitionsException( $options, $expectException ) {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->throwException( new \Kafka\Exception ) );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-
-               if ( $expectException ) {
-                       $this->setExpectedException( 'Kafka\Exception' );
-               }
-
-               $handler = new KafkaHandler( $produce, $options );
-               $handler->handle( [
-                       'channel' => 'foo',
-                       'level' => Logger::EMERGENCY,
-                       'extra' => [],
-                       'context' => [],
-               ] );
-
-               if ( !$expectException ) {
-                       $this->assertTrue( true, 'no exception was thrown' );
-               }
-       }
-
-       /**
-        * @dataProvider swallowsExceptionsWhenRequested
-        */
-       public function testSendException( $options, $expectException ) {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->throwException( new \Kafka\Exception ) );
-
-               if ( $expectException ) {
-                       $this->setExpectedException( 'Kafka\Exception' );
-               }
-
-               $handler = new KafkaHandler( $produce, $options );
-               $handler->handle( [
-                       'channel' => 'foo',
-                       'level' => Logger::EMERGENCY,
-                       'extra' => [],
-                       'context' => [],
-               ] );
-
-               if ( !$expectException ) {
-                       $this->assertTrue( true, 'no exception was thrown' );
-               }
-       }
-
-       public function testHandlesNullFormatterResult() {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $mockMethod = $produce->expects( $this->exactly( 2 ) )
-                       ->method( 'setMessages' );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-               // evil hax
-               $matcher = TestingAccessWrapper::newFromObject( $mockMethod )->matcher;
-               TestingAccessWrapper::newFromObject( $matcher )->parametersMatcher =
-                       new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [
-                               [ $this->anything(), $this->anything(), [ 'words' ] ],
-                               [ $this->anything(), $this->anything(), [ 'lines' ] ]
-                       ] );
-
-               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
-               $formatter->expects( $this->any() )
-                       ->method( 'format' )
-                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
-
-               $handler = new KafkaHandler( $produce, [] );
-               $handler->setFormatter( $formatter );
-               for ( $i = 0; $i < 3; ++$i ) {
-                       $handler->handle( [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ] );
-               }
-       }
-
-       public function testBatchHandlesNullFormatterResult() {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $produce->expects( $this->once() )
-                       ->method( 'setMessages' )
-                       ->with( $this->anything(), $this->anything(), [ 'words', 'lines' ] );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-
-               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
-               $formatter->expects( $this->any() )
-                       ->method( 'format' )
-                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
-
-               $handler = new KafkaHandler( $produce, [] );
-               $handler->setFormatter( $formatter );
-               $handler->handleBatch( [
-                       [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ],
-                       [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ],
-                       [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ],
-               ] );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php
deleted file mode 100644 (file)
index bdd5c81..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-/**
- * 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
- */
-
-namespace MediaWiki\Logger\Monolog;
-
-use AssertionError;
-use InvalidArgumentException;
-use LengthException;
-use LogicException;
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-class LineFormatterTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               if ( !class_exists( 'Monolog\Formatter\LineFormatter' ) ) {
-                       $this->markTestSkipped( 'This test requires monolog to be installed' );
-               }
-               parent::setUp();
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionNoTrace() {
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( false );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new LogicException( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
-               $this->assertNotContains( "\n  #0", $out );
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionTrace() {
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( true );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new LogicException( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
-               $this->assertContains( "\n  #0", $out );
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionErrorNoTrace() {
-               if ( !class_exists( AssertionError::class ) ) {
-                       $this->markTestSkipped( 'AssertionError class does not exist' );
-               }
-
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( false );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new AssertionError( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
-               $this->assertNotContains( "\n  #0", $out );
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionErrorTrace() {
-               if ( !class_exists( AssertionError::class ) ) {
-                       $this->markTestSkipped( 'AssertionError class does not exist' );
-               }
-
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( true );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new AssertionError( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
-               $this->assertContains( "\n  #0", $out );
-       }
-}
diff --git a/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php b/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php
deleted file mode 100644 (file)
index 8d94404..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- *
- * @group Diff
- */
-class ArrayDiffFormatterTest extends MediaWikiTestCase {
-
-       /**
-        * @param Diff $input
-        * @param array $expectedOutput
-        * @dataProvider provideTestFormat
-        * @covers ArrayDiffFormatter::format
-        */
-       public function testFormat( $input, $expectedOutput ) {
-               $instance = new ArrayDiffFormatter();
-               $output = $instance->format( $input );
-               $this->assertEquals( $expectedOutput, $output );
-       }
-
-       private function getMockDiff( $edits ) {
-               $diff = $this->getMockBuilder( Diff::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $diff->expects( $this->any() )
-                       ->method( 'getEdits' )
-                       ->will( $this->returnValue( $edits ) );
-               return $diff;
-       }
-
-       private function getMockDiffOp( $type = null, $orig = [], $closing = [] ) {
-               $diffOp = $this->getMockBuilder( DiffOp::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $diffOp->expects( $this->any() )
-                       ->method( 'getType' )
-                       ->will( $this->returnValue( $type ) );
-               $diffOp->expects( $this->any() )
-                       ->method( 'getOrig' )
-                       ->will( $this->returnValue( $orig ) );
-               if ( $type === 'change' ) {
-                       $diffOp->expects( $this->any() )
-                               ->method( 'getClosing' )
-                               ->with( $this->isType( 'integer' ) )
-                               ->will( $this->returnCallback( function () {
-                                       return 'mockLine';
-                               } ) );
-               } else {
-                       $diffOp->expects( $this->any() )
-                               ->method( 'getClosing' )
-                               ->will( $this->returnValue( $closing ) );
-               }
-               return $diffOp;
-       }
-
-       public function provideTestFormat() {
-               $emptyArrayTestCases = [
-                       $this->getMockDiff( [] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'change' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'FOOBARBAZ' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', 'line' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [], [ 'line' ] ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy', [], [ 'line' ] ) ] ),
-               ];
-
-               $otherTestCases = [];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1' ] ) ] ),
-                       [ [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ] ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1', 'a2' ] ) ] ),
-                       [
-                               [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ],
-                               [ 'action' => 'add', 'new' => 'a2', 'newline' => 2 ],
-                       ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1' ] ) ] ),
-                       [ [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ] ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1', 'd2' ] ) ] ),
-                       [
-                               [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ],
-                               [ 'action' => 'delete', 'old' => 'd2', 'oldline' => 2 ],
-                       ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'change', [ 'd1' ], [ 'a1' ] ) ] ),
-                       [ [
-                               'action' => 'change',
-                               'old' => 'd1',
-                               'new' => 'mockLine',
-                               'newline' => 1, 'oldline' => 1
-                       ] ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp(
-                               'change',
-                               [ 'd1', 'd2' ],
-                               [ 'a1', 'a2' ]
-                       ) ] ),
-                       [
-                               [
-                                       'action' => 'change',
-                                       'old' => 'd1',
-                                       'new' => 'mockLine',
-                                       'newline' => 1, 'oldline' => 1
-                               ],
-                               [
-                                       'action' => 'change',
-                                       'old' => 'd2',
-                                       'new' => 'mockLine',
-                                       'newline' => 2, 'oldline' => 2
-                               ],
-                       ],
-               ];
-
-               $testCases = [];
-               foreach ( $emptyArrayTestCases as $testCase ) {
-                       $testCases[] = [ $testCase, [] ];
-               }
-               foreach ( $otherTestCases as $testCase ) {
-                       $testCases[] = [ $testCase[0], $testCase[1] ];
-               }
-               return $testCases;
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/DiffOpTest.php b/tests/phpunit/includes/diff/DiffOpTest.php
deleted file mode 100644 (file)
index 3026fad..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * @author Addshore
- *
- * @group Diff
- */
-class DiffOpTest extends MediaWikiTestCase {
-
-       /**
-        * @covers DiffOp::getType
-        */
-       public function testGetType() {
-               $obj = new FakeDiffOp();
-               $obj->type = 'foo';
-               $this->assertEquals( 'foo', $obj->getType() );
-       }
-
-       /**
-        * @covers DiffOp::getOrig
-        */
-       public function testGetOrig() {
-               $obj = new FakeDiffOp();
-               $obj->orig = [ 'foo' ];
-               $this->assertEquals( [ 'foo' ], $obj->getOrig() );
-       }
-
-       /**
-        * @covers DiffOp::getClosing
-        */
-       public function testGetClosing() {
-               $obj = new FakeDiffOp();
-               $obj->closing = [ 'foo' ];
-               $this->assertEquals( [ 'foo' ], $obj->getClosing() );
-       }
-
-       /**
-        * @covers DiffOp::getClosing
-        */
-       public function testGetClosingWithParameter() {
-               $obj = new FakeDiffOp();
-               $obj->closing = [ 'foo', 'bar', 'baz' ];
-               $this->assertEquals( 'foo', $obj->getClosing( 0 ) );
-               $this->assertEquals( 'bar', $obj->getClosing( 1 ) );
-               $this->assertEquals( 'baz', $obj->getClosing( 2 ) );
-               $this->assertEquals( null, $obj->getClosing( 3 ) );
-       }
-
-       /**
-        * @covers DiffOp::norig
-        */
-       public function testNorig() {
-               $obj = new FakeDiffOp();
-               $this->assertEquals( 0, $obj->norig() );
-               $obj->orig = [ 'foo' ];
-               $this->assertEquals( 1, $obj->norig() );
-       }
-
-       /**
-        * @covers DiffOp::nclosing
-        */
-       public function testNclosing() {
-               $obj = new FakeDiffOp();
-               $this->assertEquals( 0, $obj->nclosing() );
-               $obj->closing = [ 'foo' ];
-               $this->assertEquals( 1, $obj->nclosing() );
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/DiffTest.php b/tests/phpunit/includes/diff/DiffTest.php
deleted file mode 100644 (file)
index da6d7d9..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- *
- * @group Diff
- */
-class DiffTest extends MediaWikiTestCase {
-
-       /**
-        * @covers Diff::getEdits
-        */
-       public function testGetEdits() {
-               $obj = new Diff( [], [] );
-               $obj->edits = 'FooBarBaz';
-               $this->assertEquals( 'FooBarBaz', $obj->getEdits() );
-       }
-
-}
diff --git a/tests/phpunit/includes/exception/MWExceptionHandlerTest.php b/tests/phpunit/includes/exception/MWExceptionHandlerTest.php
deleted file mode 100644 (file)
index 6606065..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-/**
- * @author Antoine Musso
- * @copyright Copyright © 2013, Antoine Musso
- * @copyright Copyright © 2013, Wikimedia Foundation Inc.
- * @file
- */
-
-class MWExceptionHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @covers MWExceptionHandler::getRedactedTrace
-        */
-       public function testGetRedactedTrace() {
-               $refvar = 'value';
-               try {
-                       $array = [ 'a', 'b' ];
-                       $object = new stdClass();
-                       self::helperThrowAnException( $array, $object, $refvar );
-               } catch ( Exception $e ) {
-               }
-
-               # Make sure our stack 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 );
-                       }
-               }
-
-               $this->assertEquals( 'value', $refvar, 'Ensuring reference variable wasn\'t changed' );
-       }
-
-       /**
-        * Helper function for testExpandArgumentsInCall
-        *
-        * Pass it an object and an array, and something by reference :-)
-        *
-        * @throws Exception
-        */
-       protected static function helperThrowAnException( $a, $b, &$c ) {
-               throw new Exception();
-       }
-}
diff --git a/tests/phpunit/includes/externalstore/ExternalStoreAccessTest.php b/tests/phpunit/includes/externalstore/ExternalStoreAccessTest.php
new file mode 100644 (file)
index 0000000..80e836f
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+use Wikimedia\Rdbms\LBFactory;
+
+/**
+ * @covers ExternalStoreAccess
+ */
+class ExternalStoreAccessTest extends MediaWikiTestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers ExternalStoreAccess::isReadOnly
+        */
+       public function testBasic() {
+               $active = [ 'memory' ];
+               $defaults = [ 'memory://cluster1', 'memory://cluster2' ];
+               $esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+               $access = new ExternalStoreAccess( $esFactory );
+
+               $this->assertEquals( false, $access->isReadOnly() );
+
+               /** @var ExternalStoreMemory $store */
+               $store = $esFactory->getStore( 'memory' );
+               $this->assertInstanceOf( ExternalStoreMemory::class, $store );
+
+               $lb = $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()->getMock();
+               $lb->expects( $this->any() )->method( 'getReadOnlyReason' )->willReturn( 'Locked' );
+               $lb->expects( $this->any() )->method( 'getServerInfo' )->willReturn( [] );
+
+               $lbFactory = $this->getMockBuilder( LBFactory::class )
+                       ->disableOriginalConstructor()->getMock();
+               $lbFactory->expects( $this->any() )->method( 'getExternalLB' )->willReturn( $lb );
+
+               $this->setService( 'DBLoadBalancerFactory', $lbFactory );
+
+               $active = [ 'db', 'mwstore' ];
+               $defaults = [ 'DB://clusterX' ];
+               $esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+               $access = new ExternalStoreAccess( $esFactory );
+               $this->assertEquals( true, $access->isReadOnly() );
+
+               $store->clear();
+       }
+
+       /**
+        * @covers ExternalStoreAccess::fetchFromURL
+        * @covers ExternalStoreAccess::fetchFromURLs
+        * @covers ExternalStoreAccess::insert
+        */
+       public function testReadWrite() {
+               $active = [ 'memory' ]; // active store types
+               $defaults = [ 'memory://cluster1', 'memory://cluster2' ];
+               $esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+               $access = new ExternalStoreAccess( $esFactory );
+
+               /** @var ExternalStoreMemory $storeLocal */
+               $storeLocal = $esFactory->getStore( 'memory' );
+               /** @var ExternalStoreMemory $storeOther */
+               $storeOther = $esFactory->getStore( 'memory', [ 'domain' => 'other' ] );
+               $this->assertInstanceOf( ExternalStoreMemory::class, $storeLocal );
+               $this->assertInstanceOf( ExternalStoreMemory::class, $storeOther );
+
+               $v1 = wfRandomString();
+               $v2 = wfRandomString();
+               $v3 = wfRandomString();
+
+               $this->assertEquals( false, $storeLocal->fetchFromURL( 'memory://cluster1/1' ) );
+
+               $url1 = 'memory://cluster1/1';
+               $this->assertEquals(
+                       $url1,
+                       $esFactory->getStoreForUrl( 'memory://cluster1' )
+                               ->store( $esFactory->getStoreLocationFromUrl( 'memory://cluster1' ), $v1 )
+               );
+               $this->assertEquals(
+                       $v1,
+                       $esFactory->getStoreForUrl( 'memory://cluster1/1' )
+                               ->fetchFromURL( 'memory://cluster1/1' )
+               );
+               $this->assertEquals( $v1, $storeLocal->fetchFromURL( 'memory://cluster1/1' ) );
+
+               $url2 = $access->insert( $v2 );
+               $url3 = $access->insert( $v3, [ 'domain' => 'other' ] );
+               $this->assertNotFalse( $url2 );
+               $this->assertNotFalse( $url3 );
+               // There is only one active store type
+               $this->assertEquals( $v2, $storeLocal->fetchFromURL( $url2 ) );
+               $this->assertEquals( $v3, $storeOther->fetchFromURL( $url3 ) );
+               $this->assertEquals( false, $storeOther->fetchFromURL( $url2 ) );
+               $this->assertEquals( false, $storeLocal->fetchFromURL( $url3 ) );
+
+               $res = $access->fetchFromURLs( [ $url1, $url2, $url3 ] );
+               $this->assertEquals( [ $url1 => $v1, $url2 => $v2, $url3 => false ], $res, "Local-only" );
+
+               $storeLocal->clear();
+               $storeOther->clear();
+       }
+}
index f762693..e63ce59 100644 (file)
@@ -2,15 +2,26 @@
 
 /**
  * @covers ExternalStoreFactory
+ * @covers ExternalStoreAccess
  */
-class ExternalStoreFactoryTest extends PHPUnit\Framework\TestCase {
+class ExternalStoreFactoryTest extends MediaWikiTestCase {
 
        use MediaWikiCoversValidator;
 
-       public function testExternalStoreFactory_noStores() {
-               $factory = new ExternalStoreFactory( [] );
-               $this->assertFalse( $factory->getStoreObject( 'ForTesting' ) );
-               $this->assertFalse( $factory->getStoreObject( 'foo' ) );
+       /**
+        * @expectedException ExternalStoreException
+        */
+       public function testExternalStoreFactory_noStores1() {
+               $factory = new ExternalStoreFactory( [], [], 'test-id' );
+               $factory->getStore( 'ForTesting' );
+       }
+
+       /**
+        * @expectedException ExternalStoreException
+        */
+       public function testExternalStoreFactory_noStores2() {
+               $factory = new ExternalStoreFactory( [], [], 'test-id' );
+               $factory->getStore( 'foo' );
        }
 
        public function provideStoreNames() {
@@ -24,18 +35,108 @@ class ExternalStoreFactoryTest extends PHPUnit\Framework\TestCase {
         * @dataProvider provideStoreNames
         */
        public function testExternalStoreFactory_someStore_protoMatch( $proto ) {
-               $factory = new ExternalStoreFactory( [ 'ForTesting' ] );
-               $store = $factory->getStoreObject( $proto );
+               $factory = new ExternalStoreFactory( [ 'ForTesting' ], [], 'test-id' );
+               $store = $factory->getStore( $proto );
                $this->assertInstanceOf( ExternalStoreForTesting::class, $store );
        }
 
        /**
         * @dataProvider provideStoreNames
+        * @expectedException ExternalStoreException
         */
        public function testExternalStoreFactory_someStore_noProtoMatch( $proto ) {
-               $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ] );
-               $store = $factory->getStoreObject( $proto );
-               $this->assertFalse( $store );
+               $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ], [], 'test-id' );
+               $factory->getStore( $proto );
+       }
+
+       /**
+        * @covers ExternalStoreFactory::getProtocols
+        * @covers ExternalStoreFactory::getWriteBaseUrls
+        * @covers ExternalStoreFactory::getStore
+        */
+       public function testStoreFactoryBasic() {
+               $active = [ 'memory' ];
+               $defaults = [ 'memory://cluster1', 'memory://cluster2' ];
+               $esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+
+               $this->assertEquals( $active, $esFactory->getProtocols() );
+               $this->assertEquals( $defaults, $esFactory->getWriteBaseUrls() );
+
+               /** @var ExternalStoreMemory $store */
+               $store = $esFactory->getStore( 'memory' );
+               $this->assertInstanceOf( ExternalStoreMemory::class, $store );
+               $this->assertEquals( false, $store->isReadOnly( 'cluster1' ) );
+               $this->assertEquals( false, $store->isReadOnly( 'cluster2' ) );
+               $this->assertEquals( true, $store->isReadOnly( 'clusterOld' ) );
+
+               $lb = $this->getMockBuilder( \Wikimedia\Rdbms\LoadBalancer::class )
+                       ->disableOriginalConstructor()->getMock();
+               $lb->expects( $this->any() )->method( 'getReadOnlyReason' )->willReturn( 'Locked' );
+               $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactory::class )
+                       ->disableOriginalConstructor()->getMock();
+               $lbFactory->expects( $this->any() )->method( 'getExternalLB' )->willReturn( $lb );
+
+               $this->setService( 'DBLoadBalancerFactory', $lbFactory );
+
+               $active = [ 'db', 'mwstore' ];
+               $defaults = [ 'db://clusterX' ];
+               $esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+               $this->assertEquals( $active, $esFactory->getProtocols() );
+               $this->assertEquals( $defaults, $esFactory->getWriteBaseUrls() );
+
+               $store->clear();
        }
 
+       /**
+        * @covers ExternalStoreFactory::getStoreForUrl
+        * @covers ExternalStoreFactory::getStoreLocationFromUrl
+        */
+       public function testStoreFactoryReadWrite() {
+               $active = [ 'memory' ]; // active store types
+               $defaults = [ 'memory://cluster1', 'memory://cluster2' ];
+               $esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+               $access = new ExternalStoreAccess( $esFactory );
+
+               /** @var ExternalStoreMemory $storeLocal */
+               $storeLocal = $esFactory->getStore( 'memory' );
+               /** @var ExternalStoreMemory $storeOther */
+               $storeOther = $esFactory->getStore( 'memory', [ 'domain' => 'other' ] );
+               $this->assertInstanceOf( ExternalStoreMemory::class, $storeLocal );
+               $this->assertInstanceOf( ExternalStoreMemory::class, $storeOther );
+
+               $v1 = wfRandomString();
+               $v2 = wfRandomString();
+               $v3 = wfRandomString();
+
+               $this->assertEquals( false, $storeLocal->fetchFromURL( 'memory://cluster1/1' ) );
+
+               $url1 = 'memory://cluster1/1';
+               $this->assertEquals(
+                       $url1,
+                       $esFactory->getStoreForUrl( 'memory://cluster1' )
+                               ->store( $esFactory->getStoreLocationFromUrl( 'memory://cluster1' ), $v1 )
+               );
+               $this->assertEquals(
+                       $v1,
+                       $esFactory->getStoreForUrl( 'memory://cluster1/1' )
+                               ->fetchFromURL( 'memory://cluster1/1' )
+               );
+               $this->assertEquals( $v1, $storeLocal->fetchFromURL( 'memory://cluster1/1' ) );
+
+               $url2 = $access->insert( $v2 );
+               $url3 = $access->insert( $v3, [ 'domain' => 'other' ] );
+               $this->assertNotFalse( $url2 );
+               $this->assertNotFalse( $url3 );
+               // There is only one active store type
+               $this->assertEquals( $v2, $storeLocal->fetchFromURL( $url2 ) );
+               $this->assertEquals( $v3, $storeOther->fetchFromURL( $url3 ) );
+               $this->assertEquals( false, $storeOther->fetchFromURL( $url2 ) );
+               $this->assertEquals( false, $storeLocal->fetchFromURL( $url3 ) );
+
+               $res = $access->fetchFromURLs( [ $url1, $url2, $url3 ] );
+               $this->assertEquals( [ $url1 => $v1, $url2 => $v2, $url3 => false ], $res, "Local-only" );
+
+               $storeLocal->clear();
+               $storeOther->clear();
+       }
 }
index 7ca3874..60db27d 100644 (file)
@@ -8,7 +8,7 @@ class ExternalStoreTest extends MediaWikiTestCase {
        public function testExternalFetchFromURL_noExternalStores() {
                $this->setService(
                        'ExternalStoreFactory',
-                       new ExternalStoreFactory( [] )
+                       new ExternalStoreFactory( [], [], 'test-id' )
                );
 
                $this->assertFalse(
@@ -23,7 +23,7 @@ class ExternalStoreTest extends MediaWikiTestCase {
        public function testExternalFetchFromURL_someExternalStore() {
                $this->setService(
                        'ExternalStoreFactory',
-                       new ExternalStoreFactory( [ 'ForTesting' ] )
+                       new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' )
                );
 
                $this->assertEquals(
diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
deleted file mode 100644 (file)
index 9584d4b..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-class InstallDocFormatterTest extends MediaWikiTestCase {
-       /**
-        * @covers InstallDocFormatter
-        * @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 [
-                       # Escape some wikitext
-                       [ 'Install &lt;tag>', 'Install <tag>', 'Escaping <' ],
-                       [ 'Install &#123;&#123;template}}', 'Install {{template}}', 'Escaping [[' ],
-                       [ 'Install &#91;&#91;page]]', 'Install [[page]]', 'Escaping {{' ],
-                       [ 'Install &#95;&#95;TOC&#95;&#95;', 'Install __TOC__', 'Escaping __' ],
-                       [ 'Install ', "Install \r", 'Removing \r' ],
-
-                       # Transform \t{1,2} into :{1,2}
-                       [ ':One indentation', "\tOne indentation", 'Replacing a single \t' ],
-                       [ '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ],
-
-                       # Transform 'T123' links
-                       [
-                               '<span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
-                               'T123', 'Testing T123 links' ],
-                       [
-                               'bug <span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
-                               'bug T123', 'Testing bug T123 links' ],
-                       [
-                               '(<span class="config-plainlink">[https://phabricator.wikimedia.org/T987654 T987654]</span>)',
-                               '(T987654)', 'Testing (T987654) links' ],
-
-                       # "Tabc" shouldn't work
-                       [ 'Tfoobar', 'Tfoobar', "Don't match T followed by non-digits" ],
-                       [ 'T!!fakefake!!', 'T!!fakefake!!', "Don't match T followed by non-digits" ],
-
-                       # Transform 'bug 123' links
-                       [
-                               '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>',
-                               'bug 123', 'Testing bug 123 links' ],
-                       [
-                               '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)',
-                               '(bug 987654)', 'Testing (bug 987654) links' ],
-
-                       # "bug abc" shouldn't work
-                       [ 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ],
-                       [ 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ],
-
-                       # Transform '$wgFooBar' links
-                       [
-                               '<span class="config-plainlink">'
-                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
-                               '$wgFooBar', 'Testing basic $wgFooBar' ],
-                       [
-                               '<span class="config-plainlink">'
-                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
-                               '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ],
-                       [
-                               '<span class="config-plainlink">'
-                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>',
-                               '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ],
-
-                       # Icky variables that shouldn't link
-                       [
-                               '$myAwesomeVariable',
-                               '$myAwesomeVariable',
-                               'Testing $myAwesomeVariable (not starting with $wg)'
-                       ],
-                       [ '$()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
deleted file mode 100644 (file)
index e255089..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-/**
- * @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 [
-                       [ true, 'simple_01', 'Simple TNS name' ],
-                       [ true, 'simple_01.world', 'TNS name with domain' ],
-                       [ true, 'simple_01.domain.net', 'TNS name with domain' ],
-                       [ true, 'host123', 'Host only' ],
-                       [ true, 'host123.domain.net', 'FQDN only' ],
-                       [ true, '//host123.domain.net', 'FQDN URL only' ],
-                       [ true, '123.223.213.132', 'Host IP only' ],
-                       [ true, 'host:1521', 'Host and port' ],
-                       [ true, 'host:1521/service', 'Host, port and service' ],
-                       [ true, 'host:1521/service:shared', 'Host, port, service and shared server type' ],
-                       [ true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ],
-                       [ true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ],
-                       [
-                               true,
-                               'host:1521/service:shared/instance1',
-                               'Host, port, service, server type and instance'
-                       ],
-                       [ true, 'host:1521//instance1', 'Host, port and instance' ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php b/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php
deleted file mode 100644 (file)
index 0a13de1..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-
-use MediaWiki\Interwiki\InterwikiLookupAdapter;
-
-/**
- * @covers MediaWiki\Interwiki\InterwikiLookupAdapter
- *
- * @group MediaWiki
- * @group Interwiki
- */
-class InterwikiLookupAdapterTest extends MediaWikiTestCase {
-
-       /**
-        * @var InterwikiLookupAdapter
-        */
-       private $interwikiLookup;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->interwikiLookup = new InterwikiLookupAdapter(
-                       $this->getSiteLookup( $this->getSites() )
-               );
-       }
-
-       public function testIsValidInterwiki() {
-               $this->assertTrue(
-                       $this->interwikiLookup->isValidInterwiki( 'enwt' ),
-                       'enwt known prefix is valid'
-               );
-               $this->assertTrue(
-                       $this->interwikiLookup->isValidInterwiki( 'foo' ),
-                       'foo site known prefix is valid'
-               );
-               $this->assertFalse(
-                       $this->interwikiLookup->isValidInterwiki( 'xyz' ),
-                       'unknown prefix is not valid'
-               );
-       }
-
-       public function testFetch() {
-               $interwiki = $this->interwikiLookup->fetch( '' );
-               $this->assertNull( $interwiki );
-
-               $interwiki = $this->interwikiLookup->fetch( 'xyz' );
-               $this->assertFalse( $interwiki );
-
-               $interwiki = $this->interwikiLookup->fetch( 'foo' );
-               $this->assertInstanceOf( Interwiki::class, $interwiki );
-               $this->assertSame( 'foobar', $interwiki->getWikiID() );
-
-               $interwiki = $this->interwikiLookup->fetch( 'enwt' );
-               $this->assertInstanceOf( Interwiki::class, $interwiki );
-
-               $this->assertSame( 'https://en.wiktionary.org/wiki/$1', $interwiki->getURL(), 'getURL' );
-               $this->assertSame( 'https://en.wiktionary.org/w/api.php', $interwiki->getAPI(), 'getAPI' );
-               $this->assertSame( 'enwiktionary', $interwiki->getWikiID(), 'getWikiID' );
-               $this->assertTrue( $interwiki->isLocal(), 'isLocal' );
-       }
-
-       public function testGetAllPrefixes() {
-               $foo = [
-                       'iw_prefix' => 'foo',
-                       'iw_url' => '',
-                       'iw_api' => '',
-                       'iw_wikiid' => 'foobar',
-                       'iw_local' => false,
-                       'iw_trans' => false,
-               ];
-               $enwt = [
-                       'iw_prefix' => 'enwt',
-                       'iw_url' => 'https://en.wiktionary.org/wiki/$1',
-                       'iw_api' => 'https://en.wiktionary.org/w/api.php',
-                       'iw_wikiid' => 'enwiktionary',
-                       'iw_local' => true,
-                       'iw_trans' => false,
-               ];
-
-               $this->assertEquals(
-                       [ $foo, $enwt ],
-                       $this->interwikiLookup->getAllPrefixes(),
-                       'getAllPrefixes()'
-               );
-
-               $this->assertEquals(
-                       [ $foo ],
-                       $this->interwikiLookup->getAllPrefixes( false ),
-                       'get external prefixes'
-               );
-
-               $this->assertEquals(
-                       [ $enwt ],
-                       $this->interwikiLookup->getAllPrefixes( true ),
-                       'get local prefixes'
-               );
-       }
-
-       private function getSiteLookup( SiteList $sites ) {
-               $siteLookup = $this->getMockBuilder( SiteLookup::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $siteLookup->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( $sites ) );
-
-               return $siteLookup;
-       }
-
-       private function getSites() {
-               $sites = [];
-
-               $site = new Site();
-               $site->setGlobalId( 'foobar' );
-               $site->addInterwikiId( 'foo' );
-               $site->setSource( 'external' );
-               $sites[] = $site;
-
-               $site = new MediaWikiSite();
-               $site->setGlobalId( 'enwiktionary' );
-               $site->setGroup( 'wiktionary' );
-               $site->setLanguageCode( 'en' );
-               $site->addNavigationId( 'enwiktionary' );
-               $site->addInterwikiId( 'enwt' );
-               $site->setSource( 'local' );
-               $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
-               $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
-               $sites[] = $site;
-
-               return new SiteList( $sites );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php
deleted file mode 100644 (file)
index 550ec0b..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-class ReplicatedBagOStuffTest extends MediaWikiTestCase {
-       /** @var HashBagOStuff */
-       private $writeCache;
-       /** @var HashBagOStuff */
-       private $readCache;
-       /** @var ReplicatedBagOStuff */
-       private $cache;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->writeCache = new HashBagOStuff();
-               $this->readCache = new HashBagOStuff();
-               $this->cache = new ReplicatedBagOStuff( [
-                       'writeFactory' => $this->writeCache,
-                       'readFactory' => $this->readCache,
-               ] );
-       }
-
-       /**
-        * @covers ReplicatedBagOStuff::set
-        */
-       public function testSet() {
-               $key = 'a key';
-               $value = 'a value';
-               $this->cache->set( $key, $value );
-
-               // Write to master.
-               $this->assertEquals( $value, $this->writeCache->get( $key ) );
-               // Don't write to replica. Replication is deferred to backend.
-               $this->assertFalse( $this->readCache->get( $key ) );
-       }
-
-       /**
-        * @covers ReplicatedBagOStuff::get
-        */
-       public function testGet() {
-               $key = 'a key';
-
-               $write = 'one value';
-               $this->writeCache->set( $key, $write );
-               $read = 'another value';
-               $this->readCache->set( $key, $read );
-
-               // Read from replica.
-               $this->assertEquals( $read, $this->cache->get( $key ) );
-       }
-
-       /**
-        * @covers ReplicatedBagOStuff::get
-        */
-       public function testGetAbsent() {
-               $key = 'a key';
-               $value = 'a value';
-               $this->writeCache->set( $key, $value );
-
-               // Don't read from master. No failover if value is absent.
-               $this->assertFalse( $this->cache->get( $key ) );
-       }
-}
diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php
deleted file mode 100644 (file)
index 4b3ba07..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-/**
- * @group Media
- */
-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( [ '¼' ], $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( [ 'ÃÃø' ], $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( [ 'ø' ], $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( [ '¼' ], $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( [ '¼', '¼½' ], $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( [ '¼' ], $res['Keywords'] );
-       }
-}
diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php
deleted file mode 100644 (file)
index 7a052f6..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * @group Media
- */
-class MediaHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @covers MediaHandler::fitBoxWidth
-        *
-        * @dataProvider provideTestFitBoxWidth
-        */
-       public function testFitBoxWidth( $width, $height, $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: {z$result}x$y2" );
-       }
-
-       public static function provideTestFitBoxWidth() {
-               return array_merge(
-                       static::generateTestFitBoxWidthData( 50, 50, [
-                                       50 => 50,
-                                       17 => 17,
-                                       18 => 18 ]
-                       ),
-                       static::generateTestFitBoxWidthData( 366, 300, [
-                                       50 => 61,
-                                       17 => 21,
-                                       18 => 22 ]
-                       ),
-                       static::generateTestFitBoxWidthData( 300, 366, [
-                                       50 => 41,
-                                       17 => 14,
-                                       18 => 15 ]
-                       ),
-                       static::generateTestFitBoxWidthData( 100, 400, [
-                                       50 => 12,
-                                       17 => 4,
-                                       18 => 4 ]
-                       )
-               );
-       }
-
-       /**
-        * Generate single test cases by combining the dimensions and tests contents
-        *
-        * It creates:
-        * [$width, $height, $max, $expected],
-        * [$width, $height, $max2, $expected2], ...
-        * out of parameters:
-        * $width, $height, { $max => $expected, $max2 => $expected2, ... }
-        *
-        * @param int $width
-        * @param int $height
-        * @param array $tests associative array of $max => $expected values
-        * @return array
-        */
-       private static function generateTestFitBoxWidthData( $width, $height, $tests ) {
-               $result = [];
-               foreach ( $tests as $max => $expected ) {
-                       $result[] = [ $width, $height, $max, $expected ];
-               }
-               return $result;
-       }
-}
diff --git a/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php b/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php
deleted file mode 100644 (file)
index 45971da..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-/**
- * @group BagOStuff
- */
-class MemcachedBagOStuffTest extends MediaWikiTestCase {
-       /** @var MemcachedBagOStuff */
-       private $cache;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->cache = new MemcachedPhpBagOStuff( [ 'keyspace' => 'test', 'servers' => [] ] );
-       }
-
-       /**
-        * @covers MemcachedBagOStuff::makeKey
-        */
-       public function testKeyNormalization() {
-               $this->assertEquals(
-                       'test:vanilla',
-                       $this->cache->makeKey( 'vanilla' )
-               );
-
-               $this->assertEquals(
-                       'test:punctuation_marks_are_ok:!@$^&*()',
-                       $this->cache->makeKey( 'punctuation_marks_are_ok', '!@$^&*()' )
-               );
-
-               $this->assertEquals(
-                       'test:but_spaces:hashes%23:and%0Anewlines:are_not',
-                       $this->cache->makeKey( 'but spaces', 'hashes#', "and\nnewlines", 'are_not' )
-               );
-
-               $this->assertEquals(
-                       'test:this:key:contains:%F0%9D%95%9E%F0%9D%95%A6%F0%9D%95%9D%F0%9D%95%A5%F0%9' .
-                               'D%95%9A%F0%9D%95%93%F0%9D%95%AA%F0%9D%95%A5%F0%9D%95%96:characters',
-                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖', 'characters' )
-               );
-
-               $this->assertEquals(
-                       'test:this:key:contains:#c118f92685a635cb843039de50014c9c',
-                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕥𝕠𝕠 𝕞𝕒𝕟𝕪 𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖 𝕔𝕙𝕒𝕣𝕒𝕔𝕥𝕖𝕣𝕤' )
-               );
-
-               $this->assertEquals(
-                       'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5',
-                       $this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙',
-                               '𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' )
-               );
-
-               $this->assertEquals(
-                       'test:%23%235820ad1d105aa4dc698585c39df73e19',
-                       $this->cache->makeKey( '##5820ad1d105aa4dc698585c39df73e19' )
-               );
-
-               $this->assertEquals(
-                       'test:percent_is_escaped:!@$%25^&*()',
-                       $this->cache->makeKey( 'percent_is_escaped', '!@$%^&*()' )
-               );
-
-               $this->assertEquals(
-                       'test:colon_is_escaped:!@$%3A^&*()',
-                       $this->cache->makeKey( 'colon_is_escaped', '!@$:^&*()' )
-               );
-
-               $this->assertEquals(
-                       'test:long_key_part_hashed:#0244f7b1811d982dd932dd7de01465ac',
-                       $this->cache->makeKey( 'long_key_part_hashed', str_repeat( 'y', 500 ) )
-               );
-       }
-
-       /**
-        * @dataProvider validKeyProvider
-        * @covers MemcachedBagOStuff::validateKeyEncoding
-        */
-       public function testValidateKeyEncoding( $key ) {
-               $this->assertSame( $key, $this->cache->validateKeyEncoding( $key ) );
-       }
-
-       public function validKeyProvider() {
-               return [
-                       'empty' => [ '' ],
-                       'digits' => [ '09' ],
-                       'letters' => [ 'AZaz' ],
-                       'ASCII special characters' => [ '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ],
-               ];
-       }
-
-       /**
-        * @dataProvider invalidKeyProvider
-        * @covers MemcachedBagOStuff::validateKeyEncoding
-        */
-       public function testValidateKeyEncodingThrowsException( $key ) {
-               $this->setExpectedException( Exception::class );
-               $this->cache->validateKeyEncoding( $key );
-       }
-
-       public function invalidKeyProvider() {
-               return [
-                       [ "\x00" ],
-                       [ ' ' ],
-                       [ "\x1F" ],
-                       [ "\x7F" ],
-                       [ "\x80" ],
-                       [ "\xFF" ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php b/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php
deleted file mode 100644 (file)
index dfbca70..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-/**
- * @group BagOStuff
- *
- * @covers RESTBagOStuff
- */
-class RESTBagOStuffTest extends MediaWikiTestCase {
-
-       /**
-        * @var MultiHttpClient
-        */
-       private $client;
-       /**
-        * @var RESTBagOStuff
-        */
-       private $bag;
-
-       public function setUp() {
-               parent::setUp();
-               $this->client =
-                       $this->getMockBuilder( MultiHttpClient::class )
-                               ->setConstructorArgs( [ [] ] )
-                               ->setMethods( [ 'run' ] )
-                               ->getMock();
-               $this->bag = new RESTBagOStuff( [ 'client' => $this->client, 'url' => 'http://test/rest/' ] );
-       }
-
-       public function testGet() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 200, 'OK', [], '"somedata"', 0 ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertEquals( 'somedata', $result );
-       }
-
-       public function testGetNotExist() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 404, 'Not found', [], 'Nothing to see here', 0 ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertFalse( $result );
-       }
-
-       public function testGetBadClient() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 0, '', [], '', 'cURL has failed you today' ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertFalse( $result );
-               $this->assertEquals( BagOStuff::ERR_UNREACHABLE, $this->bag->getLastError() );
-       }
-
-       public function testGetBadServer() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 500, 'Too busy', [], 'Server is too busy', '' ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertFalse( $result );
-               $this->assertEquals( BagOStuff::ERR_UNEXPECTED, $this->bag->getLastError() );
-       }
-
-       public function testPut() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'PUT',
-                       'url' => 'http://test/rest/42xyz42',
-                       'body' => '"postdata"',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
-               $result = $this->bag->set( '42xyz42', 'postdata' );
-               $this->assertTrue( $result );
-       }
-
-       public function testDelete() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'DELETE',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
-               $result = $this->bag->delete( '42xyz42' );
-               $this->assertTrue( $result );
-       }
-}
index 34b2525..5c5ce5c 100644 (file)
@@ -4,6 +4,7 @@
  * @group Database
  */
 class ArticleTablesTest extends MediaWikiLangTestCase {
+
        /**
         * Make sure that T16404 doesn't strike again. We don't want
         * templatelinks based on the user language when {{int:}} is used, only the
@@ -16,7 +17,7 @@ class ArticleTablesTest extends MediaWikiLangTestCase {
                $title = Title::newFromText( 'T16404' );
                $page = WikiPage::factory( $title );
                $user = new User();
-               $user->mRights = [ 'createpage', 'edit', 'purge' ];
+               $this->overrideUserPermissions( $user, [ 'createpage', 'edit', 'purge' ] );
                $this->setContentLang( 'es' );
                $this->setUserLang( 'fr' );
 
index 3a3feee..ee6c227 100644 (file)
@@ -1374,6 +1374,7 @@ more stuff
 
                // Now, try the rollback
                $admin->addGroup( 'sysop' ); // Make the test user a sysop
+               MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
                $token = $admin->getEditToken( 'rollback' );
                $errors = $page->doRollback(
                        $secondUser->getName(),
index 812702b..34ddb1f 100644 (file)
@@ -877,7 +877,7 @@ EOF
 
                $bClocks = $b->mParseStartTime;
 
-               $a->mergeInternalMetaDataFrom( $b->object, 'b' );
+               $a->mergeInternalMetaDataFrom( $b->object );
                $mergedClocks = $a->mParseStartTime;
 
                foreach ( $mergedClocks as $clock => $timestamp ) {
@@ -890,7 +890,7 @@ EOF
                $a->resetParseStartTime();
                $aClocks = $a->mParseStartTime;
 
-               $a->mergeInternalMetaDataFrom( $b->object, 'b' );
+               $a->mergeInternalMetaDataFrom( $b->object );
                $mergedClocks = $a->mParseStartTime;
 
                foreach ( $mergedClocks as $clock => $timestamp ) {
@@ -902,7 +902,7 @@ EOF
                $a = new ParserOutput();
                $a = TestingAccessWrapper::newFromObject( $a );
 
-               $a->mergeInternalMetaDataFrom( $b->object, 'b' );
+               $a->mergeInternalMetaDataFrom( $b->object );
                $mergedClocks = $a->mParseStartTime;
 
                foreach ( $mergedClocks as $clock => $timestamp ) {
diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php
deleted file mode 100644 (file)
index 898ef2d..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * @group Parser
- * @covers MWTidy
- */
-class TidyTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               parent::setUp();
-               if ( !MWTidy::isEnabled() ) {
-                       $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 static function provideTestWrapping() {
-               $testMathML = <<<'MathML'
-<math xmlns="http://www.w3.org/1998/Math/MathML">
-    <mrow>
-      <mi>a</mi>
-      <mo>&InvisibleTimes;</mo>
-      <msup>
-        <mi>x</mi>
-        <mn>2</mn>
-      </msup>
-      <mo>+</mo>
-      <mi>b</mi>
-      <mo>&InvisibleTimes; </mo>
-      <mi>x</mi>
-      <mo>+</mo>
-      <mi>c</mi>
-    </mrow>
-  </math>
-MathML;
-               return [
-                       [
-                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
-                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
-                               '<mw:editsection> should survive tidy'
-                       ],
-                       [
-                               '<editsection page="foo" section="bar">foo</editsection>',
-                               '<editsection page="foo" section="bar">foo</editsection>',
-                               '<editsection> should survive tidy'
-                       ],
-                       [ '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ],
-                       [ "<link foo=\"bar\" />foo", '<link foo="bar"/>foo', '<link> should survive tidy' ],
-                       [ "<meta foo=\"bar\" />foo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ],
-                       [ $testMathML, $testMathML, '<math> should survive tidy' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/password/PasswordTest.php b/tests/phpunit/includes/password/PasswordTest.php
deleted file mode 100644 (file)
index 61a5147..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-/**
- * Testing framework for the Password infrastructure
- *
- * 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 InvalidPassword
- */
-class PasswordTest extends MediaWikiTestCase {
-       public function testInvalidPlaintext() {
-               $passwordFactory = new PasswordFactory();
-               $invalid = $passwordFactory->newFromPlaintext( null );
-
-               $this->assertInstanceOf( InvalidPassword::class, $invalid );
-       }
-}
diff --git a/tests/phpunit/includes/preferences/FiltersTest.php b/tests/phpunit/includes/preferences/FiltersTest.php
deleted file mode 100644 (file)
index 60b01b8..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-/**
- * 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
- */
-
-use MediaWiki\Preferences\IntvalFilter;
-use MediaWiki\Preferences\MultiUsernameFilter;
-use MediaWiki\Preferences\TimezoneFilter;
-
-/**
- * @group Preferences
- */
-class FiltersTest extends MediaWikiTestCase {
-       /**
-        * @covers MediaWiki\Preferences\IntvalFilter::filterFromForm()
-        * @covers MediaWiki\Preferences\IntvalFilter::filterForForm()
-        */
-       public function testIntvalFilter() {
-               $filter = new IntvalFilter();
-               self::assertSame( 0, $filter->filterFromForm( '0' ) );
-               self::assertSame( 3, $filter->filterFromForm( '3' ) );
-               self::assertSame( '123', $filter->filterForForm( '123' ) );
-       }
-
-       /**
-        * @covers       MediaWiki\Preferences\TimezoneFilter::filterFromForm()
-        * @dataProvider provideTimezoneFilter
-        *
-        * @param string $input
-        * @param string $expected
-        */
-       public function testTimezoneFilter( $input, $expected ) {
-               $filter = new TimezoneFilter();
-               $result = $filter->filterFromForm( $input );
-               self::assertEquals( $expected, $result );
-       }
-
-       public function provideTimezoneFilter() {
-               return [
-                       [ 'ZoneInfo', 'Offset|0' ],
-                       [ 'ZoneInfo|bogus', 'Offset|0' ],
-                       [ 'System', 'System' ],
-                       [ '2:30', 'Offset|150' ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterFromForm()
-        * @dataProvider provideMultiUsernameFilterFrom
-        *
-        * @param string $input
-        * @param string|null $expected
-        */
-       public function testMultiUsernameFilterFrom( $input, $expected ) {
-               $filter = $this->makeMultiUsernameFilter();
-               $result = $filter->filterFromForm( $input );
-               self::assertSame( $expected, $result );
-       }
-
-       public function provideMultiUsernameFilterFrom() {
-               return [
-                       [ '', null ],
-                       [ "\n\n\n", null ],
-                       [ 'Foo', '1' ],
-                       [ "\n\n\nFoo\nBar\n", "1\n2" ],
-                       [ "Baz\nInvalid\nFoo", "3\n1" ],
-                       [ "Invalid", null ],
-                       [ "Invalid\n\n\nInvalid\n", null ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterForForm()
-        * @dataProvider provideMultiUsernameFilterFor
-        *
-        * @param string $input
-        * @param string $expected
-        */
-       public function testMultiUsernameFilterFor( $input, $expected ) {
-               $filter = $this->makeMultiUsernameFilter();
-               $result = $filter->filterForForm( $input );
-               self::assertSame( $expected, $result );
-       }
-
-       public function provideMultiUsernameFilterFor() {
-               return [
-                       [ '', '' ],
-                       [ "\n", '' ],
-                       [ '1', 'Foo' ],
-                       [ "\n1\n\n2\377\n", "Foo\nBar" ],
-                       [ "666\n667", '' ],
-               ];
-       }
-
-       private function makeMultiUsernameFilter() {
-               $userMapping = [
-                       'Foo' => 1,
-                       'Bar' => 2,
-                       'Baz' => 3,
-               ];
-               $flipped = array_flip( $userMapping );
-               $idLookup = self::getMockBuilder( CentralIdLookup::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'centralIdsFromNames', 'namesFromCentralIds' ] )
-                       ->getMockForAbstractClass();
-
-               $idLookup->method( 'centralIdsFromNames' )
-                       ->will( self::returnCallback( function ( $names ) use ( $userMapping ) {
-                               $ids = [];
-                               foreach ( $names as $name ) {
-                                       $ids[] = $userMapping[$name] ?? null;
-                               }
-                               return array_filter( $ids, 'is_numeric' );
-                       } ) );
-               $idLookup->method( 'namesFromCentralIds' )
-                       ->will( self::returnCallback( function ( $ids ) use ( $flipped ) {
-                               $names = [];
-                               foreach ( $ids as $id ) {
-                                       $names[] = $flipped[$id] ?? null;
-                               }
-                               return array_filter( $names, 'is_string' );
-                       } ) );
-
-               return new MultiUsernameFilter( $idLookup );
-       }
-}
diff --git a/tests/phpunit/includes/registration/ExtensionProcessorTest.php b/tests/phpunit/includes/registration/ExtensionProcessorTest.php
deleted file mode 100644 (file)
index cdd5c63..0000000
+++ /dev/null
@@ -1,829 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers ExtensionProcessor
- */
-class ExtensionProcessorTest extends MediaWikiTestCase {
-
-       private $dir, $dirname;
-
-       public function setUp() {
-               parent::setUp();
-               $this->dir = __DIR__ . '/FooBar/extension.json';
-               $this->dirname = dirname( $this->dir );
-       }
-
-       /**
-        * 'name' is absolutely required
-        *
-        * @var array
-        */
-       public static $default = [
-               'name' => 'FooBar',
-       ];
-
-       public function testExtractInfo() {
-               // Test that attributes that begin with @ are ignored
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, self::$default + [
-                       '@metadata' => [ 'foobarbaz' ],
-                       'AnAttribute' => [ 'omg' ],
-                       'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
-                       'SpecialPages' => [ 'Foo' => 'SpecialFoo' ],
-                       'callback' => 'FooBar::onRegistration',
-               ], 1 );
-
-               $extracted = $processor->getExtractedInfo();
-               $attributes = $extracted['attributes'];
-               $this->assertArrayHasKey( 'AnAttribute', $attributes );
-               $this->assertArrayNotHasKey( '@metadata', $attributes );
-               $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
-               $this->assertSame(
-                       [ 'FooBar' => 'FooBar::onRegistration' ],
-                       $extracted['callbacks']
-               );
-               $this->assertSame(
-                       [ 'Foo' => 'SpecialFoo' ],
-                       $extracted['globals']['wgSpecialPages']
-               );
-       }
-
-       public function testExtractNamespaces() {
-               // Test that namespace IDs can be overwritten
-               if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
-                       define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
-               }
-
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, self::$default + [
-                       'namespaces' => [
-                               [
-                                       'id' => 332200,
-                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
-                                       'name' => 'Test_A',
-                                       'defaultcontentmodel' => 'TestModel',
-                                       'gender' => [
-                                               'male' => 'Male test',
-                                               'female' => 'Female test',
-                                       ],
-                                       'subpages' => true,
-                                       'content' => true,
-                                       'protection' => 'userright',
-                               ],
-                               [ // Test_X will use ID 123456 not 334400
-                                       'id' => 334400,
-                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
-                                       'name' => 'Test_X',
-                                       'defaultcontentmodel' => 'TestModel'
-                               ],
-                       ]
-               ], 1 );
-
-               $extracted = $processor->getExtractedInfo();
-
-               $this->assertArrayHasKey(
-                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
-                       $extracted['defines']
-               );
-               $this->assertArrayNotHasKey(
-                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
-                       $extracted['defines']
-               );
-
-               $this->assertSame(
-                       $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
-                       332200
-               );
-
-               $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
-               $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
-               $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
-               $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
-
-               $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
-               $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
-               $this->assertSame(
-                       [ 'male' => 'Male test', 'female' => 'Female test' ],
-                       $extracted['globals']['wgExtraGenderNamespaces'][332200]
-               );
-               // A has subpages, X does not
-               $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] );
-               $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] );
-       }
-
-       public static function provideRegisterHooks() {
-               $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
-               // Format:
-               // Current $wgHooks
-               // Content in extension.json
-               // Expected value of $wgHooks
-               return [
-                       // No hooks
-                       [
-                               [],
-                               self::$default,
-                               $merge,
-                       ],
-                       // No current hooks, adding one for "FooBaz" in string format
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
-                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
-                       ],
-                       // Hook for "FooBaz", adding another one
-                       [
-                               [ 'FooBaz' => [ 'PriorCallback' ] ],
-                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
-                               [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
-                       ],
-                       // No current hooks, adding one for "FooBaz" in verbose array format
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
-                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
-                       ],
-                       // Hook for "BarBaz", adding one for "FooBaz"
-                       [
-                               [ 'BarBaz' => [ 'BarBazCallback' ] ],
-                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
-                               [
-                                       'BarBaz' => [ 'BarBazCallback' ],
-                                       'FooBaz' => [ 'FooBazCallback' ],
-                               ] + $merge,
-                       ],
-                       // Callbacks for FooBaz wrapped in an array
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
-                               [
-                                       'FooBaz' => [ 'Callback1' ],
-                               ] + $merge,
-                       ],
-                       // Multiple callbacks for FooBaz hook
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
-                               [
-                                       'FooBaz' => [ 'Callback1', 'Callback2' ],
-                               ] + $merge,
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideRegisterHooks
-        */
-       public function testRegisterHooks( $pre, $info, $expected ) {
-               $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
-               $processor->extractInfo( $this->dir, $info, 1 );
-               $extracted = $processor->getExtractedInfo();
-               $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
-       }
-
-       public function testExtractConfig1() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => 'somevalue',
-                               'Foo' => 10,
-                               '@IGNORED' => 'yes',
-                       ],
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               '_prefix' => 'eg',
-                               'Bar' => 'somevalue'
-                       ],
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 1 );
-               $processor->extractInfo( $this->dir, $info2, 1 );
-               $extracted = $processor->getExtractedInfo();
-               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
-               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
-               $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
-               // Custom prefix:
-               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
-       }
-
-       public function testExtractConfig2() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                               'Foo' => [ 'value' => 10 ],
-                               'Path' => [ 'value' => 'foo.txt', 'path' => true ],
-                               'Namespaces' => [
-                                       'value' => [
-                                               '10' => true,
-                                               '12' => false,
-                                       ],
-                                       'merge_strategy' => 'array_plus',
-                               ],
-                       ],
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                       ],
-                       'config_prefix' => 'eg',
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 2 );
-               $processor->extractInfo( $this->dir, $info2, 2 );
-               $extracted = $processor->getExtractedInfo();
-               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
-               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
-               $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
-               // Custom prefix:
-               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
-               $this->assertSame(
-                       [ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ],
-                       $extracted['globals']['wgNamespaces']
-               );
-       }
-
-       /**
-        * @expectedException RuntimeException
-        */
-       public function testDuplicateConfigKey1() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => '',
-                       ]
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               'Bar' => 'g',
-                       ],
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 1 );
-               $processor->extractInfo( $this->dir, $info2, 1 );
-       }
-
-       /**
-        * @expectedException RuntimeException
-        */
-       public function testDuplicateConfigKey2() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                       ]
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                       ],
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 2 );
-               $processor->extractInfo( $this->dir, $info2, 2 );
-       }
-
-       public static function provideExtractExtensionMessagesFiles() {
-               $dir = __DIR__ . '/FooBar/';
-               return [
-                       [
-                               [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
-                               [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
-                       ],
-                       [
-                               [
-                                       'ExtensionMessagesFiles' => [
-                                               'FooBarAlias' => 'FooBar.alias.php',
-                                               'FooBarMagic' => 'FooBar.magic.i18n.php',
-                                       ],
-                               ],
-                               [
-                                       'wgExtensionMessagesFiles' => [
-                                               'FooBarAlias' => $dir . 'FooBar.alias.php',
-                                               'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
-                                       ],
-                               ],
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideExtractExtensionMessagesFiles
-        */
-       public function testExtractExtensionMessagesFiles( $input, $expected ) {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
-               $out = $processor->getExtractedInfo();
-               foreach ( $expected as $key => $value ) {
-                       $this->assertEquals( $value, $out['globals'][$key] );
-               }
-       }
-
-       public static function provideExtractMessagesDirs() {
-               $dir = __DIR__ . '/FooBar/';
-               return [
-                       [
-                               [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
-                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
-                       ],
-                       [
-                               [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
-                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideExtractMessagesDirs
-        */
-       public function testExtractMessagesDirs( $input, $expected ) {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
-               $out = $processor->getExtractedInfo();
-               foreach ( $expected as $key => $value ) {
-                       $this->assertEquals( $value, $out['globals'][$key] );
-               }
-       }
-
-       public function testExtractCredits() {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, self::$default, 1 );
-               $this->setExpectedException( Exception::class );
-               $processor->extractInfo( $this->dir, self::$default, 1 );
-       }
-
-       /**
-        * @dataProvider provideExtractResourceLoaderModules
-        */
-       public function testExtractResourceLoaderModules(
-               $input,
-               array $expectedGlobals,
-               array $expectedAttribs = []
-       ) {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
-               $out = $processor->getExtractedInfo();
-               foreach ( $expectedGlobals as $key => $value ) {
-                       $this->assertEquals( $value, $out['globals'][$key] );
-               }
-               foreach ( $expectedAttribs as $key => $value ) {
-                       $this->assertEquals( $value, $out['attributes'][$key] );
-               }
-       }
-
-       public static function provideExtractResourceLoaderModules() {
-               $dir = __DIR__ . '/FooBar';
-               return [
-                       // Generic module with localBasePath/remoteExtPath specified
-                       [
-                               // Input
-                               [
-                                       'ResourceModules' => [
-                                               'test.foo' => [
-                                                       'styles' => 'foobar.js',
-                                                       'localBasePath' => '',
-                                                       'remoteExtPath' => 'FooBar',
-                                               ],
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModules' => [
-                                               'test.foo' => [
-                                                       'styles' => 'foobar.js',
-                                                       'localBasePath' => $dir,
-                                                       'remoteExtPath' => 'FooBar',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       // ResourceFileModulePaths specified:
-                       [
-                               // Input
-                               [
-                                       'ResourceFileModulePaths' => [
-                                               'localBasePath' => 'modules',
-                                               'remoteExtPath' => 'FooBar/modules',
-                                       ],
-                                       'ResourceModules' => [
-                                               // No paths
-                                               'test.foo' => [
-                                                       'styles' => 'foo.js',
-                                               ],
-                                               // Different paths set
-                                               'test.bar' => [
-                                                       'styles' => 'bar.js',
-                                                       'localBasePath' => 'subdir',
-                                                       'remoteExtPath' => 'FooBar/subdir',
-                                               ],
-                                               // Custom class with no paths set
-                                               'test.class' => [
-                                                       'class' => 'FooBarModule',
-                                                       'extra' => 'argument',
-                                               ],
-                                               // Custom class with a localBasePath
-                                               'test.class.with.path' => [
-                                                       'class' => 'FooBarPathModule',
-                                                       'extra' => 'argument',
-                                                       'localBasePath' => '',
-                                               ]
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModules' => [
-                                               'test.foo' => [
-                                                       'styles' => 'foo.js',
-                                                       'localBasePath' => "$dir/modules",
-                                                       'remoteExtPath' => 'FooBar/modules',
-                                               ],
-                                               'test.bar' => [
-                                                       'styles' => 'bar.js',
-                                                       'localBasePath' => "$dir/subdir",
-                                                       'remoteExtPath' => 'FooBar/subdir',
-                                               ],
-                                               'test.class' => [
-                                                       'class' => 'FooBarModule',
-                                                       'extra' => 'argument',
-                                                       'localBasePath' => "$dir/modules",
-                                                       'remoteExtPath' => 'FooBar/modules',
-                                               ],
-                                               'test.class.with.path' => [
-                                                       'class' => 'FooBarPathModule',
-                                                       'extra' => 'argument',
-                                                       'localBasePath' => $dir,
-                                                       'remoteExtPath' => 'FooBar/modules',
-                                               ]
-                                       ],
-                               ],
-                       ],
-                       // ResourceModuleSkinStyles with file module paths
-                       [
-                               // Input
-                               [
-                                       'ResourceFileModulePaths' => [
-                                               'localBasePath' => '',
-                                               'remoteSkinPath' => 'FooBar',
-                                       ],
-                                       'ResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                               ]
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                                       'localBasePath' => $dir,
-                                                       'remoteSkinPath' => 'FooBar',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       // ResourceModuleSkinStyles with file module paths and an override
-                       [
-                               // Input
-                               [
-                                       'ResourceFileModulePaths' => [
-                                               'localBasePath' => '',
-                                               'remoteSkinPath' => 'FooBar',
-                                       ],
-                                       'ResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                                       'remoteSkinPath' => 'BarFoo'
-                                               ],
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                                       'localBasePath' => $dir,
-                                                       'remoteSkinPath' => 'BarFoo',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       'QUnit test module' => [
-                               // Input
-                               [
-                                       'QUnitTestModule' => [
-                                               'localBasePath' => '',
-                                               'remoteExtPath' => 'Foo',
-                                               'scripts' => 'bar.js',
-                                       ],
-                               ],
-                               // Expected
-                               [],
-                               [
-                                       'QUnitTestModules' => [
-                                               'test.FooBar' => [
-                                                       'localBasePath' => $dir,
-                                                       'remoteExtPath' => 'Foo',
-                                                       'scripts' => 'bar.js',
-                                               ],
-                                       ],
-                               ],
-                       ],
-               ];
-       }
-
-       public static function provideSetToGlobal() {
-               return [
-                       [
-                               [ 'wgAPIModules', 'wgAvailableRights' ],
-                               [],
-                               [
-                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
-                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
-                               ],
-                               [
-                                       'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
-                                       'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
-                               ],
-                       ],
-                       [
-                               [ 'wgAPIModules', 'wgAvailableRights' ],
-                               [
-                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
-                                       'wgAvailableRights' => [ 'barbaz' ]
-                               ],
-                               [
-                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
-                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
-                               ],
-                               [
-                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
-                                       'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
-                               ],
-                       ],
-                       [
-                               [ 'wgGroupPermissions' ],
-                               [
-                                       'wgGroupPermissions' => [
-                                               'sysop' => [ 'delete' ]
-                                       ],
-                               ],
-                               [
-                                       'GroupPermissions' => [
-                                               'sysop' => [ 'undelete' ],
-                                               'user' => [ 'edit' ]
-                                       ],
-                               ],
-                               [
-                                       'wgGroupPermissions' => [
-                                               'sysop' => [ 'delete', 'undelete' ],
-                                               'user' => [ 'edit' ]
-                                       ],
-                               ]
-                       ]
-               ];
-       }
-
-       /**
-        * Attributes under manifest_version 2
-        */
-       public function testExtractAttributes() {
-               $processor = new ExtensionProcessor();
-               // Load FooBar extension
-               $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'name' => 'Baz',
-                               'attributes' => [
-                                       // Loaded
-                                       'FooBar' => [
-                                               'Plugins' => [
-                                                       'ext.baz.foobar',
-                                               ],
-                                       ],
-                                       // Not loaded
-                                       'FizzBuzz' => [
-                                               'MorePlugins' => [
-                                                       'ext.baz.fizzbuzz',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       2
-               );
-
-               $info = $processor->getExtractedInfo();
-               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
-               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
-               $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
-       }
-
-       /**
-        * Attributes under manifest_version 1
-        */
-       public function testAttributes1() {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'name' => 'FooBar',
-                               'FooBarPlugins' => [
-                                       'ext.baz.foobar',
-                               ],
-                               'FizzBuzzMorePlugins' => [
-                                       'ext.baz.fizzbuzz',
-                               ],
-                       ],
-                       1
-               );
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'name' => 'FooBar2',
-                               'FizzBuzzMorePlugins' => [
-                                       'ext.bar.fizzbuzz',
-                               ]
-                       ],
-                       1
-               );
-
-               $info = $processor->getExtractedInfo();
-               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
-               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
-               $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
-               $this->assertSame(
-                       [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ],
-                       $info['attributes']['FizzBuzzMorePlugins']
-               );
-       }
-
-       public function testAttributes1_notarray() {
-               $processor = new ExtensionProcessor();
-               $this->setExpectedException(
-                       InvalidArgumentException::class,
-                       "The value for 'FooBarPlugins' should be an array (from {$this->dir})"
-               );
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'FooBarPlugins' => 'ext.baz.foobar',
-                       ] + self::$default,
-                       1
-               );
-       }
-
-       public function testExtractPathBasedGlobal() {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'ParserTestFiles' => [
-                                       'tests/parserTests.txt',
-                                       'tests/extraParserTests.txt',
-                               ],
-                               'ServiceWiringFiles' => [
-                                       'includes/ServiceWiring.php'
-                               ],
-                       ] + self::$default,
-                       1
-               );
-               $globals = $processor->getExtractedInfo()['globals'];
-               $this->assertArrayHasKey( 'wgParserTestFiles', $globals );
-               $this->assertSame( [
-                       "{$this->dirname}/tests/parserTests.txt",
-                       "{$this->dirname}/tests/extraParserTests.txt"
-               ], $globals['wgParserTestFiles'] );
-               $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals );
-               $this->assertSame( [
-                       "{$this->dirname}/includes/ServiceWiring.php"
-               ], $globals['wgServiceWiringFiles'] );
-       }
-
-       public function testGetRequirements() {
-               $info = self::$default + [
-                       'requires' => [
-                               'MediaWiki' => '>= 1.25.0',
-                               'platform' => [
-                                       'php' => '>= 5.5.9'
-                               ],
-                               'extensions' => [
-                                       'Bar' => '*'
-                               ]
-                       ]
-               ];
-               $processor = new ExtensionProcessor();
-               $this->assertSame(
-                       $info['requires'],
-                       $processor->getRequirements( $info, false )
-               );
-               $this->assertSame(
-                       [],
-                       $processor->getRequirements( [], false )
-               );
-       }
-
-       public function testGetDevRequirements() {
-               $info = self::$default + [
-                       'dev-requires' => [
-                               'MediaWiki' => '>= 1.31.0',
-                               'platform' => [
-                                       'ext-foo' => '*',
-                               ],
-                               'skins' => [
-                                       'Baz' => '*',
-                               ],
-                               'extensions' => [
-                                       'Biz' => '*',
-                               ],
-                       ],
-               ];
-               $processor = new ExtensionProcessor();
-               $this->assertSame(
-                       $info['dev-requires'],
-                       $processor->getRequirements( $info, true )
-               );
-               // Set some standard requirements, so we can test merging
-               $info['requires'] = [
-                       'MediaWiki' => '>= 1.25.0',
-                       'platform' => [
-                               'php' => '>= 5.5.9'
-                       ],
-                       'extensions' => [
-                               'Bar' => '*'
-                       ]
-               ];
-               $this->assertSame(
-                       [
-                               'MediaWiki' => '>= 1.25.0 >= 1.31.0',
-                               'platform' => [
-                                       'php' => '>= 5.5.9',
-                                       'ext-foo' => '*',
-                               ],
-                               'extensions' => [
-                                       'Bar' => '*',
-                                       'Biz' => '*',
-                               ],
-                               'skins' => [
-                                       'Baz' => '*',
-                               ],
-                       ],
-                       $processor->getRequirements( $info, true )
-               );
-
-               // If there's no dev-requires, it just returns requires
-               unset( $info['dev-requires'] );
-               $this->assertSame(
-                       $info['requires'],
-                       $processor->getRequirements( $info, true )
-               );
-       }
-
-       public function testGetExtraAutoloaderPaths() {
-               $processor = new ExtensionProcessor();
-               $this->assertSame(
-                       [ "{$this->dirname}/vendor/autoload.php" ],
-                       $processor->getExtraAutoloaderPaths( $this->dirname, [
-                               'load_composer_autoloader' => true,
-                       ] )
-               );
-       }
-
-       /**
-        * Verify that extension.schema.json is in sync with ExtensionProcessor
-        *
-        * @coversNothing
-        */
-       public function testGlobalSettingsDocumentedInSchema() {
-               global $IP;
-               $globalSettings = TestingAccessWrapper::newFromClass(
-                       ExtensionProcessor::class )->globalSettings;
-
-               $version = ExtensionRegistry::MANIFEST_VERSION;
-               $schema = FormatJson::decode(
-                       file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
-                       true
-               );
-               $missing = [];
-               foreach ( $globalSettings as $global ) {
-                       if ( !isset( $schema['properties'][$global] ) ) {
-                               $missing[] = $global;
-                       }
-               }
-
-               $this->assertEquals( [], $missing,
-                       "The following global settings are not documented in docs/extension.schema.json" );
-       }
-}
-
-/**
- * Allow overriding the default value of $this->globals
- * so we can test merging
- */
-class MockExtensionProcessor extends ExtensionProcessor {
-       public function __construct( $globals = [] ) {
-               $this->globals = $globals + $this->globals;
-       }
-}
diff --git a/tests/phpunit/includes/search/SearchIndexFieldTest.php b/tests/phpunit/includes/search/SearchIndexFieldTest.php
deleted file mode 100644 (file)
index 8b4119e..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-/**
- * @group Search
- * @covers SearchIndexFieldDefinition
- */
-class SearchIndexFieldTest extends MediaWikiTestCase {
-
-       public function getMergeCases() {
-               return [
-                       [ 0, 'test', 0, 'test', true ],
-                       [ SearchIndexField::INDEX_TYPE_NESTED, 'test',
-                               SearchIndexField::INDEX_TYPE_NESTED, 'test', false ],
-                       [ 0, 'test', 0, 'test2', true ],
-                       [ 0, 'test', 1, 'test', false ],
-               ];
-       }
-
-       /**
-        * @dataProvider getMergeCases
-        * @param int $t1
-        * @param string $n1
-        * @param int $t2
-        * @param string $n2
-        * @param bool $result
-        */
-       public function testMerge( $t1, $n1, $t2, $n2, $result ) {
-               $field1 =
-                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
-                               ->setMethods( [ 'getMapping' ] )
-                               ->setConstructorArgs( [ $n1, $t1 ] )
-                               ->getMock();
-               $field2 =
-                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
-                               ->setMethods( [ 'getMapping' ] )
-                               ->setConstructorArgs( [ $n2, $t2 ] )
-                               ->getMock();
-
-               if ( $result ) {
-                       $this->assertNotFalse( $field1->merge( $field2 ) );
-               } else {
-                       $this->assertFalse( $field1->merge( $field2 ) );
-               }
-
-               $field1->setFlag( 0xFF );
-               $this->assertFalse( $field1->merge( $field2 ) );
-
-               $field1->setMergeCallback(
-                       function ( $a, $b ) {
-                               return "test";
-                       }
-               );
-               $this->assertEquals( "test", $field1->merge( $field2 ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/session/MetadataMergeExceptionTest.php b/tests/phpunit/includes/session/MetadataMergeExceptionTest.php
deleted file mode 100644 (file)
index 8cb4302..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\MetadataMergeException
- */
-class MetadataMergeExceptionTest extends MediaWikiTestCase {
-
-       public function testBasics() {
-               $data = [ 'foo' => 'bar' ];
-
-               $ex = new MetadataMergeException();
-               $this->assertInstanceOf( \UnexpectedValueException::class, $ex );
-               $this->assertSame( [], $ex->getContext() );
-
-               $ex2 = new MetadataMergeException( 'Message', 42, $ex, $data );
-               $this->assertSame( 'Message', $ex2->getMessage() );
-               $this->assertSame( 42, $ex2->getCode() );
-               $this->assertSame( $ex, $ex2->getPrevious() );
-               $this->assertSame( $data, $ex2->getContext() );
-
-               $ex->setContext( $data );
-               $this->assertSame( $data, $ex->getContext() );
-       }
-
-}
diff --git a/tests/phpunit/includes/session/SessionIdTest.php b/tests/phpunit/includes/session/SessionIdTest.php
deleted file mode 100644 (file)
index 2b06d97..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\SessionId
- */
-class SessionIdTest extends MediaWikiTestCase {
-
-       public function testEverything() {
-               $id = new SessionId( 'foo' );
-               $this->assertSame( 'foo', $id->getId() );
-               $this->assertSame( 'foo', (string)$id );
-               $id->setId( 'bar' );
-               $this->assertSame( 'bar', $id->getId() );
-               $this->assertSame( 'bar', (string)$id );
-       }
-
-}
diff --git a/tests/phpunit/includes/skins/SkinFactoryTest.php b/tests/phpunit/includes/skins/SkinFactoryTest.php
deleted file mode 100644 (file)
index 4289fd9..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-class SkinFactoryTest extends MediaWikiTestCase {
-
-       /**
-        * @covers SkinFactory::register
-        */
-       public function testRegister() {
-               $factory = new SkinFactory();
-               $factory->register( 'fallback', 'Fallback', function () {
-                       return new SkinFallback();
-               } );
-               $this->assertTrue( true ); // No exception thrown
-               $this->setExpectedException( InvalidArgumentException::class );
-               $factory->register( 'invalid', 'Invalid', 'Invalid callback' );
-       }
-
-       /**
-        * @covers SkinFactory::makeSkin
-        */
-       public function testMakeSkinWithNoBuilders() {
-               $factory = new SkinFactory();
-               $this->setExpectedException( SkinException::class );
-               $factory->makeSkin( 'nobuilderregistered' );
-       }
-
-       /**
-        * @covers SkinFactory::makeSkin
-        */
-       public function testMakeSkinWithInvalidCallback() {
-               $factory = new SkinFactory();
-               $factory->register( 'unittest', 'Unittest', function () {
-                       return true; // Not a Skin object
-               } );
-               $this->setExpectedException( UnexpectedValueException::class );
-               $factory->makeSkin( 'unittest' );
-       }
-
-       /**
-        * @covers SkinFactory::makeSkin
-        */
-       public function testMakeSkinWithValidCallback() {
-               $factory = new SkinFactory();
-               $factory->register( 'testfallback', 'TestFallback', function () {
-                       return new SkinFallback();
-               } );
-
-               $skin = $factory->makeSkin( 'testfallback' );
-               $this->assertInstanceOf( Skin::class, $skin );
-               $this->assertInstanceOf( SkinFallback::class, $skin );
-               $this->assertEquals( 'fallback', $skin->getSkinName() );
-       }
-
-       /**
-        * @covers Skin::__construct
-        * @covers Skin::getSkinName
-        */
-       public function testGetSkinName() {
-               $skin = new SkinFallback();
-               $this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
-               $skin = new SkinFallback( 'testname' );
-               $this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
-       }
-
-       /**
-        * @covers SkinFactory::getSkinNames
-        */
-       public function testGetSkinNames() {
-               $factory = new SkinFactory();
-               // A fake callback we can use that will never be called
-               $callback = function () {
-                       // NOP
-               };
-               $factory->register( 'skin1', 'Skin1', $callback );
-               $factory->register( 'skin2', 'Skin2', $callback );
-               $names = $factory->getSkinNames();
-               $this->assertArrayHasKey( 'skin1', $names );
-               $this->assertArrayHasKey( 'skin2', $names );
-               $this->assertEquals( 'Skin1', $names['skin1'] );
-               $this->assertEquals( 'Skin2', $names['skin2'] );
-       }
-}
diff --git a/tests/phpunit/includes/title/ForeignTitleTest.php b/tests/phpunit/includes/title/ForeignTitleTest.php
deleted file mode 100644 (file)
index f2fccc7..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-<?php
-/**
- * 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 This, that and the other
- */
-
-/**
- * @covers ForeignTitle
- *
- * @group Title
- */
-class ForeignTitleTest extends MediaWikiTestCase {
-
-       public function basicProvider() {
-               return [
-                       [
-                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
-                               20, 'Contributor', 'JohnDoe'
-                       ],
-                       [
-                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
-                               1, 'Discussion', 'Capital'
-                       ],
-                       [
-                               new ForeignTitle( 0, '', 'MainNamespace' ),
-                               0, '', 'MainNamespace'
-                       ],
-                       [
-                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
-                               4, 'Some_ns', 'Article_title_with_spaces'
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider basicProvider
-        */
-       public function testBasic( ForeignTitle $title, $expectedId, $expectedName,
-               $expectedText
-       ) {
-               $this->assertEquals( true, $title->isNamespaceIdKnown() );
-               $this->assertEquals( $expectedId, $title->getNamespaceId() );
-               $this->assertEquals( $expectedName, $title->getNamespaceName() );
-               $this->assertEquals( $expectedText, $title->getText() );
-       }
-
-       public function testUnknownNamespaceCheck() {
-               $title = new ForeignTitle( null, 'this', 'that' );
-
-               $this->assertEquals( false, $title->isNamespaceIdKnown() );
-               $this->assertEquals( 'this', $title->getNamespaceName() );
-               $this->assertEquals( 'that', $title->getText() );
-       }
-
-       public function testUnknownNamespaceError() {
-               $this->setExpectedException( MWException::class );
-               $title = new ForeignTitle( null, 'this', 'that' );
-               $title->getNamespaceId();
-       }
-
-       public function fullTextProvider() {
-               return [
-                       [
-                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
-                               'Contributor:JohnDoe'
-                       ],
-                       [
-                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
-                               'Discussion:Capital'
-                       ],
-                       [
-                               new ForeignTitle( 0, '', 'MainNamespace' ),
-                               'MainNamespace'
-                       ],
-                       [
-                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
-                               'Some_ns:Article_title_with_spaces'
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider fullTextProvider
-        */
-       public function testFullText( ForeignTitle $title, $fullText ) {
-               $this->assertEquals( $fullText, $title->getFullText() );
-       }
-}
diff --git a/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php b/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php
deleted file mode 100644 (file)
index 9aa3578..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-/**
- * 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 This, that and the other
- */
-
-/**
- * @covers NamespaceAwareForeignTitleFactory
- *
- * @group Title
- */
-class NamespaceAwareForeignTitleFactoryTest extends MediaWikiTestCase {
-
-       public function basicProvider() {
-               return [
-                       [
-                               'MainNamespaceArticle', 0,
-                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
-                       ],
-                       [
-                               'MainNamespaceArticle', null,
-                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
-                       ],
-                       [
-                               'Magic:_The_Gathering', 0,
-                               new ForeignTitle( 0, '', 'Magic:_The_Gathering' ),
-                       ],
-                       [
-                               'Talk:Nice_talk', 1,
-                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
-                       ],
-                       [
-                               'Talk:Magic:_The_Gathering', 1,
-                               new ForeignTitle( 1, 'Talk', 'Magic:_The_Gathering' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 0,
-                               new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', null,
-                               new ForeignTitle( 9000, 'Bogus', 'Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 4,
-                               new ForeignTitle( 4, 'Bogus', 'Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 1,
-                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
-                       ],
-                       // Misconfigured wiki with unregistered namespace (T114115)
-                       [
-                               'Nice_talk', 1234,
-                               new ForeignTitle( 1234, 'Ns1234', 'Nice_talk' ),
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider basicProvider
-        */
-       public function testBasic( $title, $ns, ForeignTitle $foreignTitle ) {
-               $foreignNamespaces = [
-                       0 => '', 1 => 'Talk', 100 => 'Portal', 9000 => 'Bogus'
-               ];
-
-               $factory = new NamespaceAwareForeignTitleFactory( $foreignNamespaces );
-               $testTitle = $factory->createForeignTitle( $title, $ns );
-
-               $this->assertEquals( $testTitle->isNamespaceIdKnown(),
-                       $foreignTitle->isNamespaceIdKnown() );
-
-               if (
-                       $testTitle->isNamespaceIdKnown() &&
-                       $foreignTitle->isNamespaceIdKnown()
-               ) {
-                       $this->assertEquals( $testTitle->getNamespaceId(),
-                               $foreignTitle->getNamespaceId() );
-               }
-
-               $this->assertEquals( $testTitle->getNamespaceName(),
-                       $foreignTitle->getNamespaceName() );
-               $this->assertEquals( $testTitle->getText(), $foreignTitle->getText() );
-       }
-}
diff --git a/tests/phpunit/includes/title/TitleValueTest.php b/tests/phpunit/includes/title/TitleValueTest.php
deleted file mode 100644 (file)
index bbeb068..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-/**
- * 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 Daniel Kinzler
- */
-
-/**
- * @covers TitleValue
- *
- * @group Title
- */
-class TitleValueTest extends MediaWikiTestCase {
-
-       public function goodConstructorProvider() {
-               return [
-                       [ NS_MAIN, '', 'fragment', '', true, false ],
-                       [ NS_USER, 'TestThis', 'stuff', '', true, false ],
-                       [ NS_USER, 'TestThis', '', 'baz', false, true ],
-               ];
-       }
-
-       /**
-        * @dataProvider goodConstructorProvider
-        */
-       public function testConstruction( $ns, $text, $fragment, $interwiki, $hasFragment,
-               $hasInterwiki
-       ) {
-               $title = new TitleValue( $ns, $text, $fragment, $interwiki );
-
-               $this->assertEquals( $ns, $title->getNamespace() );
-               $this->assertTrue( $title->inNamespace( $ns ) );
-               $this->assertEquals( $text, $title->getText() );
-               $this->assertEquals( $fragment, $title->getFragment() );
-               $this->assertEquals( $hasFragment, $title->hasFragment() );
-               $this->assertEquals( $interwiki, $title->getInterwiki() );
-               $this->assertEquals( $hasInterwiki, $title->isExternal() );
-       }
-
-       public function badConstructorProvider() {
-               return [
-                       [ 'foo', 'title', 'fragment', '' ],
-                       [ null, 'title', 'fragment', '' ],
-                       [ 2.3, 'title', 'fragment', '' ],
-
-                       [ NS_MAIN, 5, 'fragment', '' ],
-                       [ NS_MAIN, null, 'fragment', '' ],
-                       [ NS_USER, '', 'fragment', '' ],
-                       [ NS_MAIN, 'foo bar', '', '' ],
-                       [ NS_MAIN, 'bar_', '', '' ],
-                       [ NS_MAIN, '_foo', '', '' ],
-                       [ NS_MAIN, ' eek ', '', '' ],
-
-                       [ NS_MAIN, 'title', 5, '' ],
-                       [ NS_MAIN, 'title', null, '' ],
-                       [ NS_MAIN, 'title', [], '' ],
-
-                       [ NS_MAIN, 'title', '', 5 ],
-                       [ NS_MAIN, 'title', null, 5 ],
-                       [ NS_MAIN, 'title', [], 5 ],
-               ];
-       }
-
-       /**
-        * @dataProvider badConstructorProvider
-        */
-       public function testConstructionErrors( $ns, $text, $fragment, $interwiki ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new TitleValue( $ns, $text, $fragment, $interwiki );
-       }
-
-       public function fragmentTitleProvider() {
-               return [
-                       [ new TitleValue( NS_MAIN, 'Test' ), 'foo' ],
-                       [ new TitleValue( NS_TALK, 'Test', 'foo' ), '' ],
-                       [ new TitleValue( NS_CATEGORY, 'Test', 'foo' ), 'bar' ],
-               ];
-       }
-
-       /**
-        * @dataProvider fragmentTitleProvider
-        */
-       public function testCreateFragmentTitle( TitleValue $title, $fragment ) {
-               $fragmentTitle = $title->createFragmentTarget( $fragment );
-
-               $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() );
-               $this->assertEquals( $title->getText(), $fragmentTitle->getText() );
-               $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
-       }
-
-       public function getTextProvider() {
-               return [
-                       [ 'Foo', 'Foo' ],
-                       [ 'Foo_Bar', 'Foo Bar' ],
-               ];
-       }
-
-       /**
-        * @dataProvider getTextProvider
-        */
-       public function testGetText( $dbkey, $text ) {
-               $title = new TitleValue( NS_MAIN, $dbkey );
-
-               $this->assertEquals( $text, $title->getText() );
-       }
-
-       public function provideTestToString() {
-               yield [
-                       new TitleValue( 0, 'Foo' ),
-                       '0:Foo'
-               ];
-               yield [
-                       new TitleValue( 1, 'Bar_Baz' ),
-                       '1:Bar_Baz'
-               ];
-               yield [
-                       new TitleValue( 9, 'JoJo', 'Frag' ),
-                       '9:JoJo#Frag'
-               ];
-               yield [
-                       new TitleValue( 200, 'tea', 'Fragment', 'wikicode' ),
-                       'wikicode:200:tea#Fragment'
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestToString
-        */
-       public function testToString( TitleValue $value, $expected ) {
-               $this->assertSame(
-                       $expected,
-                       $value->__toString()
-               );
-       }
-}
diff --git a/tests/phpunit/includes/user/UserArrayFromResultTest.php b/tests/phpunit/includes/user/UserArrayFromResultTest.php
deleted file mode 100644 (file)
index 4cbfe46..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- * @covers UserArrayFromResult
- */
-class UserArrayFromResultTest extends MediaWikiTestCase {
-
-       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
-               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
-                       ->disableOriginalConstructor();
-
-               $resultWrapper = $resultWrapper->getMock();
-               $resultWrapper->expects( $this->atLeastOnce() )
-                       ->method( 'current' )
-                       ->will( $this->returnValue( $row ) );
-               $resultWrapper->expects( $this->any() )
-                       ->method( 'numRows' )
-                       ->will( $this->returnValue( $numRows ) );
-
-               return $resultWrapper;
-       }
-
-       private function getRowWithUsername( $username = 'fooUser' ) {
-               $row = new stdClass();
-               $row->user_name = $username;
-               return $row;
-       }
-
-       /**
-        * @covers UserArrayFromResult::__construct
-        */
-       public function testConstructionWithFalseRow() {
-               $row = false;
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new UserArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertEquals( $row, $object->current );
-       }
-
-       /**
-        * @covers UserArrayFromResult::__construct
-        */
-       public function testConstructionWithRow() {
-               $username = 'addshore';
-               $row = $this->getRowWithUsername( $username );
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new UserArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertInstanceOf( User::class, $object->current );
-               $this->assertEquals( $username, $object->current->mName );
-       }
-
-       public static function provideNumberOfRows() {
-               return [
-                       [ 0 ],
-                       [ 1 ],
-                       [ 122 ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNumberOfRows
-        * @covers UserArrayFromResult::count
-        */
-       public function testCountWithVaryingValues( $numRows ) {
-               $object = new UserArrayFromResult( $this->getMockResultWrapper(
-                       $this->getRowWithUsername(),
-                       $numRows
-               ) );
-               $this->assertEquals( $numRows, $object->count() );
-       }
-
-       /**
-        * @covers UserArrayFromResult::current
-        */
-       public function testCurrentAfterConstruction() {
-               $username = 'addshore';
-               $userRow = $this->getRowWithUsername( $username );
-               $object = new UserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
-               $this->assertInstanceOf( User::class, $object->current() );
-               $this->assertEquals( $username, $object->current()->mName );
-       }
-
-       public function provideTestValid() {
-               return [
-                       [ $this->getRowWithUsername(), true ],
-                       [ false, false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestValid
-        * @covers UserArrayFromResult::valid
-        */
-       public function testValid( $input, $expected ) {
-               $object = new UserArrayFromResult( $this->getMockResultWrapper( $input ) );
-               $this->assertEquals( $expected, $object->valid() );
-       }
-
-       // @todo unit test for key()
-       // @todo unit test for next()
-       // @todo unit test for rewind()
-}
index 4862747..340a4c3 100644 (file)
@@ -46,6 +46,8 @@ class UserGroupMembershipTest extends MediaWikiTestCase {
                $this->userTester->addGroup( 'unittesters' );
                $this->expiryTime = wfTimestamp( TS_MW, time() + 100500 );
                $this->userTester->addGroup( 'testwriters', $this->expiryTime );
+
+               $this->resetServices();
        }
 
        /**
index 79c6e96..5a978f9 100644 (file)
@@ -129,12 +129,12 @@ class UserTest extends MediaWikiTestCase {
                $this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
 
                // Add a hook manipluating the rights
-               $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserGetRights' => [ function ( $user, &$rights ) {
+               $this->setTemporaryHook( 'UserGetRights', function ( $user, &$rights ) {
                        $rights[] = 'nukeworld';
                        $rights = array_diff( $rights, [ 'writetest' ] );
-               } ] ] );
+               } );
 
-               $userWrapper->mRights = null;
+               $this->resetServices();
                $rights = $user->getRights();
                $this->assertContains( 'test', $rights );
                $this->assertContains( 'runtest', $rights );
@@ -156,7 +156,7 @@ class UserTest extends MediaWikiTestCase {
                $mockRequest->method( 'getSession' )->willReturn( $session );
                $userWrapper->mRequest = $mockRequest;
 
-               $userWrapper->mRights = null;
+               $this->resetServices();
                $rights = $user->getRights();
                $this->assertContains( 'test', $rights );
                $this->assertNotContains( 'runtest', $rights );
@@ -928,9 +928,11 @@ class UserTest extends MediaWikiTestCase {
 
                $this->setMwGlobals( 'wgRateLimitsExcludedIPs', [] );
                $noRateLimitUser = $this->getMockBuilder( User::class )->disableOriginalConstructor()
-                       ->setMethods( [ 'getIP', 'getRights' ] )->getMock();
+                       ->setMethods( [ 'getIP', 'getId', 'getGroups' ] )->getMock();
                $noRateLimitUser->expects( $this->any() )->method( 'getIP' )->willReturn( '1.2.3.4' );
-               $noRateLimitUser->expects( $this->any() )->method( 'getRights' )->willReturn( [ 'noratelimit' ] );
+               $noRateLimitUser->expects( $this->any() )->method( 'getId' )->willReturn( 0 );
+               $noRateLimitUser->expects( $this->any() )->method( 'getGroups' )->willReturn( [] );
+               $this->overrideUserPermissions( $noRateLimitUser, 'noratelimit' );
                $this->assertFalse( $noRateLimitUser->isPingLimitable() );
        }
 
diff --git a/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php
deleted file mode 100644 (file)
index f424b21..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-<?php
-
-use MediaWiki\User\UserIdentityValue;
-
-/**
- * @author Addshore
- *
- * @covers NoWriteWatchedItemStore
- */
-class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
-
-       public function testAddWatch() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'addWatch' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->addWatch(
-                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
-       }
-
-       public function testAddWatchBatchForUser() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'addWatchBatchForUser' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] );
-       }
-
-       public function testRemoveWatch() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'removeWatch' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->removeWatch(
-                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
-       }
-
-       public function testSetNotificationTimestampsForUser() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'setNotificationTimestampsForUser' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->setNotificationTimestampsForUser(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       'timestamp',
-                       []
-               );
-       }
-
-       public function testUpdateNotificationTimestamp() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'updateNotificationTimestamp' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->updateNotificationTimestamp(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' ),
-                       'timestamp'
-               );
-       }
-
-       public function testResetNotificationTimestamp() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'resetNotificationTimestamp' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->resetNotificationTimestamp(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-       }
-
-       public function testCountWatchedItems() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'countWatchedItems' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countWatchedItems(
-                       new UserIdentityValue( 1, 'MockUser', 0 )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountWatchers() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'countWatchers' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countWatchers(
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountVisitingWatchers() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countVisitingWatchers' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countVisitingWatchers(
-                       new TitleValue( 0, 'Foo' ),
-                       9
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountWatchersMultiple() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countVisitingWatchersMultiple' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countWatchersMultiple(
-                       [ new TitleValue( 0, 'Foo' ) ],
-                       []
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountVisitingWatchersMultiple() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countVisitingWatchersMultiple' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countVisitingWatchersMultiple(
-                       [ [ new TitleValue( 0, 'Foo' ), 99 ] ],
-                       11
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testGetWatchedItem() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'getWatchedItem' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->getWatchedItem(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testLoadWatchedItem() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'loadWatchedItem' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->loadWatchedItem(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testGetWatchedItemsForUser() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'getWatchedItemsForUser' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->getWatchedItemsForUser(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       []
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testIsWatched() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'isWatched' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->isWatched(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testGetNotificationTimestampsBatch() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'getNotificationTimestampsBatch' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->getNotificationTimestampsBatch(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       [ new TitleValue( 0, 'Foo' ) ]
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountUnreadNotifications() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countUnreadNotifications' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countUnreadNotifications(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       88
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testDuplicateAllAssociatedEntries() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->duplicateAllAssociatedEntries(
-                       new TitleValue( 0, 'Foo' ),
-                       new TitleValue( 0, 'Bar' )
-               );
-       }
-
-}
index d406c88..cce9d0e 100644 (file)
@@ -11,7 +11,7 @@
  *
  * @author Katie Filbert < aude.wiki@gmail.com >
  */
-class SpecialPageAliasTest extends MediaWikiTestCase {
+class SpecialPageAliasTest extends \MediaWikiUnitTestCase {
 
        /**
         * @coversNothing
diff --git a/tests/phpunit/unit/includes/FauxResponseTest.php b/tests/phpunit/unit/includes/FauxResponseTest.php
new file mode 100644 (file)
index 0000000..5e208ac
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * 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 \MediaWikiUnitTestCase {
+       /** @var FauxResponse */
+       protected $response;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->response = new FauxResponse;
+       }
+
+       /**
+        * @covers FauxResponse::setCookie
+        * @covers FauxResponse::getCookie
+        * @covers FauxResponse::getCookieData
+        * @covers FauxResponse::getCookies
+        */
+       public function testCookie() {
+               $expire = time() + 100;
+               $cookie = [
+                       'value' => 'val',
+                       'path' => '/path',
+                       'domain' => 'domain',
+                       'secure' => true,
+                       'httpOnly' => false,
+                       'raw' => false,
+                       'expire' => $expire,
+               ];
+
+               $this->assertEquals( null, $this->response->getCookie( 'xkey' ), 'Non-existing cookie' );
+               $this->response->setCookie( 'key', 'val', $expire, [
+                       'prefix' => 'x',
+                       'path' => '/path',
+                       'domain' => 'domain',
+                       'secure' => 1,
+                       'httpOnly' => 0,
+               ] );
+               $this->assertEquals( 'val', $this->response->getCookie( 'xkey' ), 'Existing cookie' );
+               $this->assertEquals( $cookie, $this->response->getCookieData( 'xkey' ),
+                       'Existing cookie (data)' );
+               $this->assertEquals( [ 'xkey' => $cookie ], $this->response->getCookies(),
+                       'Existing cookies' );
+       }
+
+       /**
+        * @covers FauxResponse::getheader
+        * @covers FauxResponse::header
+        */
+       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'
+               );
+       }
+
+       /**
+        * @covers FauxResponse::getStatusCode
+        */
+       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->statusHeader( 210 );
+               $this->assertEquals(
+                       210,
+                       $this->response->getStatusCode(),
+                       'Handle statusHeader method'
+               );
+
+               $this->response->header( 'Location: http://localhost/', false, 206 );
+               $this->assertEquals(
+                       206,
+                       $this->response->getStatusCode(),
+                       'Third parameter with another header'
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/FormOptionsInitializationTest.php b/tests/phpunit/unit/includes/FormOptionsInitializationTest.php
new file mode 100644 (file)
index 0000000..708956d
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * Test class for FormOptions initialization
+ * Ensure the FormOptions::add() does what we want it to do.
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsInitializationTest extends \MediaWikiUnitTestCase {
+       /**
+        * @var FormOptions
+        */
+       protected $object;
+
+       /**
+        * A new fresh and empty FormOptions object to test initialization
+        * with.
+        */
+       protected function setUp() {
+               parent::setUp();
+               $this->object = TestingAccessWrapper::newFromObject( new FormOptions() );
+       }
+
+       /**
+        * @covers FormOptions::add
+        */
+       public function testAddStringOption() {
+               $this->object->add( 'foo', 'string value' );
+               $this->assertEquals(
+                       [
+                               'foo' => [
+                                       'default' => 'string value',
+                                       'consumed' => false,
+                                       'type' => FormOptions::STRING,
+                                       'value' => null,
+                               ]
+                       ],
+                       $this->object->options
+               );
+       }
+
+       /**
+        * @covers FormOptions::add
+        */
+       public function testAddIntegers() {
+               $this->object->add( 'one', 1 );
+               $this->object->add( 'negone', -1 );
+               $this->assertEquals(
+                       [
+                               'negone' => [
+                                       'default' => -1,
+                                       'value' => null,
+                                       'consumed' => false,
+                                       'type' => FormOptions::INT,
+                               ],
+                               'one' => [
+                                       'default' => 1,
+                                       'value' => null,
+                                       'consumed' => false,
+                                       'type' => FormOptions::INT,
+                               ]
+                       ],
+                       $this->object->options
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/FormOptionsTest.php b/tests/phpunit/unit/includes/FormOptionsTest.php
new file mode 100644 (file)
index 0000000..c14595b
--- /dev/null
@@ -0,0 +1,105 @@
+<?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.
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsTest extends \MediaWikiUnitTestCase {
+       /**
+        * @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( 'float', 0.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 assertGuessFloat( $data ) {
+               $this->guess( FormOptions::FLOAT, $data );
+       }
+
+       private function assertGuessString( $data ) {
+               $this->guess( FormOptions::STRING, $data );
+       }
+
+       private function assertGuessArray( $data ) {
+               $this->guess( FormOptions::ARR, $data );
+       }
+
+       /** Generic helper */
+       private function guess( $expected, $data ) {
+               $this->assertEquals(
+                       $expected,
+                       FormOptions::guessType( $data )
+               );
+       }
+
+       /* @} */
+
+       /**
+        * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString
+        * @covers FormOptions::guessType
+        */
+       public function testGuessTypeDetection() {
+               $this->assertGuessBoolean( true );
+               $this->assertGuessBoolean( false );
+
+               $this->assertGuessInt( 0 );
+               $this->assertGuessInt( -5 );
+               $this->assertGuessInt( 5 );
+               $this->assertGuessInt( 0x0F );
+
+               $this->assertGuessFloat( 0.0 );
+               $this->assertGuessFloat( 1.5 );
+               $this->assertGuessFloat( 1e3 );
+
+               $this->assertGuessString( 'true' );
+               $this->assertGuessString( 'false' );
+               $this->assertGuessString( '5' );
+               $this->assertGuessString( '0' );
+               $this->assertGuessString( '1.5' );
+
+               $this->assertGuessArray( [ 'foo' ] );
+       }
+
+       /**
+        * @expectedException MWException
+        * @covers FormOptions::guessType
+        */
+       public function testGuessTypeOnNullThrowException() {
+               $this->object->guessType( null );
+       }
+}
diff --git a/tests/phpunit/unit/includes/LicensesTest.php b/tests/phpunit/unit/includes/LicensesTest.php
new file mode 100644 (file)
index 0000000..e5a6bae
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @covers Licenses
+ */
+class LicensesTest extends \MediaWikiUnitTestCase {
+
+       public function testLicenses() {
+               $str = "
+* Free licenses:
+** GFDL|Debian disagrees
+";
+
+               $lc = new Licenses( [
+                       '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::class ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Rest/HeaderContainerTest.php b/tests/phpunit/unit/includes/Rest/HeaderContainerTest.php
new file mode 100644 (file)
index 0000000..e65251e
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use MediaWiki\Rest\HeaderContainer;
+
+/**
+ * @covers \MediaWiki\Rest\HeaderContainer
+ */
+class HeaderContainerTest extends \MediaWikiUnitTestCase {
+       public static function provideSetHeader() {
+               return [
+                       'simple' => [
+                               [
+                                       [ 'Test', 'foo' ]
+                               ],
+                               [ 'Test' => [ 'foo' ] ],
+                               [ 'Test' => 'foo' ]
+                       ],
+                       'replace' => [
+                               [
+                                       [ 'Test', 'foo' ],
+                                       [ 'Test', 'bar' ],
+                               ],
+                               [ 'Test' => [ 'bar' ] ],
+                               [ 'Test' => 'bar' ],
+                       ],
+                       'array value' => [
+                               [
+                                       [ 'Test', [ '1', '2' ] ],
+                                       [ 'Test', [ '3', '4' ] ],
+                               ],
+                               [ 'Test' => [ '3', '4' ] ],
+                               [ 'Test' => '3, 4' ]
+                       ],
+                       'preserve most recent case' => [
+                               [
+                                       [ 'test', 'foo' ],
+                                       [ 'tesT', 'bar' ],
+                               ],
+                               [ 'tesT' => [ 'bar' ] ],
+                               [ 'tesT' => 'bar' ]
+                       ],
+                       'empty' => [ [], [], [] ],
+               ];
+       }
+
+       /** @dataProvider provideSetHeader */
+       public function testSetHeader( $setOps, $headers, $lines ) {
+               $hc = new HeaderContainer;
+               foreach ( $setOps as list( $name, $value ) ) {
+                       $hc->setHeader( $name, $value );
+               }
+               $this->assertSame( $headers, $hc->getHeaders() );
+               $this->assertSame( $lines, $hc->getHeaderLines() );
+       }
+
+       public static function provideAddHeader() {
+               return [
+                       'simple' => [
+                               [
+                                       [ 'Test', 'foo' ]
+                               ],
+                               [ 'Test' => [ 'foo' ] ],
+                               [ 'Test' => 'foo' ]
+                       ],
+                       'add' => [
+                               [
+                                       [ 'Test', 'foo' ],
+                                       [ 'Test', 'bar' ],
+                               ],
+                               [ 'Test' => [ 'foo', 'bar' ] ],
+                               [ 'Test' => 'foo, bar' ],
+                       ],
+                       'array value' => [
+                               [
+                                       [ 'Test', [ '1', '2' ] ],
+                                       [ 'Test', [ '3', '4' ] ],
+                               ],
+                               [ 'Test' => [ '1', '2', '3', '4' ] ],
+                               [ 'Test' => '1, 2, 3, 4' ]
+                       ],
+                       'preserve original case' => [
+                               [
+                                       [ 'Test', 'foo' ],
+                                       [ 'tesT', 'bar' ],
+                               ],
+                               [ 'Test' => [ 'foo', 'bar' ] ],
+                               [ 'Test' => 'foo, bar' ]
+                       ],
+               ];
+       }
+
+       /** @dataProvider provideAddHeader */
+       public function testAddHeader( $addOps, $headers, $lines ) {
+               $hc = new HeaderContainer;
+               foreach ( $addOps as list( $name, $value ) ) {
+                       $hc->addHeader( $name, $value );
+               }
+               $this->assertSame( $headers, $hc->getHeaders() );
+               $this->assertSame( $lines, $hc->getHeaderLines() );
+       }
+
+       public static function provideRemoveHeader() {
+               return [
+                       'simple' => [
+                               [ [ 'Test', 'foo' ] ],
+                               [ 'Test' ],
+                               [],
+                               []
+                       ],
+                       'case mismatch' => [
+                               [ [ 'Test', 'foo' ] ],
+                               [ 'tesT' ],
+                               [],
+                               []
+                       ],
+                       'remove nonexistent' => [
+                               [ [ 'A', '1' ] ],
+                               [ 'B' ],
+                               [ 'A' => [ '1' ] ],
+                               [ 'A' => '1' ]
+                       ],
+               ];
+       }
+
+       /** @dataProvider provideRemoveHeader */
+       public function testRemoveHeader( $addOps, $removeOps, $headers, $lines ) {
+               $hc = new HeaderContainer;
+               foreach ( $addOps as list( $name, $value ) ) {
+                       $hc->addHeader( $name, $value );
+               }
+               foreach ( $removeOps as $name ) {
+                       $hc->removeHeader( $name );
+               }
+               $this->assertSame( $headers, $hc->getHeaders() );
+               $this->assertSame( $lines, $hc->getHeaderLines() );
+       }
+
+       public function testHasHeader() {
+               $hc = new HeaderContainer;
+               $hc->addHeader( 'A', '1' );
+               $hc->addHeader( 'B', '2' );
+               $hc->addHeader( 'C', '3' );
+               $hc->removeHeader( 'B' );
+               $hc->removeHeader( 'c' );
+               $this->assertTrue( $hc->hasHeader( 'A' ) );
+               $this->assertTrue( $hc->hasHeader( 'a' ) );
+               $this->assertFalse( $hc->hasHeader( 'B' ) );
+               $this->assertFalse( $hc->hasHeader( 'c' ) );
+               $this->assertFalse( $hc->hasHeader( 'C' ) );
+       }
+
+       public function testGetRawHeaderLines() {
+               $hc = new HeaderContainer;
+               $hc->addHeader( 'A', '1' );
+               $hc->addHeader( 'a', '2' );
+               $hc->addHeader( 'b', '3' );
+               $hc->addHeader( 'Set-Cookie', 'x' );
+               $hc->addHeader( 'SET-cookie', 'y' );
+               $this->assertSame(
+                       [
+                               'A: 1, 2',
+                               'b: 3',
+                               'Set-Cookie: x',
+                               'Set-Cookie: y',
+                       ],
+                       $hc->getRawHeaderLines()
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php b/tests/phpunit/unit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php
new file mode 100644 (file)
index 0000000..f56024c
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+namespace MediaWiki\Tests\Rest\PathTemplateMatcher;
+
+use MediaWiki\Rest\PathTemplateMatcher\PathConflict;
+use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
+
+/**
+ * @covers \MediaWiki\Rest\PathTemplateMatcher\PathMatcher
+ * @covers \MediaWiki\Rest\PathTemplateMatcher\PathConflict
+ */
+class PathMatcherTest extends \MediaWikiUnitTestCase {
+       private static $normalRoutes = [
+               '/a/b',
+               '/b/{x}',
+               '/c/{x}/d',
+               '/c/{x}/e',
+               '/c/{x}/{y}/d',
+       ];
+
+       public static function provideConflictingRoutes() {
+               return [
+                       [ '/a/b', 0, '/a/b' ],
+                       [ '/a/{x}', 0, '/a/b' ],
+                       [ '/{x}/c', 1, '/b/{x}' ],
+                       [ '/b/a', 1, '/b/{x}' ],
+                       [ '/b/{x}', 1, '/b/{x}' ],
+                       [ '/{x}/{y}/d', 2, '/c/{x}/d' ],
+               ];
+       }
+
+       public static function provideMatch() {
+               return [
+                       [ '', false ],
+                       [ '/a/b', [ 'params' => [], 'userData' => 0 ] ],
+                       [ '/b', false ],
+                       [ '/b/1', [ 'params' => [ 'x' => '1' ], 'userData' => 1 ] ],
+                       [ '/c/1/d', [ 'params' => [ 'x' => '1' ], 'userData' => 2 ] ],
+                       [ '/c/1/e', [ 'params' => [ 'x' => '1' ], 'userData' => 3 ] ],
+                       [ '/c/000/e', [ 'params' => [ 'x' => '000' ], 'userData' => 3 ] ],
+                       [ '/c/1/f', false ],
+                       [ '/c//e', [ 'params' => [ 'x' => '' ], 'userData' => 3 ] ],
+                       [ '/c///e', false ],
+               ];
+       }
+
+       public function createNormalRouter() {
+               $pm = new PathMatcher;
+               foreach ( self::$normalRoutes as $i => $route ) {
+                       $pm->add( $route, $i );
+               }
+               return $pm;
+       }
+
+       /** @dataProvider provideConflictingRoutes */
+       public function testAddConflict( $attempt, $expectedUserData, $expectedTemplate ) {
+               $pm = $this->createNormalRouter();
+               $actualTemplate = null;
+               $actualUserData = null;
+               try {
+                       $pm->add( $attempt, 'conflict' );
+               } catch ( PathConflict $pc ) {
+                       $actualTemplate = $pc->existingTemplate;
+                       $actualUserData = $pc->existingUserData;
+               }
+               $this->assertSame( $expectedUserData, $actualUserData );
+               $this->assertSame( $expectedTemplate, $actualTemplate );
+       }
+
+       /** @dataProvider provideMatch */
+       public function testMatch( $path, $expectedResult ) {
+               $pm = $this->createNormalRouter();
+               $result = $pm->match( $path );
+               $this->assertSame( $expectedResult, $result );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Rest/StringStreamTest.php b/tests/phpunit/unit/includes/Rest/StringStreamTest.php
new file mode 100644 (file)
index 0000000..1e72239
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use MediaWiki\Rest\StringStream;
+
+/** @covers \MediaWiki\Rest\StringStream */
+class StringStreamTest extends \MediaWikiUnitTestCase {
+       public static function provideSeekGetContents() {
+               return [
+                       [ 'abcde', 0, SEEK_SET, 'abcde' ],
+                       [ 'abcde', 1, SEEK_SET, 'bcde' ],
+                       [ 'abcde', 5, SEEK_SET, '' ],
+                       [ 'abcde', 1, SEEK_CUR, 'cde' ],
+                       [ 'abcde', 0, SEEK_END, '' ],
+               ];
+       }
+
+       /** @dataProvider provideSeekGetContents */
+       public function testCopyToStream( $input, $offset, $whence, $expected ) {
+               $ss = new StringStream;
+               $ss->write( $input );
+               $ss->seek( 1 );
+               $ss->seek( $offset, $whence );
+               $destStream = fopen( 'php://memory', 'w+' );
+               $ss->copyToStream( $destStream );
+               fseek( $destStream, 0 );
+               $result = stream_get_contents( $destStream );
+               $this->assertSame( $expected, $result );
+       }
+
+       public function testGetSize() {
+               $ss = new StringStream;
+               $this->assertSame( 0, $ss->getSize() );
+               $ss->write( "hello" );
+               $this->assertSame( 5, $ss->getSize() );
+               $ss->rewind();
+               $this->assertSame( 5, $ss->getSize() );
+       }
+
+       public function testTell() {
+               $ss = new StringStream;
+               $this->assertSame( $ss->tell(), 0 );
+               $ss->write( "abc" );
+               $this->assertSame( $ss->tell(), 3 );
+               $ss->seek( 0 );
+               $ss->read( 1 );
+               $this->assertSame( $ss->tell(), 1 );
+       }
+
+       public function testEof() {
+               $ss = new StringStream( 'abc' );
+               $this->assertFalse( $ss->eof() );
+               $ss->read( 1 );
+               $this->assertFalse( $ss->eof() );
+               $ss->read( 1 );
+               $this->assertFalse( $ss->eof() );
+               $ss->read( 1 );
+               $this->assertTrue( $ss->eof() );
+               $ss->rewind();
+               $this->assertFalse( $ss->eof() );
+       }
+
+       public function testIsSeekable() {
+               $ss = new StringStream;
+               $this->assertTrue( $ss->isSeekable() );
+       }
+
+       public function testIsReadable() {
+               $ss = new StringStream;
+               $this->assertTrue( $ss->isReadable() );
+       }
+
+       public function testIsWritable() {
+               $ss = new StringStream;
+               $this->assertTrue( $ss->isWritable() );
+       }
+
+       public function testSeekWrite() {
+               $ss = new StringStream;
+               $this->assertSame( '', (string)$ss );
+               $ss->write( 'a' );
+               $this->assertSame( 'a', (string)$ss );
+               $ss->write( 'b' );
+               $this->assertSame( 'ab', (string)$ss );
+               $ss->seek( 1 );
+               $ss->write( 'c' );
+               $this->assertSame( 'ac', (string)$ss );
+       }
+
+       /** @dataProvider provideSeekGetContents */
+       public function testSeekGetContents( $input, $offset, $whence, $expected ) {
+               $ss = new StringStream( $input );
+               $ss->seek( 1 );
+               $ss->seek( $offset, $whence );
+               $this->assertSame( $expected, $ss->getContents() );
+       }
+
+       public static function provideSeekRead() {
+               return [
+                       [ 'abcde', 0, SEEK_SET, 1, 'a' ],
+                       [ 'abcde', 0, SEEK_SET, 2, 'ab' ],
+                       [ 'abcde', 4, SEEK_SET, 2, 'e' ],
+                       [ 'abcde', 5, SEEK_SET, 1, '' ],
+                       [ 'abcde', 1, SEEK_CUR, 1, 'c' ],
+                       [ 'abcde', 0, SEEK_END, 1, '' ],
+                       [ 'abcde', -1, SEEK_END, 1, 'e' ],
+               ];
+       }
+
+       /** @dataProvider provideSeekRead */
+       public function testSeekRead( $input, $offset, $whence, $length, $expected ) {
+               $ss = new StringStream( $input );
+               $ss->seek( 1 );
+               $ss->seek( $offset, $whence );
+               $this->assertSame( $expected, $ss->read( $length ) );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testReadBeyondEnd() {
+               $ss = new StringStream( 'abc' );
+               $ss->seek( 1, SEEK_END );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testReadBeforeStart() {
+               $ss = new StringStream( 'abc' );
+               $ss->seek( -1 );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Revision/FallbackSlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/FallbackSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..17b3504
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\FallbackSlotRoleHandler;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\FallbackSlotRoleHandler
+ */
+class FallbackSlotRoleHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @return Title
+        */
+       private function makeBlankTitleObject() {
+               return $this->createMock( Title::class );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getDefaultModel()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new FallbackSlotRoleHandler( 'foo' );
+               $this->assertSame( 'foo', $handler->getRole() );
+               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title ) );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               // For the fallback handler, no models are allowed
+               $title = $this->makeBlankTitleObject();
+               $this->assertFalse( $handler->isAllowedModel( 'FooModel', $title ) );
+               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedOn() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertFalse( $handler->isAllowedOn( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               $this->assertFalse( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/Revision/SlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/SlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..39217c2
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\SlotRoleHandler;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRoleHandler
+ */
+class SlotRoleHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @return Title
+        */
+       private function makeBlankTitleObject() {
+               return $this->createMock( Title::class );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getDefaultModel()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ] );
+               $this->assertSame( 'foo', $handler->getRole() );
+               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'frob', $hints );
+               $this->assertSame( 'niz', $hints['frob'] );
+
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertTrue( $handler->isAllowedModel( 'FooModel', $title ) );
+               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
+
+               $this->assertFalse( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/ServiceWiringTest.php b/tests/phpunit/unit/includes/ServiceWiringTest.php
new file mode 100644 (file)
index 0000000..25b0214
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @coversNothing
+ */
+class ServiceWiringTest extends \MediaWikiUnitTestCase {
+       public function testServicesAreSorted() {
+               global $IP;
+               $services = array_keys( require "$IP/includes/ServiceWiring.php" );
+               $sortedServices = $services;
+               natcasesort( $sortedServices );
+
+               $this->assertSame( $sortedServices, $services,
+                       'Please keep services sorted alphabetically' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/SiteConfigurationTest.php b/tests/phpunit/unit/includes/SiteConfigurationTest.php
new file mode 100644 (file)
index 0000000..b992a86
--- /dev/null
@@ -0,0 +1,379 @@
+<?php
+
+class SiteConfigurationTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var SiteConfiguration
+        */
+       protected $mConf;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->mConf = new SiteConfiguration;
+
+               $this->mConf->suffixes = [ 'wikipedia' => 'wiki' ];
+               $this->mConf->wikis = [ 'enwiki', 'dewiki', 'frwiki' ];
+               $this->mConf->settings = [
+                       'SimpleKey' => [
+                               'wiki' => 'wiki',
+                               'tag' => 'tag',
+                               'enwiki' => 'enwiki',
+                               'dewiki' => 'dewiki',
+                               'frwiki' => 'frwiki',
+                       ],
+
+                       'Fallback' => [
+                               'default' => 'default',
+                               'wiki' => 'wiki',
+                               'tag' => 'tag',
+                               'frwiki' => 'frwiki',
+                               'null_wiki' => null,
+                       ],
+
+                       'WithParams' => [
+                               'default' => '$lang $site $wiki',
+                       ],
+
+                       '+SomeGlobal' => [
+                               'wiki' => [
+                                       'wiki' => 'wiki',
+                               ],
+                               'tag' => [
+                                       'tag' => 'tag',
+                               ],
+                               'enwiki' => [
+                                       'enwiki' => 'enwiki',
+                               ],
+                               'dewiki' => [
+                                       'dewiki' => 'dewiki',
+                               ],
+                               'frwiki' => [
+                                       'frwiki' => 'frwiki',
+                               ],
+                       ],
+
+                       'MergeIt' => [
+                               '+wiki' => [
+                                       'wiki' => 'wiki',
+                               ],
+                               '+tag' => [
+                                       'tag' => 'tag',
+                               ],
+                               'default' => [
+                                       'default' => 'default',
+                               ],
+                               '+enwiki' => [
+                                       'enwiki' => 'enwiki',
+                               ],
+                               '+dewiki' => [
+                                       'dewiki' => 'dewiki',
+                               ],
+                               '+frwiki' => [
+                                       'frwiki' => 'frwiki',
+                               ],
+                       ],
+               ];
+
+               $GLOBALS['SomeGlobal'] = [ 'SomeGlobal' => 'SomeGlobal' ];
+       }
+
+       /**
+        * This function is used as a callback within the tests below
+        */
+       public static function getSiteParamsCallback( $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 [
+                       'suffix' => $site,
+                       'lang' => $lang,
+                       'params' => [
+                               'lang' => $lang,
+                               'site' => $site,
+                               'wiki' => $wiki,
+                       ],
+                       'tags' => [ 'tag' ],
+               ];
+       }
+
+       /**
+        * @covers SiteConfiguration::siteFromDB
+        */
+       public function testSiteFromDb() {
+               $this->assertEquals(
+                       [ 'wikipedia', 'en' ],
+                       $this->mConf->siteFromDB( 'enwiki' ),
+                       'siteFromDB()'
+               );
+               $this->assertEquals(
+                       [ 'wikipedia', '' ],
+                       $this->mConf->siteFromDB( 'wiki' ),
+                       'siteFromDB() on a suffix'
+               );
+               $this->assertEquals(
+                       [ null, null ],
+                       $this->mConf->siteFromDB( 'wikien' ),
+                       'siteFromDB() on a non-existing wiki'
+               );
+
+               $this->mConf->suffixes = [ 'wiki', '' ];
+               $this->assertEquals(
+                       [ '', 'wikien' ],
+                       $this->mConf->siteFromDB( 'wikien' ),
+                       'siteFromDB() on a non-existing wiki (2)'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::getLocalDatabases
+        */
+       public function testGetLocalDatabases() {
+               $this->assertEquals(
+                       [ 'enwiki', 'dewiki', 'frwiki' ],
+                       $this->mConf->getLocalDatabases(),
+                       'getLocalDatabases()'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::get
+        */
+       public function testGetConfVariables() {
+               // Simple
+               $this->assertEquals(
+                       'enwiki',
+                       $this->mConf->get( 'SimpleKey', 'enwiki', 'wiki' ),
+                       'get(): simple setting on an existing wiki'
+               );
+               $this->assertEquals(
+                       'dewiki',
+                       $this->mConf->get( 'SimpleKey', 'dewiki', 'wiki' ),
+                       'get(): simple setting on an existing wiki (2)'
+               );
+               $this->assertEquals(
+                       'frwiki',
+                       $this->mConf->get( 'SimpleKey', 'frwiki', 'wiki' ),
+                       'get(): simple setting on an existing wiki (3)'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'SimpleKey', 'wiki', 'wiki' ),
+                       'get(): simple setting on an suffix'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'SimpleKey', 'eswiki', 'wiki' ),
+                       'get(): simple setting on an non-existing wiki'
+               );
+
+               // Fallback
+               $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', [], [ 'tag' ] ),
+                       'get(): fallback setting on an existing wiki (with wiki tag)'
+               );
+               $this->assertEquals(
+                       'frwiki',
+                       $this->mConf->get( 'Fallback', 'frwiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): no fallback if wiki has its own setting (matching tag)'
+               );
+               $this->assertSame(
+                       // Potential regression test for T192855
+                       null,
+                       $this->mConf->get( 'Fallback', 'null_wiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): no fallback if wiki has its own setting (matching tag and uses null)'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'Fallback', 'wiki', 'wiki' ),
+                       'get(): fallback setting on an suffix'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'Fallback', 'wiki', 'wiki', [], [ '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', [], [ 'tag' ] ),
+                       'get(): fallback setting on an non-existing wiki (with wiki tag)'
+               );
+
+               // Merging
+               $common = [ 'wiki' => 'wiki', 'default' => 'default' ];
+               $commonTag = [ 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ];
+               $this->assertEquals(
+                       [ 'enwiki' => 'enwiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki' ),
+                       'get(): merging setting on an existing wiki'
+               );
+               $this->assertEquals(
+                       [ 'enwiki' => 'enwiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an existing wiki (with tag)'
+               );
+               $this->assertEquals(
+                       [ 'dewiki' => 'dewiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki' ),
+                       'get(): merging setting on an existing wiki (2)'
+               );
+               $this->assertEquals(
+                       [ 'dewiki' => 'dewiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an existing wiki (2) (with tag)'
+               );
+               $this->assertEquals(
+                       [ 'frwiki' => 'frwiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki' ),
+                       'get(): merging setting on an existing wiki (3)'
+               );
+               $this->assertEquals(
+                       [ 'frwiki' => 'frwiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an existing wiki (3) (with tag)'
+               );
+               $this->assertEquals(
+                       [ 'wiki' => 'wiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki' ),
+                       'get(): merging setting on an suffix'
+               );
+               $this->assertEquals(
+                       [ 'wiki' => 'wiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an suffix (with tag)'
+               );
+               $this->assertEquals(
+                       $common,
+                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki' ),
+                       'get(): merging setting on an non-existing wiki'
+               );
+               $this->assertEquals(
+                       $commonTag,
+                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an non-existing wiki (with tag)'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::siteFromDB
+        */
+       public function testSiteFromDbWithCallback() {
+               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+               $this->assertEquals(
+                       [ 'wiki', 'en' ],
+                       $this->mConf->siteFromDB( 'enwiki' ),
+                       'siteFromDB() with callback'
+               );
+               $this->assertEquals(
+                       [ 'wiki', '' ],
+                       $this->mConf->siteFromDB( 'wiki' ),
+                       'siteFromDB() with callback on a suffix'
+               );
+               $this->assertEquals(
+                       [ null, null ],
+                       $this->mConf->siteFromDB( 'wikien' ),
+                       'siteFromDB() with callback on a non-existing wiki'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::get
+        */
+       public function testParameterReplacement() {
+               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+               $this->assertEquals(
+                       'en wiki enwiki',
+                       $this->mConf->get( 'WithParams', 'enwiki', 'wiki' ),
+                       'get(): parameter replacement on an existing wiki'
+               );
+               $this->assertEquals(
+                       'de wiki dewiki',
+                       $this->mConf->get( 'WithParams', 'dewiki', 'wiki' ),
+                       'get(): parameter replacement on an existing wiki (2)'
+               );
+               $this->assertEquals(
+                       'fr wiki frwiki',
+                       $this->mConf->get( 'WithParams', 'frwiki', 'wiki' ),
+                       'get(): parameter replacement on an existing wiki (3)'
+               );
+               $this->assertEquals(
+                       ' wiki wiki',
+                       $this->mConf->get( 'WithParams', 'wiki', 'wiki' ),
+                       'get(): parameter replacement on an suffix'
+               );
+               $this->assertEquals(
+                       'es wiki eswiki',
+                       $this->mConf->get( 'WithParams', 'eswiki', 'wiki' ),
+                       'get(): parameter replacement on an non-existing wiki'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::getAll
+        */
+       public function testGetAllGlobals() {
+               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+               $getall = [
+                       'SimpleKey' => 'enwiki',
+                       'Fallback' => 'tag',
+                       'WithParams' => 'en wiki enwiki',
+                       'SomeGlobal' => [ 'enwiki' => 'enwiki' ] + $GLOBALS['SomeGlobal'],
+                       'MergeIt' => [
+                               'enwiki' => 'enwiki',
+                               'tag' => 'tag',
+                               'wiki' => 'wiki',
+                               'default' => 'default'
+                       ],
+               ];
+               $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' );
+
+               $this->mConf->extractAllGlobals( 'enwiki', 'wiki' );
+
+               $this->assertEquals(
+                       $getall['SimpleKey'],
+                       $GLOBALS['SimpleKey'],
+                       'extractAllGlobals(): simple setting'
+               );
+               $this->assertEquals(
+                       $getall['Fallback'],
+                       $GLOBALS['Fallback'],
+                       'extractAllGlobals(): fallback setting'
+               );
+               $this->assertEquals(
+                       $getall['WithParams'],
+                       $GLOBALS['WithParams'],
+                       'extractAllGlobals(): parameter replacement'
+               );
+               $this->assertEquals(
+                       $getall['SomeGlobal'],
+                       $GLOBALS['SomeGlobal'],
+                       'extractAllGlobals(): merging with global'
+               );
+               $this->assertEquals(
+                       $getall['MergeIt'],
+                       $GLOBALS['MergeIt'],
+                       'extractAllGlobals(): merging setting'
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Storage/PreparedEditTest.php b/tests/phpunit/unit/includes/Storage/PreparedEditTest.php
new file mode 100644 (file)
index 0000000..e3249e7
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace MediaWiki\Edit;
+
+use ParserOutput;
+
+/**
+ * @covers \MediaWiki\Edit\PreparedEdit
+ */
+class PreparedEditTest extends \MediaWikiUnitTestCase {
+       function testCallback() {
+               $output = new ParserOutput();
+               $edit = new PreparedEdit();
+               $edit->parserOutputCallback = function () {
+                       return new ParserOutput();
+               };
+
+               $this->assertEquals( $output, $edit->getOutput() );
+               $this->assertEquals( $output, $edit->output );
+       }
+}
diff --git a/tests/phpunit/unit/includes/XmlSelectTest.php b/tests/phpunit/unit/includes/XmlSelectTest.php
new file mode 100644 (file)
index 0000000..54d269e
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlSelectTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var XmlSelect
+        */
+       protected $select;
+
+       protected function setUp() {
+               parent::setUp();
+               $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 [
+                       /**
+                        * Values are set following a 3-bit Gray code where two successive
+                        * values differ by only one value.
+                        * See https://en.wikipedia.org/wiki/Gray_code
+                        */
+                       #      $name   $id    $default
+                       [ false, false, false, '<select></select>' ],
+                       [ false, false, 'foo', '<select></select>' ],
+                       [ false, 'id', 'foo', '<select id="id"></select>' ],
+                       [ false, 'id', false, '<select id="id"></select>' ],
+                       [ 'name', 'id', false, '<select name="name" id="id"></select>' ],
+                       [ 'name', 'id', 'foo', '<select name="name" id="id"></select>' ],
+                       [ 'name', false, 'foo', '<select name="name"></select>' ],
+                       [ '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'
+               );
+
+               # inexistent 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/unit/includes/auth/AuthenticationResponseTest.php b/tests/phpunit/unit/includes/auth/AuthenticationResponseTest.php
new file mode 100644 (file)
index 0000000..44b0631
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers \MediaWiki\Auth\AuthenticationResponse
+ */
+class AuthenticationResponseTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideConstructors
+        * @param string $constructor
+        * @param array $args
+        * @param array|Exception $expect
+        */
+       public function testConstructors( $constructor, $args, $expect ) {
+               if ( is_array( $expect ) ) {
+                       $res = new AuthenticationResponse();
+                       $res->messageType = 'warning';
+                       foreach ( $expect as $field => $value ) {
+                               $res->$field = $value;
+                       }
+                       $ret = call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+                       $this->assertEquals( $res, $ret );
+               } else {
+                       try {
+                               call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \Exception $ex ) {
+                               $this->assertEquals( $expect, $ex );
+                       }
+               }
+       }
+
+       public function provideConstructors() {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $msg = new \Message( 'mainpage' );
+
+               return [
+                       [ 'newPass', [], [
+                               'status' => AuthenticationResponse::PASS,
+                       ] ],
+                       [ 'newPass', [ 'name' ], [
+                               'status' => AuthenticationResponse::PASS,
+                               'username' => 'name',
+                       ] ],
+                       [ 'newPass', [ 'name', null ], [
+                               'status' => AuthenticationResponse::PASS,
+                               'username' => 'name',
+                       ] ],
+
+                       [ 'newFail', [ $msg ], [
+                               'status' => AuthenticationResponse::FAIL,
+                               'message' => $msg,
+                               'messageType' => 'error',
+                       ] ],
+
+                       [ 'newRestart', [ $msg ], [
+                               'status' => AuthenticationResponse::RESTART,
+                               'message' => $msg,
+                       ] ],
+
+                       [ 'newAbstain', [], [
+                               'status' => AuthenticationResponse::ABSTAIN,
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'warning',
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg, 'warning' ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'warning',
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg, 'error' ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'error',
+                       ] ],
+                       [ 'newUI', [ [], $msg ],
+                               new \InvalidArgumentException( '$reqs may not be empty' )
+                       ],
+
+                       [ 'newRedirect', [ [ $req ], 'http://example.org/redir' ], [
+                               'status' => AuthenticationResponse::REDIRECT,
+                               'neededRequests' => [ $req ],
+                               'redirectTarget' => 'http://example.org/redir',
+                       ] ],
+                       [
+                               'newRedirect',
+                               [ [ $req ], 'http://example.org/redir', [ 'foo' => 'bar' ] ],
+                               [
+                                       'status' => AuthenticationResponse::REDIRECT,
+                                       'neededRequests' => [ $req ],
+                                       'redirectTarget' => 'http://example.org/redir',
+                                       'redirectApiData' => [ 'foo' => 'bar' ],
+                               ]
+                       ],
+                       [ 'newRedirect', [ [], 'http://example.org/redir' ],
+                               new \InvalidArgumentException( '$reqs may not be empty' )
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/changes/ChangesListFilterGroupTest.php b/tests/phpunit/unit/includes/changes/ChangesListFilterGroupTest.php
new file mode 100644 (file)
index 0000000..bd54d50
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @covers ChangesListFilterGroup
+ */
+class ChangesListFilterGroupTest extends \MediaWikiUnitTestCase {
+       /**
+        * phpcs:disable Generic.Files.LineLength
+        * @expectedException MWException
+        * @expectedExceptionMessage Group names may not contain '_'.  Use the naming convention: 'camelCase'
+        * phpcs:enable
+        */
+       public function testReservedCharacter() {
+               new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'group_name',
+                               'priority' => 1,
+                               'filters' => [],
+                       ]
+               );
+       }
+
+       public function testAutoPriorities() {
+               $group = new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'groupName',
+                               'isFullCoverage' => true,
+                               'priority' => 1,
+                               'filters' => [
+                                       [ 'name' => 'hidefoo' ],
+                                       [ 'name' => 'hidebar' ],
+                                       [ 'name' => 'hidebaz' ],
+                               ],
+                       ]
+               );
+
+               $filters = $group->getFilters();
+               $this->assertEquals(
+                       [
+                               -2,
+                               -3,
+                               -4,
+                       ],
+                       array_map(
+                               function ( $f ) {
+                                       return $f->getPriority();
+                               },
+                               array_values( $filters )
+                       )
+               );
+       }
+
+       // Get without warnings
+       public function testGetFilter() {
+               $group = new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'groupName',
+                               'isFullCoverage' => true,
+                               'priority' => 1,
+                               'filters' => [
+                                       [ 'name' => 'foo' ],
+                               ],
+                       ]
+               );
+
+               $this->assertEquals(
+                       'foo',
+                       $group->getFilter( 'foo' )->getName()
+               );
+
+               $this->assertEquals(
+                       null,
+                       $group->getFilter( 'bar' )
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/HashConfigTest.php b/tests/phpunit/unit/includes/config/HashConfigTest.php
new file mode 100644 (file)
index 0000000..d46ee09
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+class HashConfigTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers HashConfig::newInstance
+        */
+       public function testNewInstance() {
+               $conf = HashConfig::newInstance();
+               $this->assertInstanceOf( HashConfig::class, $conf );
+       }
+
+       /**
+        * @covers HashConfig::__construct
+        */
+       public function testConstructor() {
+               $conf = new HashConfig();
+               $this->assertInstanceOf( HashConfig::class, $conf );
+
+               // Test passing arguments to the constructor
+               $conf2 = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $this->assertEquals( '1', $conf2->get( 'one' ) );
+       }
+
+       /**
+        * @covers HashConfig::get
+        */
+       public function testGet() {
+               $conf = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $this->assertEquals( '1', $conf->get( 'one' ) );
+               $this->setExpectedException( ConfigException::class, 'HashConfig::get: undefined option' );
+               $conf->get( 'two' );
+       }
+
+       /**
+        * @covers HashConfig::has
+        */
+       public function testHas() {
+               $conf = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $this->assertTrue( $conf->has( 'one' ) );
+               $this->assertFalse( $conf->has( 'two' ) );
+       }
+
+       /**
+        * @covers HashConfig::set
+        */
+       public function testSet() {
+               $conf = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $conf->set( 'two', '2' );
+               $this->assertEquals( '2', $conf->get( 'two' ) );
+               // Check that set overwrites
+               $conf->set( 'one', '3' );
+               $this->assertEquals( '3', $conf->get( 'one' ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/MultiConfigTest.php b/tests/phpunit/unit/includes/config/MultiConfigTest.php
new file mode 100644 (file)
index 0000000..4351151
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+class MultiConfigTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * Tests that settings are fetched in the right order
+        *
+        * @covers MultiConfig::__construct
+        * @covers MultiConfig::get
+        */
+       public function testGet() {
+               $multi = new MultiConfig( [
+                       new HashConfig( [ 'foo' => 'bar' ] ),
+                       new HashConfig( [ 'foo' => 'baz', 'bar' => 'foo' ] ),
+                       new HashConfig( [ 'bar' => 'baz' ] ),
+               ] );
+
+               $this->assertEquals( 'bar', $multi->get( 'foo' ) );
+               $this->assertEquals( 'foo', $multi->get( 'bar' ) );
+               $this->setExpectedException( ConfigException::class, 'MultiConfig::get: undefined option:' );
+               $multi->get( 'notset' );
+       }
+
+       /**
+        * @covers MultiConfig::has
+        */
+       public function testHas() {
+               $conf = new MultiConfig( [
+                       new HashConfig( [ 'foo' => 'foo' ] ),
+                       new HashConfig( [ 'something' => 'bleh' ] ),
+                       new HashConfig( [ 'meh' => 'eh' ] ),
+               ] );
+
+               $this->assertTrue( $conf->has( 'foo' ) );
+               $this->assertTrue( $conf->has( 'something' ) );
+               $this->assertTrue( $conf->has( 'meh' ) );
+               $this->assertFalse( $conf->has( 'what' ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/ServiceOptionsTest.php b/tests/phpunit/unit/includes/config/ServiceOptionsTest.php
new file mode 100644 (file)
index 0000000..c58c6f5
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+
+use MediaWiki\Config\ServiceOptions;
+
+/**
+ * @coversDefaultClass \MediaWiki\Config\ServiceOptions
+ */
+class ServiceOptionsTest extends \MediaWikiUnitTestCase {
+       public static $testObj;
+
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               self::$testObj = new stdclass();
+       }
+
+       /**
+        * @dataProvider provideConstructor
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        * @covers ::get
+        */
+       public function testConstructor( $expected, $keys, ...$sources ) {
+               $options = new ServiceOptions( $keys, ...$sources );
+
+               foreach ( $expected as $key => $val ) {
+                       $this->assertSame( $val, $options->get( $key ) );
+               }
+
+               // This is lumped in the same test because there's no support for depending on a test that
+               // has a data provider.
+               $options->assertRequiredOptions( array_keys( $expected ) );
+
+               // Suppress warning if no assertions were run. This is expected for empty arguments.
+               $this->assertTrue( true );
+       }
+
+       public function provideConstructor() {
+               return [
+                       'No keys' => [ [], [], [ 'a' => 'aval' ] ],
+                       'Simple array source' => [
+                               [ 'a' => 'aval', 'b' => 'bval' ],
+                               [ 'a', 'b' ],
+                               [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ],
+                       ],
+                       'Simple HashConfig source' => [
+                               [ 'a' => 'aval', 'b' => 'bval' ],
+                               [ 'a', 'b' ],
+                               new HashConfig( [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ] ),
+                       ],
+                       'Three different sources' => [
+                               [ 'a' => 'aval', 'b' => 'bval' ],
+                               [ 'a', 'b' ],
+                               [ 'z' => 'zval' ],
+                               new HashConfig( [ 'a' => 'aval', 'c' => 'cval' ] ),
+                               [ 'b' => 'bval', 'd' => 'dval' ],
+                       ],
+                       'null key' => [
+                               [ 'a' => null ],
+                               [ 'a' ],
+                               [ 'a' => null ],
+                       ],
+                       'Numeric option name' => [
+                               [ '0' => 'nothing' ],
+                               [ '0' ],
+                               [ '0' => 'nothing' ],
+                       ],
+                       'Multiple sources for one key' => [
+                               [ 'a' => 'winner' ],
+                               [ 'a' ],
+                               [ 'a' => 'winner' ],
+                               [ 'a' => 'second place' ],
+                       ],
+                       'Object value is passed by reference' => [
+                               [ 'a' => self::$testObj ],
+                               [ 'a' ],
+                               [ 'a' => self::$testObj ],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers ::__construct
+        */
+       public function testKeyNotFound() {
+               $this->setExpectedException( InvalidArgumentException::class,
+                       'Key "a" not found in input sources' );
+
+               new ServiceOptions( [ 'a' ], [ 'b' => 'bval' ], [ 'c' => 'cval' ] );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testOutOfOrderAssertRequiredOptions() {
+               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
+               $options->assertRequiredOptions( [ 'b', 'a' ] );
+               $this->assertTrue( true, 'No exception thrown' );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::get
+        */
+       public function testGetUnrecognized() {
+               $this->setExpectedException( InvalidArgumentException::class,
+                       'Unrecognized option "b"' );
+
+               $options = new ServiceOptions( [ 'a' ], [ 'a' => '' ] );
+               $options->get( 'b' );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testExtraKeys() {
+               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
+                       'Precondition failed: Unsupported options passed: b, c!' );
+
+               $options = new ServiceOptions( [ 'a', 'b', 'c' ], [ 'a' => '', 'b' => '', 'c' => '' ] );
+               $options->assertRequiredOptions( [ 'a' ] );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testMissingKeys() {
+               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
+                       'Precondition failed: Required options missing: a, b!' );
+
+               $options = new ServiceOptions( [ 'c' ], [ 'c' => '' ] );
+               $options->assertRequiredOptions( [ 'a', 'b', 'c' ] );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testExtraAndMissingKeys() {
+               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
+                       'Precondition failed: Unsupported options passed: b! Required options missing: c!' );
+
+               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
+               $options->assertRequiredOptions( [ 'a', 'c' ] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/content/JsonContentHandlerTest.php b/tests/phpunit/unit/includes/content/JsonContentHandlerTest.php
new file mode 100644 (file)
index 0000000..70db73c
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+class JsonContentHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers JsonContentHandler::makeEmptyContent
+        */
+       public function testMakeEmptyContent() {
+               $handler = new JsonContentHandler();
+               $content = $handler->makeEmptyContent();
+               $this->assertInstanceOf( JsonContent::class, $content );
+               $this->assertTrue( $content->isValid() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/MonologSpiTest.php b/tests/phpunit/unit/includes/debug/logger/MonologSpiTest.php
new file mode 100644 (file)
index 0000000..ecb5d17
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/**
+ * 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
+ */
+
+namespace MediaWiki\Logger;
+
+use Wikimedia\TestingAccessWrapper;
+
+class MonologSpiTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers MediaWiki\Logger\MonologSpi::mergeConfig
+        */
+       public function testMergeConfig() {
+               $base = [
+                       'loggers' => [
+                               '@default' => [
+                                       'processors' => [ 'constructor' ],
+                                       'handlers' => [ 'constructor' ],
+                               ],
+                       ],
+                       'processors' => [
+                               'constructor' => [
+                                       'class' => 'constructor',
+                               ],
+                       ],
+                       'handlers' => [
+                               'constructor' => [
+                                       'class' => 'constructor',
+                                       'formatter' => 'constructor',
+                               ],
+                       ],
+                       'formatters' => [
+                               'constructor' => [
+                                       'class' => 'constructor',
+                               ],
+                       ],
+               ];
+
+               $fixture = new MonologSpi( $base );
+               $this->assertSame(
+                       $base,
+                       TestingAccessWrapper::newFromObject( $fixture )->config
+               );
+
+               $fixture->mergeConfig( [
+                       'loggers' => [
+                               'merged' => [
+                                       'processors' => [ 'merged' ],
+                                       'handlers' => [ 'merged' ],
+                               ],
+                       ],
+                       'processors' => [
+                               'merged' => [
+                                       'class' => 'merged',
+                               ],
+                       ],
+                       'magic' => [
+                               'idkfa' => [ 'xyzzy' ],
+                       ],
+                       'handlers' => [
+                               'merged' => [
+                                       'class' => 'merged',
+                                       'formatter' => 'merged',
+                               ],
+                       ],
+                       'formatters' => [
+                               'merged' => [
+                                       'class' => 'merged',
+                               ],
+                       ],
+               ] );
+               $this->assertSame(
+                       [
+                               'loggers' => [
+                                       '@default' => [
+                                               'processors' => [ 'constructor' ],
+                                               'handlers' => [ 'constructor' ],
+                                       ],
+                                       'merged' => [
+                                               'processors' => [ 'merged' ],
+                                               'handlers' => [ 'merged' ],
+                                       ],
+                               ],
+                               'processors' => [
+                                       'constructor' => [
+                                               'class' => 'constructor',
+                                       ],
+                                       'merged' => [
+                                               'class' => 'merged',
+                                       ],
+                               ],
+                               'handlers' => [
+                                       'constructor' => [
+                                               'class' => 'constructor',
+                                               'formatter' => 'constructor',
+                                       ],
+                                       'merged' => [
+                                               'class' => 'merged',
+                                               'formatter' => 'merged',
+                                       ],
+                               ],
+                               'formatters' => [
+                                       'constructor' => [
+                                               'class' => 'constructor',
+                                       ],
+                                       'merged' => [
+                                               'class' => 'merged',
+                                       ],
+                               ],
+                               'magic' => [
+                                       'idkfa' => [ 'xyzzy' ],
+                               ],
+                       ],
+                       TestingAccessWrapper::newFromObject( $fixture )->config
+               );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/AvroFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/AvroFormatterTest.php
new file mode 100644 (file)
index 0000000..e091561
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/**
+ * 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
+ */
+
+namespace MediaWiki\Logger\Monolog;
+
+use PHPUnit_Framework_Error_Notice;
+
+/**
+ * @covers \MediaWiki\Logger\Monolog\AvroFormatter
+ */
+class AvroFormatterTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               if ( !class_exists( 'AvroStringIO' ) ) {
+                       $this->markTestSkipped( 'Avro is required for the AvroFormatterTest' );
+               }
+               parent::setUp();
+       }
+
+       public function testSchemaNotAvailable() {
+               $formatter = new AvroFormatter( [] );
+               $this->setExpectedException(
+                       'PHPUnit_Framework_Error_Notice',
+                       "The schema for channel 'marty' is not available"
+               );
+               $formatter->format( [ 'channel' => 'marty' ] );
+       }
+
+       public function testSchemaNotAvailableReturnValue() {
+               $formatter = new AvroFormatter( [] );
+               $noticeEnabled = PHPUnit_Framework_Error_Notice::$enabled;
+               // disable conversion of notices
+               PHPUnit_Framework_Error_Notice::$enabled = false;
+               // have to keep the user notice from being output
+               \Wikimedia\suppressWarnings();
+               $res = $formatter->format( [ 'channel' => 'marty' ] );
+               \Wikimedia\restoreWarnings();
+               PHPUnit_Framework_Error_Notice::$enabled = $noticeEnabled;
+               $this->assertNull( $res );
+       }
+
+       public function testDoesSomethingWhenSchemaAvailable() {
+               $formatter = new AvroFormatter( [
+                       'string' => [
+                               'schema' => [ 'type' => 'string' ],
+                               'revision' => 1010101,
+                       ]
+               ] );
+               $res = $formatter->format( [
+                       'channel' => 'string',
+                       'context' => 'better to be',
+               ] );
+               $this->assertNotNull( $res );
+               // basically just tell us if avro changes its string encoding, or if
+               // we completely fail to generate a log message.
+               $this->assertEquals( 'AAAAAAAAD2m1GGJldHRlciB0byBiZQ==', base64_encode( $res ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/KafkaHandlerTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/KafkaHandlerTest.php
new file mode 100644 (file)
index 0000000..bbac17f
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/**
+ * 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
+ */
+
+namespace MediaWiki\Logger\Monolog;
+
+use Monolog\Logger;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers \MediaWiki\Logger\Monolog\KafkaHandler
+ */
+class KafkaHandlerTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               if ( !class_exists( 'Monolog\Handler\AbstractProcessingHandler' )
+                       || !class_exists( 'Kafka\Produce' )
+               ) {
+                       $this->markTestSkipped( 'Monolog and Kafka are required for the KafkaHandlerTest' );
+               }
+
+               parent::setUp();
+       }
+
+       public function topicNamingProvider() {
+               return [
+                       [ [], 'monolog_foo' ],
+                       [ [ 'alias' => [ 'foo' => 'bar' ] ], 'bar' ]
+               ];
+       }
+
+       /**
+        * @dataProvider topicNamingProvider
+        */
+       public function testTopicNaming( $options, $expect ) {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $produce->expects( $this->once() )
+                       ->method( 'setMessages' )
+                       ->with( $expect, $this->anything(), $this->anything() );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+
+               $handler = new KafkaHandler( $produce, $options );
+               $handler->handle( [
+                       'channel' => 'foo',
+                       'level' => Logger::EMERGENCY,
+                       'extra' => [],
+                       'context' => [],
+               ] );
+       }
+
+       public function swallowsExceptionsWhenRequested() {
+               return [
+                       // defaults to false
+                       [ [], true ],
+                       // also try false explicitly
+                       [ [ 'swallowExceptions' => false ], true ],
+                       // turn it on
+                       [ [ 'swallowExceptions' => true ], false ],
+               ];
+       }
+
+       /**
+        * @dataProvider swallowsExceptionsWhenRequested
+        */
+       public function testGetAvailablePartitionsException( $options, $expectException ) {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->throwException( new \Kafka\Exception ) );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+
+               if ( $expectException ) {
+                       $this->setExpectedException( 'Kafka\Exception' );
+               }
+
+               $handler = new KafkaHandler( $produce, $options );
+               $handler->handle( [
+                       'channel' => 'foo',
+                       'level' => Logger::EMERGENCY,
+                       'extra' => [],
+                       'context' => [],
+               ] );
+
+               if ( !$expectException ) {
+                       $this->assertTrue( true, 'no exception was thrown' );
+               }
+       }
+
+       /**
+        * @dataProvider swallowsExceptionsWhenRequested
+        */
+       public function testSendException( $options, $expectException ) {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->throwException( new \Kafka\Exception ) );
+
+               if ( $expectException ) {
+                       $this->setExpectedException( 'Kafka\Exception' );
+               }
+
+               $handler = new KafkaHandler( $produce, $options );
+               $handler->handle( [
+                       'channel' => 'foo',
+                       'level' => Logger::EMERGENCY,
+                       'extra' => [],
+                       'context' => [],
+               ] );
+
+               if ( !$expectException ) {
+                       $this->assertTrue( true, 'no exception was thrown' );
+               }
+       }
+
+       public function testHandlesNullFormatterResult() {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $mockMethod = $produce->expects( $this->exactly( 2 ) )
+                       ->method( 'setMessages' );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+               // evil hax
+               $matcher = TestingAccessWrapper::newFromObject( $mockMethod )->matcher;
+               TestingAccessWrapper::newFromObject( $matcher )->parametersMatcher =
+                       new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [
+                               [ $this->anything(), $this->anything(), [ 'words' ] ],
+                               [ $this->anything(), $this->anything(), [ 'lines' ] ]
+                       ] );
+
+               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
+               $formatter->expects( $this->any() )
+                       ->method( 'format' )
+                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
+
+               $handler = new KafkaHandler( $produce, [] );
+               $handler->setFormatter( $formatter );
+               for ( $i = 0; $i < 3; ++$i ) {
+                       $handler->handle( [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ] );
+               }
+       }
+
+       public function testBatchHandlesNullFormatterResult() {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $produce->expects( $this->once() )
+                       ->method( 'setMessages' )
+                       ->with( $this->anything(), $this->anything(), [ 'words', 'lines' ] );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+
+               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
+               $formatter->expects( $this->any() )
+                       ->method( 'format' )
+                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
+
+               $handler = new KafkaHandler( $produce, [] );
+               $handler->setFormatter( $formatter );
+               $handler->handleBatch( [
+                       [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ],
+                       [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ],
+                       [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ],
+               ] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/LineFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/LineFormatterTest.php
new file mode 100644 (file)
index 0000000..8da3d93
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * 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
+ */
+
+namespace MediaWiki\Logger\Monolog;
+
+use AssertionError;
+use InvalidArgumentException;
+use LengthException;
+use LogicException;
+use Wikimedia\TestingAccessWrapper;
+
+class LineFormatterTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               if ( !class_exists( 'Monolog\Formatter\LineFormatter' ) ) {
+                       $this->markTestSkipped( 'This test requires monolog to be installed' );
+               }
+               parent::setUp();
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionNoTrace() {
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( false );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new LogicException( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
+               $this->assertNotContains( "\n  #0", $out );
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionTrace() {
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( true );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new LogicException( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
+               $this->assertContains( "\n  #0", $out );
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionErrorNoTrace() {
+               if ( !class_exists( AssertionError::class ) ) {
+                       $this->markTestSkipped( 'AssertionError class does not exist' );
+               }
+
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( false );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new AssertionError( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
+               $this->assertNotContains( "\n  #0", $out );
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionErrorTrace() {
+               if ( !class_exists( AssertionError::class ) ) {
+                       $this->markTestSkipped( 'AssertionError class does not exist' );
+               }
+
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( true );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new AssertionError( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
+               $this->assertContains( "\n  #0", $out );
+       }
+}
diff --git a/tests/phpunit/unit/includes/diff/ArrayDiffFormatterTest.php b/tests/phpunit/unit/includes/diff/ArrayDiffFormatterTest.php
new file mode 100644 (file)
index 0000000..d436991
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @group Diff
+ */
+class ArrayDiffFormatterTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @param Diff $input
+        * @param array $expectedOutput
+        * @dataProvider provideTestFormat
+        * @covers ArrayDiffFormatter::format
+        */
+       public function testFormat( $input, $expectedOutput ) {
+               $instance = new ArrayDiffFormatter();
+               $output = $instance->format( $input );
+               $this->assertEquals( $expectedOutput, $output );
+       }
+
+       private function getMockDiff( $edits ) {
+               $diff = $this->getMockBuilder( Diff::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $diff->expects( $this->any() )
+                       ->method( 'getEdits' )
+                       ->will( $this->returnValue( $edits ) );
+               return $diff;
+       }
+
+       private function getMockDiffOp( $type = null, $orig = [], $closing = [] ) {
+               $diffOp = $this->getMockBuilder( DiffOp::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $diffOp->expects( $this->any() )
+                       ->method( 'getType' )
+                       ->will( $this->returnValue( $type ) );
+               $diffOp->expects( $this->any() )
+                       ->method( 'getOrig' )
+                       ->will( $this->returnValue( $orig ) );
+               if ( $type === 'change' ) {
+                       $diffOp->expects( $this->any() )
+                               ->method( 'getClosing' )
+                               ->with( $this->isType( 'integer' ) )
+                               ->will( $this->returnCallback( function () {
+                                       return 'mockLine';
+                               } ) );
+               } else {
+                       $diffOp->expects( $this->any() )
+                               ->method( 'getClosing' )
+                               ->will( $this->returnValue( $closing ) );
+               }
+               return $diffOp;
+       }
+
+       public function provideTestFormat() {
+               $emptyArrayTestCases = [
+                       $this->getMockDiff( [] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'change' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'FOOBARBAZ' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', 'line' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [], [ 'line' ] ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy', [], [ 'line' ] ) ] ),
+               ];
+
+               $otherTestCases = [];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1' ] ) ] ),
+                       [ [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ] ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1', 'a2' ] ) ] ),
+                       [
+                               [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ],
+                               [ 'action' => 'add', 'new' => 'a2', 'newline' => 2 ],
+                       ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1' ] ) ] ),
+                       [ [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ] ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1', 'd2' ] ) ] ),
+                       [
+                               [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ],
+                               [ 'action' => 'delete', 'old' => 'd2', 'oldline' => 2 ],
+                       ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'change', [ 'd1' ], [ 'a1' ] ) ] ),
+                       [ [
+                               'action' => 'change',
+                               'old' => 'd1',
+                               'new' => 'mockLine',
+                               'newline' => 1, 'oldline' => 1
+                       ] ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp(
+                               'change',
+                               [ 'd1', 'd2' ],
+                               [ 'a1', 'a2' ]
+                       ) ] ),
+                       [
+                               [
+                                       'action' => 'change',
+                                       'old' => 'd1',
+                                       'new' => 'mockLine',
+                                       'newline' => 1, 'oldline' => 1
+                               ],
+                               [
+                                       'action' => 'change',
+                                       'old' => 'd2',
+                                       'new' => 'mockLine',
+                                       'newline' => 2, 'oldline' => 2
+                               ],
+                       ],
+               ];
+
+               $testCases = [];
+               foreach ( $emptyArrayTestCases as $testCase ) {
+                       $testCases[] = [ $testCase, [] ];
+               }
+               foreach ( $otherTestCases as $testCase ) {
+                       $testCases[] = [ $testCase[0], $testCase[1] ];
+               }
+               return $testCases;
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/DiffOpTest.php b/tests/phpunit/unit/includes/diff/DiffOpTest.php
new file mode 100644 (file)
index 0000000..4e1aced
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @author Addshore
+ *
+ * @group Diff
+ */
+class DiffOpTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers DiffOp::getType
+        */
+       public function testGetType() {
+               $obj = new FakeDiffOp();
+               $obj->type = 'foo';
+               $this->assertEquals( 'foo', $obj->getType() );
+       }
+
+       /**
+        * @covers DiffOp::getOrig
+        */
+       public function testGetOrig() {
+               $obj = new FakeDiffOp();
+               $obj->orig = [ 'foo' ];
+               $this->assertEquals( [ 'foo' ], $obj->getOrig() );
+       }
+
+       /**
+        * @covers DiffOp::getClosing
+        */
+       public function testGetClosing() {
+               $obj = new FakeDiffOp();
+               $obj->closing = [ 'foo' ];
+               $this->assertEquals( [ 'foo' ], $obj->getClosing() );
+       }
+
+       /**
+        * @covers DiffOp::getClosing
+        */
+       public function testGetClosingWithParameter() {
+               $obj = new FakeDiffOp();
+               $obj->closing = [ 'foo', 'bar', 'baz' ];
+               $this->assertEquals( 'foo', $obj->getClosing( 0 ) );
+               $this->assertEquals( 'bar', $obj->getClosing( 1 ) );
+               $this->assertEquals( 'baz', $obj->getClosing( 2 ) );
+               $this->assertEquals( null, $obj->getClosing( 3 ) );
+       }
+
+       /**
+        * @covers DiffOp::norig
+        */
+       public function testNorig() {
+               $obj = new FakeDiffOp();
+               $this->assertEquals( 0, $obj->norig() );
+               $obj->orig = [ 'foo' ];
+               $this->assertEquals( 1, $obj->norig() );
+       }
+
+       /**
+        * @covers DiffOp::nclosing
+        */
+       public function testNclosing() {
+               $obj = new FakeDiffOp();
+               $this->assertEquals( 0, $obj->nclosing() );
+               $obj->closing = [ 'foo' ];
+               $this->assertEquals( 1, $obj->nclosing() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/DiffTest.php b/tests/phpunit/unit/includes/diff/DiffTest.php
new file mode 100644 (file)
index 0000000..f0a8490
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @group Diff
+ */
+class DiffTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers Diff::getEdits
+        */
+       public function testGetEdits() {
+               $obj = new Diff( [], [] );
+               $obj->edits = 'FooBarBaz';
+               $this->assertEquals( 'FooBarBaz', $obj->getEdits() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php b/tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php
new file mode 100644 (file)
index 0000000..2b021c4
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ * @file
+ */
+
+class MWExceptionHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers MWExceptionHandler::getRedactedTrace
+        */
+       public function testGetRedactedTrace() {
+               $refvar = 'value';
+               try {
+                       $array = [ 'a', 'b' ];
+                       $object = new stdClass();
+                       self::helperThrowAnException( $array, $object, $refvar );
+               } catch ( Exception $e ) {
+               }
+
+               # Make sure our stack 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 );
+                       }
+               }
+
+               $this->assertEquals( 'value', $refvar, 'Ensuring reference variable wasn\'t changed' );
+       }
+
+       /**
+        * Helper function for testExpandArgumentsInCall
+        *
+        * Pass it an object and an array, and something by reference :-)
+        *
+        * @throws Exception
+        */
+       protected static function helperThrowAnException( $a, $b, &$c ) {
+               throw new Exception();
+       }
+}
diff --git a/tests/phpunit/unit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/unit/includes/installer/InstallDocFormatterTest.php
new file mode 100644 (file)
index 0000000..fddc3b8
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+class InstallDocFormatterTest extends \MediaWikiUnitTestCase {
+       /**
+        * @covers InstallDocFormatter
+        * @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 [
+                       # Escape some wikitext
+                       [ 'Install &lt;tag>', 'Install <tag>', 'Escaping <' ],
+                       [ 'Install &#123;&#123;template}}', 'Install {{template}}', 'Escaping [[' ],
+                       [ 'Install &#91;&#91;page]]', 'Install [[page]]', 'Escaping {{' ],
+                       [ 'Install &#95;&#95;TOC&#95;&#95;', 'Install __TOC__', 'Escaping __' ],
+                       [ 'Install ', "Install \r", 'Removing \r' ],
+
+                       # Transform \t{1,2} into :{1,2}
+                       [ ':One indentation', "\tOne indentation", 'Replacing a single \t' ],
+                       [ '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ],
+
+                       # Transform 'T123' links
+                       [
+                               '<span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
+                               'T123', 'Testing T123 links' ],
+                       [
+                               'bug <span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
+                               'bug T123', 'Testing bug T123 links' ],
+                       [
+                               '(<span class="config-plainlink">[https://phabricator.wikimedia.org/T987654 T987654]</span>)',
+                               '(T987654)', 'Testing (T987654) links' ],
+
+                       # "Tabc" shouldn't work
+                       [ 'Tfoobar', 'Tfoobar', "Don't match T followed by non-digits" ],
+                       [ 'T!!fakefake!!', 'T!!fakefake!!', "Don't match T followed by non-digits" ],
+
+                       # Transform 'bug 123' links
+                       [
+                               '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>',
+                               'bug 123', 'Testing bug 123 links' ],
+                       [
+                               '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)',
+                               '(bug 987654)', 'Testing (bug 987654) links' ],
+
+                       # "bug abc" shouldn't work
+                       [ 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ],
+                       [ 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ],
+
+                       # Transform '$wgFooBar' links
+                       [
+                               '<span class="config-plainlink">'
+                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
+                               '$wgFooBar', 'Testing basic $wgFooBar' ],
+                       [
+                               '<span class="config-plainlink">'
+                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
+                               '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ],
+                       [
+                               '<span class="config-plainlink">'
+                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>',
+                               '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ],
+
+                       # Icky variables that shouldn't link
+                       [
+                               '$myAwesomeVariable',
+                               '$myAwesomeVariable',
+                               'Testing $myAwesomeVariable (not starting with $wg)'
+                       ],
+                       [ '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/installer/OracleInstallerTest.php b/tests/phpunit/unit/includes/installer/OracleInstallerTest.php
new file mode 100644 (file)
index 0000000..69b5552
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @group Installer
+ */
+class OracleInstallerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @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 [
+                       [ true, 'simple_01', 'Simple TNS name' ],
+                       [ true, 'simple_01.world', 'TNS name with domain' ],
+                       [ true, 'simple_01.domain.net', 'TNS name with domain' ],
+                       [ true, 'host123', 'Host only' ],
+                       [ true, 'host123.domain.net', 'FQDN only' ],
+                       [ true, '//host123.domain.net', 'FQDN URL only' ],
+                       [ true, '123.223.213.132', 'Host IP only' ],
+                       [ true, 'host:1521', 'Host and port' ],
+                       [ true, 'host:1521/service', 'Host, port and service' ],
+                       [ true, 'host:1521/service:shared', 'Host, port, service and shared server type' ],
+                       [ true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ],
+                       [ true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ],
+                       [
+                               true,
+                               'host:1521/service:shared/instance1',
+                               'Host, port, service, server type and instance'
+                       ],
+                       [ true, 'host:1521//instance1', 'Host, port and instance' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/interwiki/InterwikiLookupAdapterTest.php b/tests/phpunit/unit/includes/interwiki/InterwikiLookupAdapterTest.php
new file mode 100644 (file)
index 0000000..abbd2d7
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+
+use MediaWiki\Interwiki\InterwikiLookupAdapter;
+
+/**
+ * @covers MediaWiki\Interwiki\InterwikiLookupAdapter
+ *
+ * @group MediaWiki
+ * @group Interwiki
+ */
+class InterwikiLookupAdapterTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var InterwikiLookupAdapter
+        */
+       private $interwikiLookup;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->interwikiLookup = new InterwikiLookupAdapter(
+                       $this->getSiteLookup( $this->getSites() )
+               );
+       }
+
+       public function testIsValidInterwiki() {
+               $this->assertTrue(
+                       $this->interwikiLookup->isValidInterwiki( 'enwt' ),
+                       'enwt known prefix is valid'
+               );
+               $this->assertTrue(
+                       $this->interwikiLookup->isValidInterwiki( 'foo' ),
+                       'foo site known prefix is valid'
+               );
+               $this->assertFalse(
+                       $this->interwikiLookup->isValidInterwiki( 'xyz' ),
+                       'unknown prefix is not valid'
+               );
+       }
+
+       public function testFetch() {
+               $interwiki = $this->interwikiLookup->fetch( '' );
+               $this->assertNull( $interwiki );
+
+               $interwiki = $this->interwikiLookup->fetch( 'xyz' );
+               $this->assertFalse( $interwiki );
+
+               $interwiki = $this->interwikiLookup->fetch( 'foo' );
+               $this->assertInstanceOf( Interwiki::class, $interwiki );
+               $this->assertSame( 'foobar', $interwiki->getWikiID() );
+
+               $interwiki = $this->interwikiLookup->fetch( 'enwt' );
+               $this->assertInstanceOf( Interwiki::class, $interwiki );
+
+               $this->assertSame( 'https://en.wiktionary.org/wiki/$1', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( 'https://en.wiktionary.org/w/api.php', $interwiki->getAPI(), 'getAPI' );
+               $this->assertSame( 'enwiktionary', $interwiki->getWikiID(), 'getWikiID' );
+               $this->assertTrue( $interwiki->isLocal(), 'isLocal' );
+       }
+
+       public function testGetAllPrefixes() {
+               $foo = [
+                       'iw_prefix' => 'foo',
+                       'iw_url' => '',
+                       'iw_api' => '',
+                       'iw_wikiid' => 'foobar',
+                       'iw_local' => false,
+                       'iw_trans' => false,
+               ];
+               $enwt = [
+                       'iw_prefix' => 'enwt',
+                       'iw_url' => 'https://en.wiktionary.org/wiki/$1',
+                       'iw_api' => 'https://en.wiktionary.org/w/api.php',
+                       'iw_wikiid' => 'enwiktionary',
+                       'iw_local' => true,
+                       'iw_trans' => false,
+               ];
+
+               $this->assertEquals(
+                       [ $foo, $enwt ],
+                       $this->interwikiLookup->getAllPrefixes(),
+                       'getAllPrefixes()'
+               );
+
+               $this->assertEquals(
+                       [ $foo ],
+                       $this->interwikiLookup->getAllPrefixes( false ),
+                       'get external prefixes'
+               );
+
+               $this->assertEquals(
+                       [ $enwt ],
+                       $this->interwikiLookup->getAllPrefixes( true ),
+                       'get local prefixes'
+               );
+       }
+
+       private function getSiteLookup( SiteList $sites ) {
+               $siteLookup = $this->getMockBuilder( SiteLookup::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $siteLookup->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( $sites ) );
+
+               return $siteLookup;
+       }
+
+       private function getSites() {
+               $sites = [];
+
+               $site = new Site();
+               $site->setGlobalId( 'foobar' );
+               $site->addInterwikiId( 'foo' );
+               $site->setSource( 'external' );
+               $sites[] = $site;
+
+               $site = new MediaWikiSite();
+               $site->setGlobalId( 'enwiktionary' );
+               $site->setGroup( 'wiktionary' );
+               $site->setLanguageCode( 'en' );
+               $site->addNavigationId( 'enwiktionary' );
+               $site->addInterwikiId( 'enwt' );
+               $site->setSource( 'local' );
+               $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
+               $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
+               $sites[] = $site;
+
+               return new SiteList( $sites );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/unit/includes/libs/objectcache/ReplicatedBagOStuffTest.php
new file mode 100644 (file)
index 0000000..64d282f
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+class ReplicatedBagOStuffTest extends \MediaWikiUnitTestCase {
+       /** @var HashBagOStuff */
+       private $writeCache;
+       /** @var HashBagOStuff */
+       private $readCache;
+       /** @var ReplicatedBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->writeCache = new HashBagOStuff();
+               $this->readCache = new HashBagOStuff();
+               $this->cache = new ReplicatedBagOStuff( [
+                       'writeFactory' => $this->writeCache,
+                       'readFactory' => $this->readCache,
+               ] );
+       }
+
+       /**
+        * @covers ReplicatedBagOStuff::set
+        */
+       public function testSet() {
+               $key = 'a key';
+               $value = 'a value';
+               $this->cache->set( $key, $value );
+
+               // Write to master.
+               $this->assertEquals( $value, $this->writeCache->get( $key ) );
+               // Don't write to replica. Replication is deferred to backend.
+               $this->assertFalse( $this->readCache->get( $key ) );
+       }
+
+       /**
+        * @covers ReplicatedBagOStuff::get
+        */
+       public function testGet() {
+               $key = 'a key';
+
+               $write = 'one value';
+               $this->writeCache->set( $key, $write );
+               $read = 'another value';
+               $this->readCache->set( $key, $read );
+
+               // Read from replica.
+               $this->assertEquals( $read, $this->cache->get( $key ) );
+       }
+
+       /**
+        * @covers ReplicatedBagOStuff::get
+        */
+       public function testGetAbsent() {
+               $key = 'a key';
+               $value = 'a value';
+               $this->writeCache->set( $key, $value );
+
+               // Don't read from master. No failover if value is absent.
+               $this->assertFalse( $this->cache->get( $key ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/IPTCTest.php b/tests/phpunit/unit/includes/media/IPTCTest.php
new file mode 100644 (file)
index 0000000..430493c
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @group Media
+ */
+class IPTCTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @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( [ '¼' ], $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( [ 'ÃÃø' ], $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( [ 'ø' ], $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( [ '¼' ], $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( [ '¼', '¼½' ], $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( [ '¼' ], $res['Keywords'] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/MediaHandlerTest.php b/tests/phpunit/unit/includes/media/MediaHandlerTest.php
new file mode 100644 (file)
index 0000000..eb4ece8
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @group Media
+ */
+class MediaHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers MediaHandler::fitBoxWidth
+        *
+        * @dataProvider provideTestFitBoxWidth
+        */
+       public function testFitBoxWidth( $width, $height, $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: {z$result}x$y2" );
+       }
+
+       public static function provideTestFitBoxWidth() {
+               return array_merge(
+                       static::generateTestFitBoxWidthData( 50, 50, [
+                                       50 => 50,
+                                       17 => 17,
+                                       18 => 18 ]
+                       ),
+                       static::generateTestFitBoxWidthData( 366, 300, [
+                                       50 => 61,
+                                       17 => 21,
+                                       18 => 22 ]
+                       ),
+                       static::generateTestFitBoxWidthData( 300, 366, [
+                                       50 => 41,
+                                       17 => 14,
+                                       18 => 15 ]
+                       ),
+                       static::generateTestFitBoxWidthData( 100, 400, [
+                                       50 => 12,
+                                       17 => 4,
+                                       18 => 4 ]
+                       )
+               );
+       }
+
+       /**
+        * Generate single test cases by combining the dimensions and tests contents
+        *
+        * It creates:
+        * [$width, $height, $max, $expected],
+        * [$width, $height, $max2, $expected2], ...
+        * out of parameters:
+        * $width, $height, { $max => $expected, $max2 => $expected2, ... }
+        *
+        * @param int $width
+        * @param int $height
+        * @param array $tests associative array of $max => $expected values
+        * @return array
+        */
+       private static function generateTestFitBoxWidthData( $width, $height, $tests ) {
+               $result = [];
+               foreach ( $tests as $max => $expected ) {
+                       $result[] = [ $width, $height, $max, $expected ];
+               }
+               return $result;
+       }
+}
diff --git a/tests/phpunit/unit/includes/objectcache/MemcachedBagOStuffTest.php b/tests/phpunit/unit/includes/objectcache/MemcachedBagOStuffTest.php
new file mode 100644 (file)
index 0000000..eb040b4
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @group BagOStuff
+ */
+class MemcachedBagOStuffTest extends \MediaWikiUnitTestCase {
+       /** @var MemcachedBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->cache = new MemcachedPhpBagOStuff( [ 'keyspace' => 'test', 'servers' => [] ] );
+       }
+
+       /**
+        * @covers MemcachedBagOStuff::makeKey
+        */
+       public function testKeyNormalization() {
+               $this->assertEquals(
+                       'test:vanilla',
+                       $this->cache->makeKey( 'vanilla' )
+               );
+
+               $this->assertEquals(
+                       'test:punctuation_marks_are_ok:!@$^&*()',
+                       $this->cache->makeKey( 'punctuation_marks_are_ok', '!@$^&*()' )
+               );
+
+               $this->assertEquals(
+                       'test:but_spaces:hashes%23:and%0Anewlines:are_not',
+                       $this->cache->makeKey( 'but spaces', 'hashes#', "and\nnewlines", 'are_not' )
+               );
+
+               $this->assertEquals(
+                       'test:this:key:contains:%F0%9D%95%9E%F0%9D%95%A6%F0%9D%95%9D%F0%9D%95%A5%F0%9' .
+                               'D%95%9A%F0%9D%95%93%F0%9D%95%AA%F0%9D%95%A5%F0%9D%95%96:characters',
+                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖', 'characters' )
+               );
+
+               $this->assertEquals(
+                       'test:this:key:contains:#c118f92685a635cb843039de50014c9c',
+                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕥𝕠𝕠 𝕞𝕒𝕟𝕪 𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖 𝕔𝕙𝕒𝕣𝕒𝕔𝕥𝕖𝕣𝕤' )
+               );
+
+               $this->assertEquals(
+                       'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5',
+                       $this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙',
+                               '𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' )
+               );
+
+               $this->assertEquals(
+                       'test:%23%235820ad1d105aa4dc698585c39df73e19',
+                       $this->cache->makeKey( '##5820ad1d105aa4dc698585c39df73e19' )
+               );
+
+               $this->assertEquals(
+                       'test:percent_is_escaped:!@$%25^&*()',
+                       $this->cache->makeKey( 'percent_is_escaped', '!@$%^&*()' )
+               );
+
+               $this->assertEquals(
+                       'test:colon_is_escaped:!@$%3A^&*()',
+                       $this->cache->makeKey( 'colon_is_escaped', '!@$:^&*()' )
+               );
+
+               $this->assertEquals(
+                       'test:long_key_part_hashed:#0244f7b1811d982dd932dd7de01465ac',
+                       $this->cache->makeKey( 'long_key_part_hashed', str_repeat( 'y', 500 ) )
+               );
+       }
+
+       /**
+        * @dataProvider validKeyProvider
+        * @covers MemcachedBagOStuff::validateKeyEncoding
+        */
+       public function testValidateKeyEncoding( $key ) {
+               $this->assertSame( $key, $this->cache->validateKeyEncoding( $key ) );
+       }
+
+       public function validKeyProvider() {
+               return [
+                       'empty' => [ '' ],
+                       'digits' => [ '09' ],
+                       'letters' => [ 'AZaz' ],
+                       'ASCII special characters' => [ '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidKeyProvider
+        * @covers MemcachedBagOStuff::validateKeyEncoding
+        */
+       public function testValidateKeyEncodingThrowsException( $key ) {
+               $this->setExpectedException( Exception::class );
+               $this->cache->validateKeyEncoding( $key );
+       }
+
+       public function invalidKeyProvider() {
+               return [
+                       [ "\x00" ],
+                       [ ' ' ],
+                       [ "\x1F" ],
+                       [ "\x7F" ],
+                       [ "\x80" ],
+                       [ "\xFF" ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/objectcache/RESTBagOStuffTest.php b/tests/phpunit/unit/includes/objectcache/RESTBagOStuffTest.php
new file mode 100644 (file)
index 0000000..459e3ee
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+/**
+ * @group BagOStuff
+ *
+ * @covers RESTBagOStuff
+ */
+class RESTBagOStuffTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var MultiHttpClient
+        */
+       private $client;
+       /**
+        * @var RESTBagOStuff
+        */
+       private $bag;
+
+       public function setUp() {
+               parent::setUp();
+               $this->client =
+                       $this->getMockBuilder( MultiHttpClient::class )
+                               ->setConstructorArgs( [ [] ] )
+                               ->setMethods( [ 'run' ] )
+                               ->getMock();
+               $this->bag = new RESTBagOStuff( [ 'client' => $this->client, 'url' => 'http://test/rest/' ] );
+       }
+
+       public function testGet() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 200, 'OK', [], '"somedata"', 0 ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertEquals( 'somedata', $result );
+       }
+
+       public function testGetNotExist() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 404, 'Not found', [], 'Nothing to see here', 0 ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertFalse( $result );
+       }
+
+       public function testGetBadClient() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 0, '', [], '', 'cURL has failed you today' ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertFalse( $result );
+               $this->assertEquals( BagOStuff::ERR_UNREACHABLE, $this->bag->getLastError() );
+       }
+
+       public function testGetBadServer() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 500, 'Too busy', [], 'Server is too busy', '' ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertFalse( $result );
+               $this->assertEquals( BagOStuff::ERR_UNEXPECTED, $this->bag->getLastError() );
+       }
+
+       public function testPut() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'PUT',
+                       'url' => 'http://test/rest/42xyz42',
+                       'body' => '"postdata"',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
+               $result = $this->bag->set( '42xyz42', 'postdata' );
+               $this->assertTrue( $result );
+       }
+
+       public function testDelete() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'DELETE',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
+               $result = $this->bag->delete( '42xyz42' );
+               $this->assertTrue( $result );
+       }
+}
diff --git a/tests/phpunit/unit/includes/parser/TidyTest.php b/tests/phpunit/unit/includes/parser/TidyTest.php
new file mode 100644 (file)
index 0000000..1adb6a6
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @group Parser
+ * @covers MWTidy
+ */
+class TidyTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+               if ( !MWTidy::isEnabled() ) {
+                       $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 static function provideTestWrapping() {
+               $testMathML = <<<'MathML'
+<math xmlns="http://www.w3.org/1998/Math/MathML">
+    <mrow>
+      <mi>a</mi>
+      <mo>&InvisibleTimes;</mo>
+      <msup>
+        <mi>x</mi>
+        <mn>2</mn>
+      </msup>
+      <mo>+</mo>
+      <mi>b</mi>
+      <mo>&InvisibleTimes; </mo>
+      <mi>x</mi>
+      <mo>+</mo>
+      <mi>c</mi>
+    </mrow>
+  </math>
+MathML;
+               return [
+                       [
+                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+                               '<mw:editsection> should survive tidy'
+                       ],
+                       [
+                               '<editsection page="foo" section="bar">foo</editsection>',
+                               '<editsection page="foo" section="bar">foo</editsection>',
+                               '<editsection> should survive tidy'
+                       ],
+                       [ '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ],
+                       [ "<link foo=\"bar\" />foo", '<link foo="bar"/>foo', '<link> should survive tidy' ],
+                       [ "<meta foo=\"bar\" />foo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ],
+                       [ $testMathML, $testMathML, '<math> should survive tidy' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/password/PasswordTest.php b/tests/phpunit/unit/includes/password/PasswordTest.php
new file mode 100644 (file)
index 0000000..b41c0f4
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Testing framework for the Password infrastructure
+ *
+ * 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 InvalidPassword
+ */
+class PasswordTest extends \MediaWikiUnitTestCase {
+       public function testInvalidPlaintext() {
+               $passwordFactory = new PasswordFactory();
+               $invalid = $passwordFactory->newFromPlaintext( null );
+
+               $this->assertInstanceOf( InvalidPassword::class, $invalid );
+       }
+}
diff --git a/tests/phpunit/unit/includes/preferences/FiltersTest.php b/tests/phpunit/unit/includes/preferences/FiltersTest.php
new file mode 100644 (file)
index 0000000..d2b5d05
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+/**
+ * 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
+ */
+
+use MediaWiki\Preferences\IntvalFilter;
+use MediaWiki\Preferences\MultiUsernameFilter;
+use MediaWiki\Preferences\TimezoneFilter;
+
+/**
+ * @group Preferences
+ */
+class FiltersTest extends \MediaWikiUnitTestCase {
+       /**
+        * @covers MediaWiki\Preferences\IntvalFilter::filterFromForm()
+        * @covers MediaWiki\Preferences\IntvalFilter::filterForForm()
+        */
+       public function testIntvalFilter() {
+               $filter = new IntvalFilter();
+               self::assertSame( 0, $filter->filterFromForm( '0' ) );
+               self::assertSame( 3, $filter->filterFromForm( '3' ) );
+               self::assertSame( '123', $filter->filterForForm( '123' ) );
+       }
+
+       /**
+        * @covers       MediaWiki\Preferences\TimezoneFilter::filterFromForm()
+        * @dataProvider provideTimezoneFilter
+        *
+        * @param string $input
+        * @param string $expected
+        */
+       public function testTimezoneFilter( $input, $expected ) {
+               $filter = new TimezoneFilter();
+               $result = $filter->filterFromForm( $input );
+               self::assertEquals( $expected, $result );
+       }
+
+       public function provideTimezoneFilter() {
+               return [
+                       [ 'ZoneInfo', 'Offset|0' ],
+                       [ 'ZoneInfo|bogus', 'Offset|0' ],
+                       [ 'System', 'System' ],
+                       [ '2:30', 'Offset|150' ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterFromForm()
+        * @dataProvider provideMultiUsernameFilterFrom
+        *
+        * @param string $input
+        * @param string|null $expected
+        */
+       public function testMultiUsernameFilterFrom( $input, $expected ) {
+               $filter = $this->makeMultiUsernameFilter();
+               $result = $filter->filterFromForm( $input );
+               self::assertSame( $expected, $result );
+       }
+
+       public function provideMultiUsernameFilterFrom() {
+               return [
+                       [ '', null ],
+                       [ "\n\n\n", null ],
+                       [ 'Foo', '1' ],
+                       [ "\n\n\nFoo\nBar\n", "1\n2" ],
+                       [ "Baz\nInvalid\nFoo", "3\n1" ],
+                       [ "Invalid", null ],
+                       [ "Invalid\n\n\nInvalid\n", null ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterForForm()
+        * @dataProvider provideMultiUsernameFilterFor
+        *
+        * @param string $input
+        * @param string $expected
+        */
+       public function testMultiUsernameFilterFor( $input, $expected ) {
+               $filter = $this->makeMultiUsernameFilter();
+               $result = $filter->filterForForm( $input );
+               self::assertSame( $expected, $result );
+       }
+
+       public function provideMultiUsernameFilterFor() {
+               return [
+                       [ '', '' ],
+                       [ "\n", '' ],
+                       [ '1', 'Foo' ],
+                       [ "\n1\n\n2\377\n", "Foo\nBar" ],
+                       [ "666\n667", '' ],
+               ];
+       }
+
+       private function makeMultiUsernameFilter() {
+               $userMapping = [
+                       'Foo' => 1,
+                       'Bar' => 2,
+                       'Baz' => 3,
+               ];
+               $flipped = array_flip( $userMapping );
+               $idLookup = self::getMockBuilder( CentralIdLookup::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'centralIdsFromNames', 'namesFromCentralIds' ] )
+                       ->getMockForAbstractClass();
+
+               $idLookup->method( 'centralIdsFromNames' )
+                       ->will( self::returnCallback( function ( $names ) use ( $userMapping ) {
+                               $ids = [];
+                               foreach ( $names as $name ) {
+                                       $ids[] = $userMapping[$name] ?? null;
+                               }
+                               return array_filter( $ids, 'is_numeric' );
+                       } ) );
+               $idLookup->method( 'namesFromCentralIds' )
+                       ->will( self::returnCallback( function ( $ids ) use ( $flipped ) {
+                               $names = [];
+                               foreach ( $ids as $id ) {
+                                       $names[] = $flipped[$id] ?? null;
+                               }
+                               return array_filter( $names, 'is_string' );
+                       } ) );
+
+               return new MultiUsernameFilter( $idLookup );
+       }
+}
diff --git a/tests/phpunit/unit/includes/registration/ExtensionProcessorTest.php b/tests/phpunit/unit/includes/registration/ExtensionProcessorTest.php
new file mode 100644 (file)
index 0000000..13de142
--- /dev/null
@@ -0,0 +1,829 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ExtensionProcessor
+ */
+class ExtensionProcessorTest extends \MediaWikiUnitTestCase {
+
+       private $dir, $dirname;
+
+       public function setUp() {
+               parent::setUp();
+               $this->dir = __DIR__ . '/FooBar/extension.json';
+               $this->dirname = dirname( $this->dir );
+       }
+
+       /**
+        * 'name' is absolutely required
+        *
+        * @var array
+        */
+       public static $default = [
+               'name' => 'FooBar',
+       ];
+
+       public function testExtractInfo() {
+               // Test that attributes that begin with @ are ignored
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, self::$default + [
+                       '@metadata' => [ 'foobarbaz' ],
+                       'AnAttribute' => [ 'omg' ],
+                       'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
+                       'SpecialPages' => [ 'Foo' => 'SpecialFoo' ],
+                       'callback' => 'FooBar::onRegistration',
+               ], 1 );
+
+               $extracted = $processor->getExtractedInfo();
+               $attributes = $extracted['attributes'];
+               $this->assertArrayHasKey( 'AnAttribute', $attributes );
+               $this->assertArrayNotHasKey( '@metadata', $attributes );
+               $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
+               $this->assertSame(
+                       [ 'FooBar' => 'FooBar::onRegistration' ],
+                       $extracted['callbacks']
+               );
+               $this->assertSame(
+                       [ 'Foo' => 'SpecialFoo' ],
+                       $extracted['globals']['wgSpecialPages']
+               );
+       }
+
+       public function testExtractNamespaces() {
+               // Test that namespace IDs can be overwritten
+               if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
+                       define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
+               }
+
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, self::$default + [
+                       'namespaces' => [
+                               [
+                                       'id' => 332200,
+                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
+                                       'name' => 'Test_A',
+                                       'defaultcontentmodel' => 'TestModel',
+                                       'gender' => [
+                                               'male' => 'Male test',
+                                               'female' => 'Female test',
+                                       ],
+                                       'subpages' => true,
+                                       'content' => true,
+                                       'protection' => 'userright',
+                               ],
+                               [ // Test_X will use ID 123456 not 334400
+                                       'id' => 334400,
+                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
+                                       'name' => 'Test_X',
+                                       'defaultcontentmodel' => 'TestModel'
+                               ],
+                       ]
+               ], 1 );
+
+               $extracted = $processor->getExtractedInfo();
+
+               $this->assertArrayHasKey(
+                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
+                       $extracted['defines']
+               );
+               $this->assertArrayNotHasKey(
+                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
+                       $extracted['defines']
+               );
+
+               $this->assertSame(
+                       $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
+                       332200
+               );
+
+               $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
+               $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
+               $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
+               $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
+
+               $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
+               $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
+               $this->assertSame(
+                       [ 'male' => 'Male test', 'female' => 'Female test' ],
+                       $extracted['globals']['wgExtraGenderNamespaces'][332200]
+               );
+               // A has subpages, X does not
+               $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] );
+               $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] );
+       }
+
+       public static function provideRegisterHooks() {
+               $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
+               // Format:
+               // Current $wgHooks
+               // Content in extension.json
+               // Expected value of $wgHooks
+               return [
+                       // No hooks
+                       [
+                               [],
+                               self::$default,
+                               $merge,
+                       ],
+                       // No current hooks, adding one for "FooBaz" in string format
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
+                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
+                       ],
+                       // Hook for "FooBaz", adding another one
+                       [
+                               [ 'FooBaz' => [ 'PriorCallback' ] ],
+                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
+                               [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
+                       ],
+                       // No current hooks, adding one for "FooBaz" in verbose array format
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
+                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
+                       ],
+                       // Hook for "BarBaz", adding one for "FooBaz"
+                       [
+                               [ 'BarBaz' => [ 'BarBazCallback' ] ],
+                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
+                               [
+                                       'BarBaz' => [ 'BarBazCallback' ],
+                                       'FooBaz' => [ 'FooBazCallback' ],
+                               ] + $merge,
+                       ],
+                       // Callbacks for FooBaz wrapped in an array
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
+                               [
+                                       'FooBaz' => [ 'Callback1' ],
+                               ] + $merge,
+                       ],
+                       // Multiple callbacks for FooBaz hook
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
+                               [
+                                       'FooBaz' => [ 'Callback1', 'Callback2' ],
+                               ] + $merge,
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideRegisterHooks
+        */
+       public function testRegisterHooks( $pre, $info, $expected ) {
+               $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
+               $processor->extractInfo( $this->dir, $info, 1 );
+               $extracted = $processor->getExtractedInfo();
+               $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
+       }
+
+       public function testExtractConfig1() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => 'somevalue',
+                               'Foo' => 10,
+                               '@IGNORED' => 'yes',
+                       ],
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               '_prefix' => 'eg',
+                               'Bar' => 'somevalue'
+                       ],
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 1 );
+               $processor->extractInfo( $this->dir, $info2, 1 );
+               $extracted = $processor->getExtractedInfo();
+               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
+               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
+               $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
+               // Custom prefix:
+               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
+       }
+
+       public function testExtractConfig2() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                               'Foo' => [ 'value' => 10 ],
+                               'Path' => [ 'value' => 'foo.txt', 'path' => true ],
+                               'Namespaces' => [
+                                       'value' => [
+                                               '10' => true,
+                                               '12' => false,
+                                       ],
+                                       'merge_strategy' => 'array_plus',
+                               ],
+                       ],
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                       ],
+                       'config_prefix' => 'eg',
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 2 );
+               $processor->extractInfo( $this->dir, $info2, 2 );
+               $extracted = $processor->getExtractedInfo();
+               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
+               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
+               $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
+               // Custom prefix:
+               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
+               $this->assertSame(
+                       [ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ],
+                       $extracted['globals']['wgNamespaces']
+               );
+       }
+
+       /**
+        * @expectedException RuntimeException
+        */
+       public function testDuplicateConfigKey1() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => '',
+                       ]
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               'Bar' => 'g',
+                       ],
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 1 );
+               $processor->extractInfo( $this->dir, $info2, 1 );
+       }
+
+       /**
+        * @expectedException RuntimeException
+        */
+       public function testDuplicateConfigKey2() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                       ]
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                       ],
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 2 );
+               $processor->extractInfo( $this->dir, $info2, 2 );
+       }
+
+       public static function provideExtractExtensionMessagesFiles() {
+               $dir = __DIR__ . '/FooBar/';
+               return [
+                       [
+                               [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
+                               [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
+                       ],
+                       [
+                               [
+                                       'ExtensionMessagesFiles' => [
+                                               'FooBarAlias' => 'FooBar.alias.php',
+                                               'FooBarMagic' => 'FooBar.magic.i18n.php',
+                                       ],
+                               ],
+                               [
+                                       'wgExtensionMessagesFiles' => [
+                                               'FooBarAlias' => $dir . 'FooBar.alias.php',
+                                               'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideExtractExtensionMessagesFiles
+        */
+       public function testExtractExtensionMessagesFiles( $input, $expected ) {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
+               $out = $processor->getExtractedInfo();
+               foreach ( $expected as $key => $value ) {
+                       $this->assertEquals( $value, $out['globals'][$key] );
+               }
+       }
+
+       public static function provideExtractMessagesDirs() {
+               $dir = __DIR__ . '/FooBar/';
+               return [
+                       [
+                               [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
+                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
+                       ],
+                       [
+                               [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
+                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideExtractMessagesDirs
+        */
+       public function testExtractMessagesDirs( $input, $expected ) {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
+               $out = $processor->getExtractedInfo();
+               foreach ( $expected as $key => $value ) {
+                       $this->assertEquals( $value, $out['globals'][$key] );
+               }
+       }
+
+       public function testExtractCredits() {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, self::$default, 1 );
+               $this->setExpectedException( Exception::class );
+               $processor->extractInfo( $this->dir, self::$default, 1 );
+       }
+
+       /**
+        * @dataProvider provideExtractResourceLoaderModules
+        */
+       public function testExtractResourceLoaderModules(
+               $input,
+               array $expectedGlobals,
+               array $expectedAttribs = []
+       ) {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
+               $out = $processor->getExtractedInfo();
+               foreach ( $expectedGlobals as $key => $value ) {
+                       $this->assertEquals( $value, $out['globals'][$key] );
+               }
+               foreach ( $expectedAttribs as $key => $value ) {
+                       $this->assertEquals( $value, $out['attributes'][$key] );
+               }
+       }
+
+       public static function provideExtractResourceLoaderModules() {
+               $dir = __DIR__ . '/FooBar';
+               return [
+                       // Generic module with localBasePath/remoteExtPath specified
+                       [
+                               // Input
+                               [
+                                       'ResourceModules' => [
+                                               'test.foo' => [
+                                                       'styles' => 'foobar.js',
+                                                       'localBasePath' => '',
+                                                       'remoteExtPath' => 'FooBar',
+                                               ],
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModules' => [
+                                               'test.foo' => [
+                                                       'styles' => 'foobar.js',
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'FooBar',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       // ResourceFileModulePaths specified:
+                       [
+                               // Input
+                               [
+                                       'ResourceFileModulePaths' => [
+                                               'localBasePath' => 'modules',
+                                               'remoteExtPath' => 'FooBar/modules',
+                                       ],
+                                       'ResourceModules' => [
+                                               // No paths
+                                               'test.foo' => [
+                                                       'styles' => 'foo.js',
+                                               ],
+                                               // Different paths set
+                                               'test.bar' => [
+                                                       'styles' => 'bar.js',
+                                                       'localBasePath' => 'subdir',
+                                                       'remoteExtPath' => 'FooBar/subdir',
+                                               ],
+                                               // Custom class with no paths set
+                                               'test.class' => [
+                                                       'class' => 'FooBarModule',
+                                                       'extra' => 'argument',
+                                               ],
+                                               // Custom class with a localBasePath
+                                               'test.class.with.path' => [
+                                                       'class' => 'FooBarPathModule',
+                                                       'extra' => 'argument',
+                                                       'localBasePath' => '',
+                                               ]
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModules' => [
+                                               'test.foo' => [
+                                                       'styles' => 'foo.js',
+                                                       'localBasePath' => "$dir/modules",
+                                                       'remoteExtPath' => 'FooBar/modules',
+                                               ],
+                                               'test.bar' => [
+                                                       'styles' => 'bar.js',
+                                                       'localBasePath' => "$dir/subdir",
+                                                       'remoteExtPath' => 'FooBar/subdir',
+                                               ],
+                                               'test.class' => [
+                                                       'class' => 'FooBarModule',
+                                                       'extra' => 'argument',
+                                                       'localBasePath' => "$dir/modules",
+                                                       'remoteExtPath' => 'FooBar/modules',
+                                               ],
+                                               'test.class.with.path' => [
+                                                       'class' => 'FooBarPathModule',
+                                                       'extra' => 'argument',
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'FooBar/modules',
+                                               ]
+                                       ],
+                               ],
+                       ],
+                       // ResourceModuleSkinStyles with file module paths
+                       [
+                               // Input
+                               [
+                                       'ResourceFileModulePaths' => [
+                                               'localBasePath' => '',
+                                               'remoteSkinPath' => 'FooBar',
+                                       ],
+                                       'ResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                               ]
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                                       'localBasePath' => $dir,
+                                                       'remoteSkinPath' => 'FooBar',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       // ResourceModuleSkinStyles with file module paths and an override
+                       [
+                               // Input
+                               [
+                                       'ResourceFileModulePaths' => [
+                                               'localBasePath' => '',
+                                               'remoteSkinPath' => 'FooBar',
+                                       ],
+                                       'ResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                                       'remoteSkinPath' => 'BarFoo'
+                                               ],
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                                       'localBasePath' => $dir,
+                                                       'remoteSkinPath' => 'BarFoo',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       'QUnit test module' => [
+                               // Input
+                               [
+                                       'QUnitTestModule' => [
+                                               'localBasePath' => '',
+                                               'remoteExtPath' => 'Foo',
+                                               'scripts' => 'bar.js',
+                                       ],
+                               ],
+                               // Expected
+                               [],
+                               [
+                                       'QUnitTestModules' => [
+                                               'test.FooBar' => [
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'Foo',
+                                                       'scripts' => 'bar.js',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       public static function provideSetToGlobal() {
+               return [
+                       [
+                               [ 'wgAPIModules', 'wgAvailableRights' ],
+                               [],
+                               [
+                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
+                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
+                               ],
+                               [
+                                       'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
+                                       'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
+                               ],
+                       ],
+                       [
+                               [ 'wgAPIModules', 'wgAvailableRights' ],
+                               [
+                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
+                                       'wgAvailableRights' => [ 'barbaz' ]
+                               ],
+                               [
+                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
+                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
+                               ],
+                               [
+                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
+                                       'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
+                               ],
+                       ],
+                       [
+                               [ 'wgGroupPermissions' ],
+                               [
+                                       'wgGroupPermissions' => [
+                                               'sysop' => [ 'delete' ]
+                                       ],
+                               ],
+                               [
+                                       'GroupPermissions' => [
+                                               'sysop' => [ 'undelete' ],
+                                               'user' => [ 'edit' ]
+                                       ],
+                               ],
+                               [
+                                       'wgGroupPermissions' => [
+                                               'sysop' => [ 'delete', 'undelete' ],
+                                               'user' => [ 'edit' ]
+                                       ],
+                               ]
+                       ]
+               ];
+       }
+
+       /**
+        * Attributes under manifest_version 2
+        */
+       public function testExtractAttributes() {
+               $processor = new ExtensionProcessor();
+               // Load FooBar extension
+               $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'name' => 'Baz',
+                               'attributes' => [
+                                       // Loaded
+                                       'FooBar' => [
+                                               'Plugins' => [
+                                                       'ext.baz.foobar',
+                                               ],
+                                       ],
+                                       // Not loaded
+                                       'FizzBuzz' => [
+                                               'MorePlugins' => [
+                                                       'ext.baz.fizzbuzz',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       2
+               );
+
+               $info = $processor->getExtractedInfo();
+               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
+               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
+               $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
+       }
+
+       /**
+        * Attributes under manifest_version 1
+        */
+       public function testAttributes1() {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'name' => 'FooBar',
+                               'FooBarPlugins' => [
+                                       'ext.baz.foobar',
+                               ],
+                               'FizzBuzzMorePlugins' => [
+                                       'ext.baz.fizzbuzz',
+                               ],
+                       ],
+                       1
+               );
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'name' => 'FooBar2',
+                               'FizzBuzzMorePlugins' => [
+                                       'ext.bar.fizzbuzz',
+                               ]
+                       ],
+                       1
+               );
+
+               $info = $processor->getExtractedInfo();
+               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
+               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
+               $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
+               $this->assertSame(
+                       [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ],
+                       $info['attributes']['FizzBuzzMorePlugins']
+               );
+       }
+
+       public function testAttributes1_notarray() {
+               $processor = new ExtensionProcessor();
+               $this->setExpectedException(
+                       InvalidArgumentException::class,
+                       "The value for 'FooBarPlugins' should be an array (from {$this->dir})"
+               );
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'FooBarPlugins' => 'ext.baz.foobar',
+                       ] + self::$default,
+                       1
+               );
+       }
+
+       public function testExtractPathBasedGlobal() {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'ParserTestFiles' => [
+                                       'tests/parserTests.txt',
+                                       'tests/extraParserTests.txt',
+                               ],
+                               'ServiceWiringFiles' => [
+                                       'includes/ServiceWiring.php'
+                               ],
+                       ] + self::$default,
+                       1
+               );
+               $globals = $processor->getExtractedInfo()['globals'];
+               $this->assertArrayHasKey( 'wgParserTestFiles', $globals );
+               $this->assertSame( [
+                       "{$this->dirname}/tests/parserTests.txt",
+                       "{$this->dirname}/tests/extraParserTests.txt"
+               ], $globals['wgParserTestFiles'] );
+               $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals );
+               $this->assertSame( [
+                       "{$this->dirname}/includes/ServiceWiring.php"
+               ], $globals['wgServiceWiringFiles'] );
+       }
+
+       public function testGetRequirements() {
+               $info = self::$default + [
+                       'requires' => [
+                               'MediaWiki' => '>= 1.25.0',
+                               'platform' => [
+                                       'php' => '>= 5.5.9'
+                               ],
+                               'extensions' => [
+                                       'Bar' => '*'
+                               ]
+                       ]
+               ];
+               $processor = new ExtensionProcessor();
+               $this->assertSame(
+                       $info['requires'],
+                       $processor->getRequirements( $info, false )
+               );
+               $this->assertSame(
+                       [],
+                       $processor->getRequirements( [], false )
+               );
+       }
+
+       public function testGetDevRequirements() {
+               $info = self::$default + [
+                       'dev-requires' => [
+                               'MediaWiki' => '>= 1.31.0',
+                               'platform' => [
+                                       'ext-foo' => '*',
+                               ],
+                               'skins' => [
+                                       'Baz' => '*',
+                               ],
+                               'extensions' => [
+                                       'Biz' => '*',
+                               ],
+                       ],
+               ];
+               $processor = new ExtensionProcessor();
+               $this->assertSame(
+                       $info['dev-requires'],
+                       $processor->getRequirements( $info, true )
+               );
+               // Set some standard requirements, so we can test merging
+               $info['requires'] = [
+                       'MediaWiki' => '>= 1.25.0',
+                       'platform' => [
+                               'php' => '>= 5.5.9'
+                       ],
+                       'extensions' => [
+                               'Bar' => '*'
+                       ]
+               ];
+               $this->assertSame(
+                       [
+                               'MediaWiki' => '>= 1.25.0 >= 1.31.0',
+                               'platform' => [
+                                       'php' => '>= 5.5.9',
+                                       'ext-foo' => '*',
+                               ],
+                               'extensions' => [
+                                       'Bar' => '*',
+                                       'Biz' => '*',
+                               ],
+                               'skins' => [
+                                       'Baz' => '*',
+                               ],
+                       ],
+                       $processor->getRequirements( $info, true )
+               );
+
+               // If there's no dev-requires, it just returns requires
+               unset( $info['dev-requires'] );
+               $this->assertSame(
+                       $info['requires'],
+                       $processor->getRequirements( $info, true )
+               );
+       }
+
+       public function testGetExtraAutoloaderPaths() {
+               $processor = new ExtensionProcessor();
+               $this->assertSame(
+                       [ "{$this->dirname}/vendor/autoload.php" ],
+                       $processor->getExtraAutoloaderPaths( $this->dirname, [
+                               'load_composer_autoloader' => true,
+                       ] )
+               );
+       }
+
+       /**
+        * Verify that extension.schema.json is in sync with ExtensionProcessor
+        *
+        * @coversNothing
+        */
+       public function testGlobalSettingsDocumentedInSchema() {
+               global $IP;
+               $globalSettings = TestingAccessWrapper::newFromClass(
+                       ExtensionProcessor::class )->globalSettings;
+
+               $version = ExtensionRegistry::MANIFEST_VERSION;
+               $schema = FormatJson::decode(
+                       file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
+                       true
+               );
+               $missing = [];
+               foreach ( $globalSettings as $global ) {
+                       if ( !isset( $schema['properties'][$global] ) ) {
+                               $missing[] = $global;
+                       }
+               }
+
+               $this->assertEquals( [], $missing,
+                       "The following global settings are not documented in docs/extension.schema.json" );
+       }
+}
+
+/**
+ * Allow overriding the default value of $this->globals
+ * so we can test merging
+ */
+class MockExtensionProcessor extends ExtensionProcessor {
+       public function __construct( $globals = [] ) {
+               $this->globals = $globals + $this->globals;
+       }
+}
diff --git a/tests/phpunit/unit/includes/search/SearchIndexFieldTest.php b/tests/phpunit/unit/includes/search/SearchIndexFieldTest.php
new file mode 100644 (file)
index 0000000..a640c96
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @group Search
+ * @covers SearchIndexFieldDefinition
+ */
+class SearchIndexFieldTest extends \MediaWikiUnitTestCase {
+
+       public function getMergeCases() {
+               return [
+                       [ 0, 'test', 0, 'test', true ],
+                       [ SearchIndexField::INDEX_TYPE_NESTED, 'test',
+                               SearchIndexField::INDEX_TYPE_NESTED, 'test', false ],
+                       [ 0, 'test', 0, 'test2', true ],
+                       [ 0, 'test', 1, 'test', false ],
+               ];
+       }
+
+       /**
+        * @dataProvider getMergeCases
+        * @param int $t1
+        * @param string $n1
+        * @param int $t2
+        * @param string $n2
+        * @param bool $result
+        */
+       public function testMerge( $t1, $n1, $t2, $n2, $result ) {
+               $field1 =
+                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
+                               ->setMethods( [ 'getMapping' ] )
+                               ->setConstructorArgs( [ $n1, $t1 ] )
+                               ->getMock();
+               $field2 =
+                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
+                               ->setMethods( [ 'getMapping' ] )
+                               ->setConstructorArgs( [ $n2, $t2 ] )
+                               ->getMock();
+
+               if ( $result ) {
+                       $this->assertNotFalse( $field1->merge( $field2 ) );
+               } else {
+                       $this->assertFalse( $field1->merge( $field2 ) );
+               }
+
+               $field1->setFlag( 0xFF );
+               $this->assertFalse( $field1->merge( $field2 ) );
+
+               $field1->setMergeCallback(
+                       function ( $a, $b ) {
+                               return "test";
+                       }
+               );
+               $this->assertEquals( "test", $field1->merge( $field2 ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/MetadataMergeExceptionTest.php b/tests/phpunit/unit/includes/session/MetadataMergeExceptionTest.php
new file mode 100644 (file)
index 0000000..707adfe
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\MetadataMergeException
+ */
+class MetadataMergeExceptionTest extends \MediaWikiUnitTestCase {
+
+       public function testBasics() {
+               $data = [ 'foo' => 'bar' ];
+
+               $ex = new MetadataMergeException();
+               $this->assertInstanceOf( \UnexpectedValueException::class, $ex );
+               $this->assertSame( [], $ex->getContext() );
+
+               $ex2 = new MetadataMergeException( 'Message', 42, $ex, $data );
+               $this->assertSame( 'Message', $ex2->getMessage() );
+               $this->assertSame( 42, $ex2->getCode() );
+               $this->assertSame( $ex, $ex2->getPrevious() );
+               $this->assertSame( $data, $ex2->getContext() );
+
+               $ex->setContext( $data );
+               $this->assertSame( $data, $ex->getContext() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/SessionIdTest.php b/tests/phpunit/unit/includes/session/SessionIdTest.php
new file mode 100644 (file)
index 0000000..3c7f8cb
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\SessionId
+ */
+class SessionIdTest extends \MediaWikiUnitTestCase {
+
+       public function testEverything() {
+               $id = new SessionId( 'foo' );
+               $this->assertSame( 'foo', $id->getId() );
+               $this->assertSame( 'foo', (string)$id );
+               $id->setId( 'bar' );
+               $this->assertSame( 'bar', $id->getId() );
+               $this->assertSame( 'bar', (string)$id );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/skins/SkinFactoryTest.php b/tests/phpunit/unit/includes/skins/SkinFactoryTest.php
new file mode 100644 (file)
index 0000000..8443c8d
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+class SkinFactoryTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers SkinFactory::register
+        */
+       public function testRegister() {
+               $factory = new SkinFactory();
+               $factory->register( 'fallback', 'Fallback', function () {
+                       return new SkinFallback();
+               } );
+               $this->assertTrue( true ); // No exception thrown
+               $this->setExpectedException( InvalidArgumentException::class );
+               $factory->register( 'invalid', 'Invalid', 'Invalid callback' );
+       }
+
+       /**
+        * @covers SkinFactory::makeSkin
+        */
+       public function testMakeSkinWithNoBuilders() {
+               $factory = new SkinFactory();
+               $this->setExpectedException( SkinException::class );
+               $factory->makeSkin( 'nobuilderregistered' );
+       }
+
+       /**
+        * @covers SkinFactory::makeSkin
+        */
+       public function testMakeSkinWithInvalidCallback() {
+               $factory = new SkinFactory();
+               $factory->register( 'unittest', 'Unittest', function () {
+                       return true; // Not a Skin object
+               } );
+               $this->setExpectedException( UnexpectedValueException::class );
+               $factory->makeSkin( 'unittest' );
+       }
+
+       /**
+        * @covers SkinFactory::makeSkin
+        */
+       public function testMakeSkinWithValidCallback() {
+               $factory = new SkinFactory();
+               $factory->register( 'testfallback', 'TestFallback', function () {
+                       return new SkinFallback();
+               } );
+
+               $skin = $factory->makeSkin( 'testfallback' );
+               $this->assertInstanceOf( Skin::class, $skin );
+               $this->assertInstanceOf( SkinFallback::class, $skin );
+               $this->assertEquals( 'fallback', $skin->getSkinName() );
+       }
+
+       /**
+        * @covers Skin::__construct
+        * @covers Skin::getSkinName
+        */
+       public function testGetSkinName() {
+               $skin = new SkinFallback();
+               $this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
+               $skin = new SkinFallback( 'testname' );
+               $this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
+       }
+
+       /**
+        * @covers SkinFactory::getSkinNames
+        */
+       public function testGetSkinNames() {
+               $factory = new SkinFactory();
+               // A fake callback we can use that will never be called
+               $callback = function () {
+                       // NOP
+               };
+               $factory->register( 'skin1', 'Skin1', $callback );
+               $factory->register( 'skin2', 'Skin2', $callback );
+               $names = $factory->getSkinNames();
+               $this->assertArrayHasKey( 'skin1', $names );
+               $this->assertArrayHasKey( 'skin2', $names );
+               $this->assertEquals( 'Skin1', $names['skin1'] );
+               $this->assertEquals( 'Skin2', $names['skin2'] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/title/ForeignTitleTest.php b/tests/phpunit/unit/includes/title/ForeignTitleTest.php
new file mode 100644 (file)
index 0000000..ec093cf
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+/**
+ * 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 This, that and the other
+ */
+
+/**
+ * @covers ForeignTitle
+ *
+ * @group Title
+ */
+class ForeignTitleTest extends \MediaWikiUnitTestCase {
+
+       public function basicProvider() {
+               return [
+                       [
+                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
+                               20, 'Contributor', 'JohnDoe'
+                       ],
+                       [
+                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
+                               1, 'Discussion', 'Capital'
+                       ],
+                       [
+                               new ForeignTitle( 0, '', 'MainNamespace' ),
+                               0, '', 'MainNamespace'
+                       ],
+                       [
+                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
+                               4, 'Some_ns', 'Article_title_with_spaces'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider basicProvider
+        */
+       public function testBasic( ForeignTitle $title, $expectedId, $expectedName,
+               $expectedText
+       ) {
+               $this->assertEquals( true, $title->isNamespaceIdKnown() );
+               $this->assertEquals( $expectedId, $title->getNamespaceId() );
+               $this->assertEquals( $expectedName, $title->getNamespaceName() );
+               $this->assertEquals( $expectedText, $title->getText() );
+       }
+
+       public function testUnknownNamespaceCheck() {
+               $title = new ForeignTitle( null, 'this', 'that' );
+
+               $this->assertEquals( false, $title->isNamespaceIdKnown() );
+               $this->assertEquals( 'this', $title->getNamespaceName() );
+               $this->assertEquals( 'that', $title->getText() );
+       }
+
+       public function testUnknownNamespaceError() {
+               $this->setExpectedException( MWException::class );
+               $title = new ForeignTitle( null, 'this', 'that' );
+               $title->getNamespaceId();
+       }
+
+       public function fullTextProvider() {
+               return [
+                       [
+                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
+                               'Contributor:JohnDoe'
+                       ],
+                       [
+                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
+                               'Discussion:Capital'
+                       ],
+                       [
+                               new ForeignTitle( 0, '', 'MainNamespace' ),
+                               'MainNamespace'
+                       ],
+                       [
+                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
+                               'Some_ns:Article_title_with_spaces'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider fullTextProvider
+        */
+       public function testFullText( ForeignTitle $title, $fullText ) {
+               $this->assertEquals( $fullText, $title->getFullText() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/title/NamespaceAwareForeignTitleFactoryTest.php b/tests/phpunit/unit/includes/title/NamespaceAwareForeignTitleFactoryTest.php
new file mode 100644 (file)
index 0000000..d777973
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * 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 This, that and the other
+ */
+
+/**
+ * @covers NamespaceAwareForeignTitleFactory
+ *
+ * @group Title
+ */
+class NamespaceAwareForeignTitleFactoryTest extends \MediaWikiUnitTestCase {
+
+       public function basicProvider() {
+               return [
+                       [
+                               'MainNamespaceArticle', 0,
+                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
+                       ],
+                       [
+                               'MainNamespaceArticle', null,
+                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
+                       ],
+                       [
+                               'Magic:_The_Gathering', 0,
+                               new ForeignTitle( 0, '', 'Magic:_The_Gathering' ),
+                       ],
+                       [
+                               'Talk:Nice_talk', 1,
+                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
+                       ],
+                       [
+                               'Talk:Magic:_The_Gathering', 1,
+                               new ForeignTitle( 1, 'Talk', 'Magic:_The_Gathering' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 0,
+                               new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', null,
+                               new ForeignTitle( 9000, 'Bogus', 'Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 4,
+                               new ForeignTitle( 4, 'Bogus', 'Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 1,
+                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
+                       ],
+                       // Misconfigured wiki with unregistered namespace (T114115)
+                       [
+                               'Nice_talk', 1234,
+                               new ForeignTitle( 1234, 'Ns1234', 'Nice_talk' ),
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider basicProvider
+        */
+       public function testBasic( $title, $ns, ForeignTitle $foreignTitle ) {
+               $foreignNamespaces = [
+                       0 => '', 1 => 'Talk', 100 => 'Portal', 9000 => 'Bogus'
+               ];
+
+               $factory = new NamespaceAwareForeignTitleFactory( $foreignNamespaces );
+               $testTitle = $factory->createForeignTitle( $title, $ns );
+
+               $this->assertEquals( $testTitle->isNamespaceIdKnown(),
+                       $foreignTitle->isNamespaceIdKnown() );
+
+               if (
+                       $testTitle->isNamespaceIdKnown() &&
+                       $foreignTitle->isNamespaceIdKnown()
+               ) {
+                       $this->assertEquals( $testTitle->getNamespaceId(),
+                               $foreignTitle->getNamespaceId() );
+               }
+
+               $this->assertEquals( $testTitle->getNamespaceName(),
+                       $foreignTitle->getNamespaceName() );
+               $this->assertEquals( $testTitle->getText(), $foreignTitle->getText() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/title/TitleValueTest.php b/tests/phpunit/unit/includes/title/TitleValueTest.php
new file mode 100644 (file)
index 0000000..cd67a93
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+/**
+ * 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 Daniel Kinzler
+ */
+
+/**
+ * @covers TitleValue
+ *
+ * @group Title
+ */
+class TitleValueTest extends \MediaWikiUnitTestCase {
+
+       public function goodConstructorProvider() {
+               return [
+                       [ NS_MAIN, '', 'fragment', '', true, false ],
+                       [ NS_USER, 'TestThis', 'stuff', '', true, false ],
+                       [ NS_USER, 'TestThis', '', 'baz', false, true ],
+               ];
+       }
+
+       /**
+        * @dataProvider goodConstructorProvider
+        */
+       public function testConstruction( $ns, $text, $fragment, $interwiki, $hasFragment,
+               $hasInterwiki
+       ) {
+               $title = new TitleValue( $ns, $text, $fragment, $interwiki );
+
+               $this->assertEquals( $ns, $title->getNamespace() );
+               $this->assertTrue( $title->inNamespace( $ns ) );
+               $this->assertEquals( $text, $title->getText() );
+               $this->assertEquals( $fragment, $title->getFragment() );
+               $this->assertEquals( $hasFragment, $title->hasFragment() );
+               $this->assertEquals( $interwiki, $title->getInterwiki() );
+               $this->assertEquals( $hasInterwiki, $title->isExternal() );
+       }
+
+       public function badConstructorProvider() {
+               return [
+                       [ 'foo', 'title', 'fragment', '' ],
+                       [ null, 'title', 'fragment', '' ],
+                       [ 2.3, 'title', 'fragment', '' ],
+
+                       [ NS_MAIN, 5, 'fragment', '' ],
+                       [ NS_MAIN, null, 'fragment', '' ],
+                       [ NS_USER, '', 'fragment', '' ],
+                       [ NS_MAIN, 'foo bar', '', '' ],
+                       [ NS_MAIN, 'bar_', '', '' ],
+                       [ NS_MAIN, '_foo', '', '' ],
+                       [ NS_MAIN, ' eek ', '', '' ],
+
+                       [ NS_MAIN, 'title', 5, '' ],
+                       [ NS_MAIN, 'title', null, '' ],
+                       [ NS_MAIN, 'title', [], '' ],
+
+                       [ NS_MAIN, 'title', '', 5 ],
+                       [ NS_MAIN, 'title', null, 5 ],
+                       [ NS_MAIN, 'title', [], 5 ],
+               ];
+       }
+
+       /**
+        * @dataProvider badConstructorProvider
+        */
+       public function testConstructionErrors( $ns, $text, $fragment, $interwiki ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new TitleValue( $ns, $text, $fragment, $interwiki );
+       }
+
+       public function fragmentTitleProvider() {
+               return [
+                       [ new TitleValue( NS_MAIN, 'Test' ), 'foo' ],
+                       [ new TitleValue( NS_TALK, 'Test', 'foo' ), '' ],
+                       [ new TitleValue( NS_CATEGORY, 'Test', 'foo' ), 'bar' ],
+               ];
+       }
+
+       /**
+        * @dataProvider fragmentTitleProvider
+        */
+       public function testCreateFragmentTitle( TitleValue $title, $fragment ) {
+               $fragmentTitle = $title->createFragmentTarget( $fragment );
+
+               $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() );
+               $this->assertEquals( $title->getText(), $fragmentTitle->getText() );
+               $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
+       }
+
+       public function getTextProvider() {
+               return [
+                       [ 'Foo', 'Foo' ],
+                       [ 'Foo_Bar', 'Foo Bar' ],
+               ];
+       }
+
+       /**
+        * @dataProvider getTextProvider
+        */
+       public function testGetText( $dbkey, $text ) {
+               $title = new TitleValue( NS_MAIN, $dbkey );
+
+               $this->assertEquals( $text, $title->getText() );
+       }
+
+       public function provideTestToString() {
+               yield [
+                       new TitleValue( 0, 'Foo' ),
+                       '0:Foo'
+               ];
+               yield [
+                       new TitleValue( 1, 'Bar_Baz' ),
+                       '1:Bar_Baz'
+               ];
+               yield [
+                       new TitleValue( 9, 'JoJo', 'Frag' ),
+                       '9:JoJo#Frag'
+               ];
+               yield [
+                       new TitleValue( 200, 'tea', 'Fragment', 'wikicode' ),
+                       'wikicode:200:tea#Fragment'
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestToString
+        */
+       public function testToString( TitleValue $value, $expected ) {
+               $this->assertSame(
+                       $expected,
+                       $value->__toString()
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/user/UserArrayFromResultTest.php b/tests/phpunit/unit/includes/user/UserArrayFromResultTest.php
new file mode 100644 (file)
index 0000000..0b2ce17
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @author Addshore
+ * @covers UserArrayFromResult
+ */
+class UserArrayFromResultTest extends \MediaWikiUnitTestCase {
+
+       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
+                       ->disableOriginalConstructor();
+
+               $resultWrapper = $resultWrapper->getMock();
+               $resultWrapper->expects( $this->atLeastOnce() )
+                       ->method( 'current' )
+                       ->will( $this->returnValue( $row ) );
+               $resultWrapper->expects( $this->any() )
+                       ->method( 'numRows' )
+                       ->will( $this->returnValue( $numRows ) );
+
+               return $resultWrapper;
+       }
+
+       private function getRowWithUsername( $username = 'fooUser' ) {
+               $row = new stdClass();
+               $row->user_name = $username;
+               return $row;
+       }
+
+       /**
+        * @covers UserArrayFromResult::__construct
+        */
+       public function testConstructionWithFalseRow() {
+               $row = false;
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new UserArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertEquals( $row, $object->current );
+       }
+
+       /**
+        * @covers UserArrayFromResult::__construct
+        */
+       public function testConstructionWithRow() {
+               $username = 'addshore';
+               $row = $this->getRowWithUsername( $username );
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new UserArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertInstanceOf( User::class, $object->current );
+               $this->assertEquals( $username, $object->current->mName );
+       }
+
+       public static function provideNumberOfRows() {
+               return [
+                       [ 0 ],
+                       [ 1 ],
+                       [ 122 ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNumberOfRows
+        * @covers UserArrayFromResult::count
+        */
+       public function testCountWithVaryingValues( $numRows ) {
+               $object = new UserArrayFromResult( $this->getMockResultWrapper(
+                       $this->getRowWithUsername(),
+                       $numRows
+               ) );
+               $this->assertEquals( $numRows, $object->count() );
+       }
+
+       /**
+        * @covers UserArrayFromResult::current
+        */
+       public function testCurrentAfterConstruction() {
+               $username = 'addshore';
+               $userRow = $this->getRowWithUsername( $username );
+               $object = new UserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
+               $this->assertInstanceOf( User::class, $object->current() );
+               $this->assertEquals( $username, $object->current()->mName );
+       }
+
+       public function provideTestValid() {
+               return [
+                       [ $this->getRowWithUsername(), true ],
+                       [ false, false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestValid
+        * @covers UserArrayFromResult::valid
+        */
+       public function testValid( $input, $expected ) {
+               $object = new UserArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $this->assertEquals( $expected, $object->valid() );
+       }
+
+       // @todo unit test for key()
+       // @todo unit test for next()
+       // @todo unit test for rewind()
+}
diff --git a/tests/phpunit/unit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/tests/phpunit/unit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php
new file mode 100644 (file)
index 0000000..556f518
--- /dev/null
@@ -0,0 +1,250 @@
+<?php
+
+use MediaWiki\User\UserIdentityValue;
+
+/**
+ * @author Addshore
+ *
+ * @covers NoWriteWatchedItemStore
+ */
+class NoWriteWatchedItemStoreUnitTest extends \MediaWikiUnitTestCase {
+
+       public function testAddWatch() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'addWatch' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->addWatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
+       }
+
+       public function testAddWatchBatchForUser() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'addWatchBatchForUser' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] );
+       }
+
+       public function testRemoveWatch() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'removeWatch' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->removeWatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
+       }
+
+       public function testSetNotificationTimestampsForUser() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'setNotificationTimestampsForUser' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->setNotificationTimestampsForUser(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       'timestamp',
+                       []
+               );
+       }
+
+       public function testUpdateNotificationTimestamp() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'updateNotificationTimestamp' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->updateNotificationTimestamp(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' ),
+                       'timestamp'
+               );
+       }
+
+       public function testResetNotificationTimestamp() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'resetNotificationTimestamp' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->resetNotificationTimestamp(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+       }
+
+       public function testCountWatchedItems() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'countWatchedItems' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countWatchedItems(
+                       new UserIdentityValue( 1, 'MockUser', 0 )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountWatchers() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'countWatchers' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countWatchers(
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountVisitingWatchers() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countVisitingWatchers' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countVisitingWatchers(
+                       new TitleValue( 0, 'Foo' ),
+                       9
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountWatchersMultiple() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countVisitingWatchersMultiple' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countWatchersMultiple(
+                       [ new TitleValue( 0, 'Foo' ) ],
+                       []
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountVisitingWatchersMultiple() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countVisitingWatchersMultiple' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countVisitingWatchersMultiple(
+                       [ [ new TitleValue( 0, 'Foo' ), 99 ] ],
+                       11
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testGetWatchedItem() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'getWatchedItem' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->getWatchedItem(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testLoadWatchedItem() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'loadWatchedItem' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->loadWatchedItem(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testGetWatchedItemsForUser() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'getWatchedItemsForUser' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->getWatchedItemsForUser(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       []
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testIsWatched() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'isWatched' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->isWatched(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testGetNotificationTimestampsBatch() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'getNotificationTimestampsBatch' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->getNotificationTimestampsBatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       [ new TitleValue( 0, 'Foo' ) ]
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountUnreadNotifications() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countUnreadNotifications' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countUnreadNotifications(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       88
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testDuplicateAllAssociatedEntries() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->duplicateAllAssociatedEntries(
+                       new TitleValue( 0, 'Foo' ),
+                       new TitleValue( 0, 'Bar' )
+               );
+       }
+
+}