Remove hard deprecation of PasswordPolicyChecks::checkPopularPasswordBlacklist
[lhc/web/wiklou.git] / tests / phpunit / includes / MediaWikiServicesTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use Wikimedia\Services\DestructibleService;
5 use Wikimedia\Services\SalvageableService;
6 use Wikimedia\Services\ServiceDisabledException;
7
8 /**
9 * @covers MediaWiki\MediaWikiServices
10 *
11 * @group MediaWiki
12 */
13 class MediaWikiServicesTest extends MediaWikiTestCase {
14 private $deprecatedServices = [];
15
16 /**
17 * @return Config
18 */
19 private function newTestConfig() {
20 $globalConfig = new GlobalVarConfig();
21
22 $testConfig = new HashConfig();
23 $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
24 $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
25
26 return $testConfig;
27 }
28
29 /**
30 * @return MediaWikiServices
31 */
32 private function newMediaWikiServices( Config $config = null ) {
33 if ( $config === null ) {
34 $config = $this->newTestConfig();
35 }
36
37 $instance = new MediaWikiServices( $config );
38
39 // Load the default wiring from the specified files.
40 $wiringFiles = $config->get( 'ServiceWiringFiles' );
41 $instance->loadWiringFiles( $wiringFiles );
42
43 return $instance;
44 }
45
46 public function testGetInstance() {
47 $services = MediaWikiServices::getInstance();
48 $this->assertInstanceOf( MediaWikiServices::class, $services );
49 }
50
51 public function testForceGlobalInstance() {
52 $newServices = $this->newMediaWikiServices();
53 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
54
55 $this->assertInstanceOf( MediaWikiServices::class, $oldServices );
56 $this->assertNotSame( $oldServices, $newServices );
57
58 $theServices = MediaWikiServices::getInstance();
59 $this->assertSame( $theServices, $newServices );
60
61 MediaWikiServices::forceGlobalInstance( $oldServices );
62
63 $theServices = MediaWikiServices::getInstance();
64 $this->assertSame( $theServices, $oldServices );
65 }
66
67 public function testResetGlobalInstance() {
68 $newServices = $this->newMediaWikiServices();
69 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
70
71 $service1 = $this->createMock( SalvageableService::class );
72 $service1->expects( $this->never() )
73 ->method( 'salvage' );
74
75 $newServices->defineService(
76 'Test',
77 function () use ( $service1 ) {
78 return $service1;
79 }
80 );
81
82 // force instantiation
83 $newServices->getService( 'Test' );
84
85 MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
86 $theServices = MediaWikiServices::getInstance();
87
88 $this->assertSame(
89 $service1,
90 $theServices->getService( 'Test' ),
91 'service definition should survive reset'
92 );
93
94 $this->assertNotSame( $theServices, $newServices );
95 $this->assertNotSame( $theServices, $oldServices );
96
97 MediaWikiServices::forceGlobalInstance( $oldServices );
98 }
99
100 public function testResetGlobalInstance_quick() {
101 $newServices = $this->newMediaWikiServices();
102 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
103
104 $service1 = $this->createMock( SalvageableService::class );
105 $service1->expects( $this->never() )
106 ->method( 'salvage' );
107
108 $service2 = $this->createMock( SalvageableService::class );
109 $service2->expects( $this->once() )
110 ->method( 'salvage' )
111 ->with( $service1 );
112
113 // sequence of values the instantiator will return
114 $instantiatorReturnValues = [
115 $service1,
116 $service2,
117 ];
118
119 $newServices->defineService(
120 'Test',
121 function () use ( &$instantiatorReturnValues ) {
122 return array_shift( $instantiatorReturnValues );
123 }
124 );
125
126 // force instantiation
127 $newServices->getService( 'Test' );
128
129 MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' );
130 $theServices = MediaWikiServices::getInstance();
131
132 $this->assertSame( $service2, $theServices->getService( 'Test' ) );
133
134 $this->assertNotSame( $theServices, $newServices );
135 $this->assertNotSame( $theServices, $oldServices );
136
137 MediaWikiServices::forceGlobalInstance( $oldServices );
138 }
139
140 public function testDisableStorageBackend() {
141 $newServices = $this->newMediaWikiServices();
142 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
143
144 $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class )
145 ->disableOriginalConstructor()
146 ->getMock();
147
148 $newServices->redefineService(
149 'DBLoadBalancerFactory',
150 function () use ( $lbFactory ) {
151 return $lbFactory;
152 }
153 );
154
155 // force the service to become active, so we can check that it does get destroyed
156 $newServices->getService( 'DBLoadBalancerFactory' );
157
158 MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
159
160 try {
161 MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
162 $this->fail( 'DBLoadBalancerFactory should have been disabled' );
163 }
164 catch ( ServiceDisabledException $ex ) {
165 // ok, as expected
166 } catch ( Throwable $ex ) {
167 $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
168 }
169
170 MediaWikiServices::forceGlobalInstance( $oldServices );
171 $newServices->destroy();
172
173 // No exception was thrown, avoid being risky
174 $this->assertTrue( true );
175 }
176
177 public function testResetChildProcessServices() {
178 $newServices = $this->newMediaWikiServices();
179 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
180
181 $service1 = $this->createMock( DestructibleService::class );
182 $service1->expects( $this->once() )
183 ->method( 'destroy' );
184
185 $service2 = $this->createMock( DestructibleService::class );
186 $service2->expects( $this->never() )
187 ->method( 'destroy' );
188
189 // sequence of values the instantiator will return
190 $instantiatorReturnValues = [
191 $service1,
192 $service2,
193 ];
194
195 $newServices->defineService(
196 'Test',
197 function () use ( &$instantiatorReturnValues ) {
198 return array_shift( $instantiatorReturnValues );
199 }
200 );
201
202 // force the service to become active, so we can check that it does get destroyed
203 $oldTestService = $newServices->getService( 'Test' );
204
205 MediaWikiServices::resetChildProcessServices();
206 $finalServices = MediaWikiServices::getInstance();
207
208 $newTestService = $finalServices->getService( 'Test' );
209 $this->assertNotSame( $oldTestService, $newTestService );
210
211 MediaWikiServices::forceGlobalInstance( $oldServices );
212 }
213
214 public function testResetServiceForTesting() {
215 $services = $this->newMediaWikiServices();
216 $serviceCounter = 0;
217
218 $services->defineService(
219 'Test',
220 function () use ( &$serviceCounter ) {
221 $serviceCounter++;
222 $service = $this->createMock( Wikimedia\Services\DestructibleService::class );
223 $service->expects( $this->once() )->method( 'destroy' );
224 return $service;
225 }
226 );
227
228 // This should do nothing. In particular, it should not create a service instance.
229 $services->resetServiceForTesting( 'Test' );
230 $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
231
232 $oldInstance = $services->getService( 'Test' );
233 $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
234
235 // The old instance should be detached, and destroy() called.
236 $services->resetServiceForTesting( 'Test' );
237 $newInstance = $services->getService( 'Test' );
238
239 $this->assertNotSame( $oldInstance, $newInstance );
240
241 // Satisfy the expectation that destroy() is called also for the second service instance.
242 $newInstance->destroy();
243 }
244
245 public function testResetServiceForTesting_noDestroy() {
246 $services = $this->newMediaWikiServices();
247
248 $services->defineService(
249 'Test',
250 function () {
251 $service = $this->createMock( Wikimedia\Services\DestructibleService::class );
252 $service->expects( $this->never() )->method( 'destroy' );
253 return $service;
254 }
255 );
256
257 $oldInstance = $services->getService( 'Test' );
258
259 // The old instance should be detached, but destroy() not called.
260 $services->resetServiceForTesting( 'Test', false );
261 $newInstance = $services->getService( 'Test' );
262
263 $this->assertNotSame( $oldInstance, $newInstance );
264 }
265
266 public function provideGetters() {
267 $getServiceCases = $this->provideGetService();
268 $getterCases = [];
269
270 // All getters should be named just like the service, with "get" added.
271 foreach ( $getServiceCases as $name => $case ) {
272 if ( $name[0] === '_' ) {
273 // Internal service, no getter
274 continue;
275 }
276 list( $service, $class ) = $case;
277 $getterCases[$name] = [
278 'get' . $service,
279 $class,
280 in_array( $service, $this->deprecatedServices )
281 ];
282 }
283
284 return $getterCases;
285 }
286
287 /**
288 * @dataProvider provideGetters
289 */
290 public function testGetters( $getter, $type, $isDeprecated = false ) {
291 if ( $isDeprecated ) {
292 $this->hideDeprecated( MediaWikiServices::class . "::$getter" );
293 }
294
295 // Test against the default instance, since the dummy will not know the default services.
296 $services = MediaWikiServices::getInstance();
297 $service = $services->$getter();
298 $this->assertInstanceOf( $type, $service );
299 }
300
301 public function provideGetService() {
302 global $IP;
303 $serviceList = require "$IP/includes/ServiceWiring.php";
304 $ret = [];
305 foreach ( $serviceList as $name => $callback ) {
306 $fun = new ReflectionFunction( $callback );
307 if ( !$fun->hasReturnType() ) {
308 throw new MWException( 'All service callbacks must have a return type defined, ' .
309 "none found for $name" );
310 }
311 $ret[$name] = [ $name, $fun->getReturnType()->__toString() ];
312 }
313 return $ret;
314 }
315
316 /**
317 * @dataProvider provideGetService
318 */
319 public function testGetService( $name, $type ) {
320 // Test against the default instance, since the dummy will not know the default services.
321 $services = MediaWikiServices::getInstance();
322
323 $service = $services->getService( $name );
324 $this->assertInstanceOf( $type, $service );
325 }
326
327 public function testDefaultServiceInstantiation() {
328 // Check all services in the default instance, not a dummy instance!
329 // Note that we instantiate all services here, including any that
330 // were registered by extensions.
331 $services = MediaWikiServices::getInstance();
332 $names = $services->getServiceNames();
333
334 foreach ( $names as $name ) {
335 $this->assertTrue( $services->hasService( $name ) );
336 $service = $services->getService( $name );
337 $this->assertInternalType( 'object', $service );
338 }
339 }
340
341 public function testDefaultServiceWiringServicesHaveTests() {
342 global $IP;
343 $testedServices = array_keys( $this->provideGetService() );
344 $allServices = array_keys( require "$IP/includes/ServiceWiring.php" );
345 $this->assertEquals(
346 [],
347 array_diff( $allServices, $testedServices ),
348 'The following services have not been added to MediaWikiServicesTest::provideGetService'
349 );
350 }
351
352 public function testGettersAreSorted() {
353 $methods = ( new ReflectionClass( MediaWikiServices::class ) )
354 ->getMethods( ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC );
355
356 $names = array_map( function ( $method ) {
357 return $method->getName();
358 }, $methods );
359 $serviceNames = array_map( function ( $name ) {
360 return "get$name";
361 }, array_keys( $this->provideGetService() ) );
362 $names = array_values( array_filter( $names, function ( $name ) use ( $serviceNames ) {
363 return in_array( $name, $serviceNames );
364 } ) );
365
366 $sortedNames = $names;
367 natcasesort( $sortedNames );
368
369 $this->assertSame( $sortedNames, $names,
370 'Please keep service getters sorted alphabetically' );
371 }
372 }