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