1<?php 2 3use MediaWiki\User\UserFactory; 4use Psr\Container\ContainerInterface; 5use Wikimedia\ObjectFactory; 6 7/** 8 * @covers ApiModuleManager 9 * @group API 10 * @group medium 11 */ 12class ApiModuleManagerTest extends MediaWikiUnitTestCase { 13 14 private function getModuleManager() { 15 // getContext is called in ApiBase::__construct 16 $apiMain = $this->createMock( ApiMain::class ); 17 $apiMain->method( 'getContext' ) 18 ->willReturn( $this->createMock( RequestContext::class ) ); 19 20 $containerInterface = $this->createMock( ContainerInterface::class ); 21 // Only needs to be able to provide the services used in the tests below, we 22 // don't need a full copy of MediaWikiServices's services. The only service 23 // actually used is a UserFactory, for demonstration purposes 24 $containerInterface->method( 'get' ) 25 ->with( 'UserFactory' ) 26 ->willReturn( $this->createMock( UserFactory::class ) ); 27 return new ApiModuleManager( 28 $apiMain, 29 new ObjectFactory( $containerInterface ) 30 ); 31 } 32 33 public function newApiLogin( $main, $action ) { 34 return new ApiLogin( $main, $action ); 35 } 36 37 public function addModuleProvider() { 38 return [ 39 'plain class' => [ 40 'login', 41 'action', 42 ApiLogin::class, 43 null, 44 ], 45 46 'with class and factory' => [ 47 'login', 48 'action', 49 ApiLogin::class, 50 [ $this, 'newApiLogin' ], 51 ], 52 53 'with spec (class only)' => [ 54 'login', 55 'action', 56 [ 57 'class' => ApiLogin::class 58 ], 59 null, 60 ], 61 62 'with spec' => [ 63 'login', 64 'action', 65 [ 66 'class' => ApiLogin::class, 67 'factory' => [ $this, 'newApiLogin' ], 68 ], 69 null, 70 ], 71 72 'with spec (using services)' => [ 73 'logout', 74 'action', 75 [ 76 'class' => ApiLogout::class, 77 'factory' => static function ( ApiMain $main, $action, UserFactory $userFactory ) { 78 // we don't actually need the UserFactory, just demonstrating 79 return new ApiLogout( $main, $action ); 80 }, 81 'services' => [ 82 'UserFactory' 83 ], 84 ], 85 null, 86 ] 87 ]; 88 } 89 90 /** 91 * @dataProvider addModuleProvider 92 */ 93 public function testAddModule( $name, $group, $spec, $factory ) { 94 if ( $factory ) { 95 $this->hideDeprecated( 96 ApiModuleManager::class . '::addModule with $class and $factory' 97 ); 98 } 99 100 $moduleManager = $this->getModuleManager(); 101 $moduleManager->addModule( $name, $group, $spec, $factory ); 102 103 $this->assertTrue( $moduleManager->isDefined( $name, $group ), 'isDefined' ); 104 $this->assertNotNull( $moduleManager->getModule( $name, $group, true ), 'getModule' ); 105 } 106 107 public function addModulesProvider() { 108 return [ 109 'empty' => [ 110 [], 111 'action', 112 ], 113 114 'simple' => [ 115 [ 116 'login' => ApiLogin::class, 117 'logout' => ApiLogout::class, 118 ], 119 'action', 120 ], 121 122 'with factories' => [ 123 [ 124 'login' => [ 125 'class' => ApiLogin::class, 126 'factory' => [ $this, 'newApiLogin' ], 127 ], 128 'logout' => [ 129 'class' => ApiLogout::class, 130 'factory' => static function ( ApiMain $main, $action ) { 131 return new ApiLogout( $main, $action ); 132 }, 133 ], 134 ], 135 'action', 136 ], 137 ]; 138 } 139 140 /** 141 * @dataProvider addModulesProvider 142 */ 143 public function testAddModules( array $modules, $group ) { 144 $moduleManager = $this->getModuleManager(); 145 $moduleManager->addModules( $modules, $group ); 146 147 foreach ( array_keys( $modules ) as $name ) { 148 $this->assertTrue( $moduleManager->isDefined( $name, $group ), 'isDefined' ); 149 $this->assertNotNull( $moduleManager->getModule( $name, $group, true ), 'getModule' ); 150 } 151 152 $this->assertTrue( true ); // Don't mark the test as risky if $modules is empty 153 } 154 155 public function getModuleProvider() { 156 $modules = [ 157 'disabled' => ApiDisabled::class, 158 'disabled2' => [ 'class' => ApiDisabled::class ], 159 'login' => [ 160 'class' => ApiLogin::class, 161 'factory' => [ $this, 'newApiLogin' ], 162 ], 163 'logout' => [ 164 'class' => ApiLogout::class, 165 'factory' => static function ( ApiMain $main, $action ) { 166 return new ApiLogout( $main, $action ); 167 }, 168 ], 169 ]; 170 171 return [ 172 'legacy entry' => [ 173 $modules, 174 'disabled', 175 ApiDisabled::class, 176 ], 177 178 'just a class' => [ 179 $modules, 180 'disabled2', 181 ApiDisabled::class, 182 ], 183 184 'with factory' => [ 185 $modules, 186 'login', 187 ApiLogin::class, 188 ], 189 190 'with closure' => [ 191 $modules, 192 'logout', 193 ApiLogout::class, 194 ], 195 ]; 196 } 197 198 /** 199 * @covers ApiModuleManager::getModule 200 * @dataProvider getModuleProvider 201 */ 202 public function testGetModule( $modules, $name, $expectedClass ) { 203 $moduleManager = $this->getModuleManager(); 204 $moduleManager->addModules( $modules, 'test' ); 205 206 // should return the right module 207 $module1 = $moduleManager->getModule( $name, null, false ); 208 $this->assertInstanceOf( $expectedClass, $module1 ); 209 210 // should pass group check (with caching disabled) 211 $module2 = $moduleManager->getModule( $name, 'test', true ); 212 $this->assertNotNull( $module2 ); 213 214 // should use cached instance 215 $module3 = $moduleManager->getModule( $name, null, false ); 216 $this->assertSame( $module1, $module3 ); 217 218 // should not use cached instance if caching is disabled 219 $module4 = $moduleManager->getModule( $name, null, true ); 220 $this->assertNotSame( $module1, $module4 ); 221 } 222 223 /** 224 * @covers ApiModuleManager::getModule 225 */ 226 public function testGetModule_null() { 227 $modules = [ 228 'login' => ApiLogin::class, 229 'logout' => ApiLogout::class, 230 ]; 231 232 $moduleManager = $this->getModuleManager(); 233 $moduleManager->addModules( $modules, 'test' ); 234 235 $this->assertNull( $moduleManager->getModule( 'quux' ), 'unknown name' ); 236 $this->assertNull( $moduleManager->getModule( 'login', 'bla' ), 'wrong group' ); 237 } 238 239 /** 240 * @covers ApiModuleManager::getNames 241 */ 242 public function testGetNames() { 243 $fooModules = [ 244 'login' => ApiLogin::class, 245 'logout' => ApiLogout::class, 246 ]; 247 248 $barModules = [ 249 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], 250 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], 251 ]; 252 253 $moduleManager = $this->getModuleManager(); 254 $moduleManager->addModules( $fooModules, 'foo' ); 255 $moduleManager->addModules( $barModules, 'bar' ); 256 257 $fooNames = $moduleManager->getNames( 'foo' ); 258 $this->assertArrayEquals( array_keys( $fooModules ), $fooNames ); 259 260 $allNames = $moduleManager->getNames(); 261 $allModules = array_merge( $fooModules, $barModules ); 262 $this->assertArrayEquals( array_keys( $allModules ), $allNames ); 263 } 264 265 /** 266 * @covers ApiModuleManager::getNamesWithClasses 267 */ 268 public function testGetNamesWithClasses() { 269 $fooModules = [ 270 'login' => ApiLogin::class, 271 'logout' => ApiLogout::class, 272 ]; 273 274 $barModules = [ 275 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], 276 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], 277 ]; 278 279 $moduleManager = $this->getModuleManager(); 280 $moduleManager->addModules( $fooModules, 'foo' ); 281 $moduleManager->addModules( $barModules, 'bar' ); 282 283 $fooNamesWithClasses = $moduleManager->getNamesWithClasses( 'foo' ); 284 $this->assertArrayEquals( $fooModules, $fooNamesWithClasses ); 285 286 $allNamesWithClasses = $moduleManager->getNamesWithClasses(); 287 $allModules = array_merge( $fooModules, [ 288 'feedcontributions' => ApiFeedContributions::class, 289 'feedrecentchanges' => ApiFeedRecentChanges::class, 290 ] ); 291 $this->assertArrayEquals( $allModules, $allNamesWithClasses ); 292 } 293 294 /** 295 * @covers ApiModuleManager::getModuleGroup 296 */ 297 public function testGetModuleGroup() { 298 $fooModules = [ 299 'login' => ApiLogin::class, 300 'logout' => ApiLogout::class, 301 ]; 302 303 $barModules = [ 304 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], 305 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], 306 ]; 307 308 $moduleManager = $this->getModuleManager(); 309 $moduleManager->addModules( $fooModules, 'foo' ); 310 $moduleManager->addModules( $barModules, 'bar' ); 311 312 $this->assertEquals( 'foo', $moduleManager->getModuleGroup( 'login' ) ); 313 $this->assertEquals( 'bar', $moduleManager->getModuleGroup( 'feedrecentchanges' ) ); 314 $this->assertNull( $moduleManager->getModuleGroup( 'quux' ) ); 315 } 316 317 /** 318 * @covers ApiModuleManager::getGroups 319 */ 320 public function testGetGroups() { 321 $fooModules = [ 322 'login' => ApiLogin::class, 323 'logout' => ApiLogout::class, 324 ]; 325 326 $barModules = [ 327 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], 328 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], 329 ]; 330 331 $moduleManager = $this->getModuleManager(); 332 $moduleManager->addModules( $fooModules, 'foo' ); 333 $moduleManager->addModules( $barModules, 'bar' ); 334 335 $groups = $moduleManager->getGroups(); 336 $this->assertArrayEquals( [ 'foo', 'bar' ], $groups ); 337 } 338 339 /** 340 * @covers ApiModuleManager::getClassName 341 */ 342 public function testGetClassName() { 343 $fooModules = [ 344 'login' => ApiLogin::class, 345 'logout' => ApiLogout::class, 346 ]; 347 348 $barModules = [ 349 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], 350 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], 351 ]; 352 353 $moduleManager = $this->getModuleManager(); 354 $moduleManager->addModules( $fooModules, 'foo' ); 355 $moduleManager->addModules( $barModules, 'bar' ); 356 357 $this->assertEquals( 358 ApiLogin::class, 359 $moduleManager->getClassName( 'login' ) 360 ); 361 $this->assertEquals( 362 ApiLogout::class, 363 $moduleManager->getClassName( 'logout' ) 364 ); 365 $this->assertEquals( 366 ApiFeedContributions::class, 367 $moduleManager->getClassName( 'feedcontributions' ) 368 ); 369 $this->assertEquals( 370 ApiFeedRecentChanges::class, 371 $moduleManager->getClassName( 'feedrecentchanges' ) 372 ); 373 $this->assertFalse( 374 $moduleManager->getClassName( 'nonexistentmodule' ) 375 ); 376 } 377 378 public function testAddModuleWithIncompleteSpec() { 379 $moduleManager = $this->getModuleManager(); 380 381 $this->expectException( \InvalidArgumentException::class ); 382 $this->expectExceptionMessage( '$spec must define a class name' ); 383 $moduleManager->addModule( 384 'logout', 385 'action', 386 [ 387 'factory' => static function ( ApiMain $main, $action ) { 388 return new ApiLogout( $main, $action ); 389 }, 390 ] 391 ); 392 } 393} 394