wiki.techinc.nl/tests/phpunit/unit/includes/HookContainer/HookContainerTest.php
Tim Starling b3d762e148 Add HookRegistry
Add a HookRegistry interface and two concrete implementations,
representing HookContainer's view of its environment. This simplifies
creation of a HookContainer for testing.

Add MediaWikiTestCaseTrait::createHookContainer() which can be used
in most of the places that were previously creating mock hook
containers. It can also replace setTemporaryHook() in some cases.

Change-Id: I9ce15591dc40b3d717c203fa973141aa45a2500c
2020-05-13 11:51:02 +10:00

488 lines
14 KiB
PHP

<?php
namespace MediaWiki\HookContainer {
use Psr\Container\ContainerInterface;
use UnexpectedValueException;
use Wikimedia\ObjectFactory;
use ExtensionRegistry;
use MediaWikiUnitTestCase;
use Wikimedia\ScopedCallback;
use Wikimedia\TestingAccessWrapper;
class HookContainerTest extends MediaWikiUnitTestCase {
/*
* Creates a new hook container with mocked ObjectFactory, ExtensionRegistry, and DeprecatedHooks
*/
private function newHookContainer(
$hooks = null, $deprecatedHooksArray = []
) {
if ( $hooks === null ) {
$handler = [ 'handler' => [
'name' => 'FooExtension-FooActionHandler',
'class' => 'FooExtension\\Hooks',
'services' => [] ]
];
$hooks = [ 'FooActionComplete' => [ $handler ] ];
}
$mockObjectFactory = $this->getObjectFactory();
$registry = new StaticHookRegistry( [], $hooks, $deprecatedHooksArray );
$hookContainer = new HookContainer( $registry, $mockObjectFactory );
return $hookContainer;
}
private function getMockExtensionRegistry( $hooks ) {
$mockRegistry = $this->createNoOpMock( ExtensionRegistry::class, [ 'getAttribute' ] );
$mockRegistry->method( 'getAttribute' )
->with( 'Hooks' )
->willReturn( $hooks );
return $mockRegistry;
}
private function getObjectFactory() {
$mockServiceContainer = $this->createMock( ContainerInterface::class );
$mockServiceContainer->method( 'get' )
->willThrowException( new \RuntimeException );
$objectFactory = new ObjectFactory( $mockServiceContainer );
return $objectFactory;
}
/**
* Values returned: hook, handler, handler arguments, options
*/
public static function provideRunLegacy() {
$fooObj = new FooClass();
$arguments = [ 'ParamsForHookHandler' ];
return [
'Method' => [ 'MWTestHook', 'FooGlobalFunction' ],
'Falsey value' => [ 'MWTestHook', false ],
'Method with arguments' => [ 'MWTestHook', [ 'FooGlobalFunction' ], $arguments ],
'Method in array' => [ 'MWTestHook', [ 'FooGlobalFunction' ] ],
'Object with no method' => [ 'MWTestHook', $fooObj ],
'Object with no method in array' => [ 'MWTestHook', [ $fooObj ], $arguments ],
'Object and method' => [ 'MWTestHook', [ $fooObj, 'FooMethod' ] ],
'Object and static method' => [
'MWTestHook',
[ 'MediaWiki\HookContainer\FooClass::FooStaticMethod' ]
],
'Object and static method as array' => [
'MWTestHook',
[ [ 'MediaWiki\HookContainer\FooClass::FooStaticMethod' ] ]
],
'Object and fully-qualified non-static method' => [
'MWTestHook',
[ $fooObj, 'MediaWiki\HookContainer\FooClass::FooMethod' ]
],
'Closure' => [ 'MWTestHook', function () {
return true;
} ],
'Closure with data' => [ 'MWTestHook', function () {
return true;
}, [ 'data' ] ]
];
}
/**
* Values returned: hook, handlersToRegister, expectedReturn
*/
public static function provideGetHandlers() {
return [
'NoHandlersExist' => [ 'MWTestHook', null, [] ],
'SuccessfulHandlerReturn' => [
'FooActionComplete',
[ 'handler' => [
'name' => 'FooExtension-FooActionHandler',
'class' => 'FooExtension\\Hooks',
'services' => [] ]
],
[ new \FooExtension\Hooks() ]
],
'SkipDeprecated' => [
'FooActionCompleteDeprecated',
[ 'handler' => [
'name' => 'FooExtension-FooActionHandler',
'class' => 'FooExtension\\Hooks',
'services' => [] ],
'deprecated' => true
],
[]
],
];
}
/**
* Values returned: hook, handlersToRegister, options
*/
public static function provideRunLegacyErrors() {
return [
[ 123 ],
[ function () {
return 'string';
} ]
];
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::salvage
*/
public function testSalvage() {
$hookContainer = $this->newHookContainer();
$hookContainer->register( 'TestHook', 'TestHandler' );
$this->assertTrue( $hookContainer->isRegistered( 'TestHook' ) );
$accessibleHookContainer = $this->newHookContainer();
$testingAccessHookContainer = TestingAccessWrapper::newFromObject( $accessibleHookContainer );
$this->assertFalse( $testingAccessHookContainer->isRegistered( 'TestHook' ) );
$testingAccessHookContainer->salvage( $hookContainer );
$this->assertTrue( $testingAccessHookContainer->isRegistered( 'TestHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::salvage
*/
public function testSalvageThrows() {
$this->expectException( 'MWException' );
$hookContainer = $this->newHookContainer();
$hookContainer->register( 'TestHook', 'TestHandler' );
$hookContainer->salvage( $hookContainer );
$this->assertTrue( $hookContainer->isRegistered( 'TestHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
* @covers \MediaWiki\HookContainer\HookContainer::register
*/
public function testRegisteredLegacy() {
$hookContainer = $this->newHookContainer();
$this->assertFalse( $hookContainer->isRegistered( 'MWTestHook' ) );
$hookContainer->register( 'MWTestHook', [ new FooClass(), 'FooMethod' ] );
$this->assertTrue( $hookContainer->isRegistered( 'MWTestHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
*/
public function testScopedRegister() {
$hookContainer = $this->newHookContainer();
$reset = $hookContainer->scopedRegister( 'MWTestHook', [ new FooClass(), 'FooMethod' ] );
$this->assertTrue( $hookContainer->isRegistered( 'MWTestHook' ) );
ScopedCallback::consume( $reset );
$this->assertFalse( $hookContainer->isRegistered( 'MWTestHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
*/
public function testScopedRegister2() {
$hookContainer = $this->newHookContainer();
$called1 = $called2 = false;
$reset1 = $hookContainer->scopedRegister( 'MWTestHook',
function () use ( &$called1 ) {
$called1 = true;
}, false
);
$reset2 = $hookContainer->scopedRegister( 'MWTestHook',
function () use ( &$called2 ) {
$called2 = true;
}, false
);
$hookContainer->run( 'MWTestHook' );
$this->assertTrue( $called1 );
$this->assertTrue( $called2 );
$called1 = $called2 = false;
$reset1 = null;
$hookContainer->run( 'MWTestHook' );
$this->assertFalse( $called1 );
$this->assertTrue( $called2 );
$called1 = $called2 = false;
$reset2 = null;
$hookContainer->run( 'MWTestHook' );
$this->assertFalse( $called1 );
$this->assertFalse( $called2 );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
*/
public function testNotRegisteredLegacy() {
$hookContainer = $this->newHookContainer();
$this->assertFalse( $hookContainer->isRegistered( 'UnregisteredHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::getHandlers
* @dataProvider provideGetHandlers
* @param $hook
* @param $handlerToRegister
* @param $expectedReturn
*/
public function testGetHandlers( $hook, $handlerToRegister, $expectedReturn ) {
if ( $handlerToRegister ) {
$hooks = [ $hook => [ $handlerToRegister ] ];
} else {
$hooks = [];
}
$fakeDeprecatedHooks = [
'FooActionCompleteDeprecated' => [ 'deprecatedVersion' => '1.35' ]
];
$hookContainer = $this->newHookContainer( $hooks, $fakeDeprecatedHooks );
$handlers = $hookContainer->getHandlers( $hook );
$this->assertArrayEquals(
$handlers,
$expectedReturn,
'HookContainer::getHandlers() should return array of handler functions'
);
}
/**
* @dataProvider provideRunLegacyErrors
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
* Test errors thrown with invalid handlers
*/
public function testRunLegacyErrors() {
$hookContainer = $this->newHookContainer();
$this->hideDeprecated(
'returning a string from a hook handler (done by hook-MWTestHook-closure for MWTestHook)'
);
$this->expectException( 'UnexpectedValueException' );
$hookContainer->register( 'MWTestHook', 123 );
$hookContainer->run( 'MWTestHook', [] );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::getLegacyHandlers
*/
public function testGetLegacyHandlers() {
$hookContainer = $this->newHookContainer();
$hookContainer->register(
'FooLegacyActionComplete',
[ new FooClass(), 'FooMethod' ]
);
$expectedHandlers = [ [ new FooClass(), 'FooMethod' ] ];
$hookHandlers = $hookContainer->getLegacyHandlers( 'FooLegacyActionComplete' );
$this->assertIsCallable( $hookHandlers[0] );
$this->assertArrayEquals(
$hookHandlers,
$expectedHandlers,
true
);
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::run
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
* @dataProvider provideRunLegacy
* Test Hook run with legacy hook system, registered via wgHooks()
* @param $event
* @param $hook
* @param array $hookArguments
* @param array $options
* @throws \FatalError
*/
public function testRunLegacy( $event, $hook, $hookArguments = [], $options = [] ) {
$hookContainer = $this->newHookContainer();
$hookContainer->register( $event, $hook );
$hookValue = $hookContainer->run( $event, $hookArguments, $options );
$this->assertTrue( $hookValue );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::run
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
* Test HookContainer::run() with abortable option
*/
public function testRunNotAbortable() {
$handler = [ 'handler' => [
'name' => 'FooExtension-InvalidReturnHandler',
'class' => 'FooExtension\\Hooks',
'services' => [] ]
];
$hookContainer = $this->newHookContainer( [ 'InvalidReturnHandler' => [ $handler ] ] );
$this->expectException( UnexpectedValueException::class );
$this->expectExceptionMessage(
"Invalid return from onInvalidReturnHandler for " .
"unabortable InvalidReturnHandler"
);
$hookRun = $hookContainer->run( 'InvalidReturnHandler', [], [ 'abortable' => false ] );
$this->assertTrue( $hookRun );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::run
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
* Test HookContainer::run() when the handler returns false
*/
public function testRunAbort() {
$handler1 = [ 'handler' => [
'name' => 'FooExtension-Abort1',
'class' => 'FooExtension\\AbortHooks1'
] ];
$handler2 = [ 'handler' => [
'name' => 'FooExtension-Abort2',
'class' => 'FooExtension\\AbortHooks2'
] ];
$handler3 = [ 'handler' => [
'name' => 'FooExtension-Abort3',
'class' => 'FooExtension\\AbortHooks3'
] ];
$hooks = [
'Abort' => [
$handler1,
$handler2,
$handler3
]
];
$hookContainer = $this->newHookContainer( $hooks );
$called = [];
$ret = $hookContainer->run( 'Abort', [ &$called ] );
$this->assertFalse( $ret );
$this->assertArrayEquals( [ 1, 2 ], $called );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::register
* Test HookContainer::register() successfully registers even when hook is deprecated
*/
public function testRegisterDeprecated() {
$this->hideDeprecated( 'FooActionComplete hook' );
$fakeDeprecatedHooks = [ 'FooActionComplete' => [ 'deprecatedVersion' => '1.0' ] ];
$handler = [
'handler' => [
'name' => 'FooExtension-FooActionHandler',
'class' => 'FooExtension\\Hooks',
'services' => []
]
];
$hookContainer = $this->newHookContainer(
[ 'FooActionComplete' => [ $handler ] ],
$fakeDeprecatedHooks );
$hookContainer->register( 'FooActionComplete', new FooClass() );
$this->assertTrue( $hookContainer->isRegistered( 'FooActionComplete' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
* Test HookContainer::isRegistered() with current hook system with arguments
*/
public function testIsRegistered() {
$hookContainer = $this->newHookContainer();
$hookContainer->register( 'FooActionComplete', function () {
return true;
} );
$isRegistered = $hookContainer->isRegistered( 'FooActionComplete' );
$this->assertTrue( $isRegistered );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::run
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
* Test HookContainer::run() throws exceptions appropriately
*/
public function testRunExceptions() {
$handler = [ 'handler' => [
'name' => 'FooExtension-InvalidReturnHandler',
'class' => 'FooExtension\\Hooks',
'services' => [] ]
];
$hookContainer = $this->newHookContainer(
[ 'InvalidReturnHandler' => [ $handler ] ] );
$this->expectException( UnexpectedValueException::class );
$hookContainer->run( 'InvalidReturnHandler' );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::emitDeprecationWarnings
*/
public function testEmitDeprecationWarnings() {
$hooks = [
'FooActionComplete' => [
[
'handler' => 'FooGlobalFunction',
'extensionPath' => 'fake-extension.json'
]
]
];
$deprecatedHooksArray = [
'FooActionComplete' => [ 'deprecatedVersion' => '1.35' ]
];
$hookContainer = $this->newHookContainer( $hooks, $deprecatedHooksArray );
$this->expectDeprecation();
$hookContainer->emitDeprecationWarnings();
}
}
// Mock class for different types of handler functions
class FooClass {
public function FooMethod( $data = false ) {
return true;
}
public static function FooStaticMethod() {
return true;
}
public static function FooMethodReturnValueError() {
return 'a string';
}
public static function onMWTestHook() {
return true;
}
}
}
// Function in global namespace
namespace {
function FooGlobalFunction() {
return true;
}
}
// Mock Extension
namespace FooExtension {
class Hooks {
public function OnFooActionComplete() {
return true;
}
public function onInvalidReturnHandler() {
return 123;
}
}
class AbortHooks1 {
public function onAbort( &$called ) {
$called[] = 1;
return true;
}
}
class AbortHooks2 {
public function onAbort( &$called ) {
$called[] = 2;
return false;
}
}
class AbortHooks3 {
public function onAbort( &$called ) {
$called[] = 3;
return true;
}
}
}