wiki.techinc.nl/tests/phpunit/unit/includes/HookContainer/HookContainerTest.php
libraryupgrader 5357695270 build: Updating dependencies
composer:
* mediawiki/mediawiki-codesniffer: 36.0.0 → 37.0.0
  The following sniffs now pass and were enabled:
  * Generic.ControlStructures.InlineControlStructure
  * MediaWiki.PHPUnit.AssertCount.NotUsed

npm:
* svgo: 2.3.0 → 2.3.1
  * https://npmjs.com/advisories/1754 (CVE-2021-33587)

Change-Id: I2a9bbee2fecbf7259876d335f565ece4b3622426
2021-07-22 03:36:05 +00:00

645 lines
19 KiB
PHP

<?php
namespace MediaWiki\HookContainer {
use MediaWikiUnitTestCase;
use Psr\Container\ContainerInterface;
use UnexpectedValueException;
use Wikimedia\ObjectFactory;
use Wikimedia\ScopedCallback;
class HookContainerTest extends MediaWikiUnitTestCase {
/*
* Creates a new hook container with StaticHookRegistry and mocked ObjectFactory
*/
private function newHookContainer(
$oldHooks = null, $newHooks = null, $deprecatedHooksArray = []
) {
if ( $oldHooks === null ) {
$oldHooks[ 'FoobarActionComplete' ][] = static function ( &$called ) {
$called[] = 11;
};
}
if ( $newHooks === null ) {
$handler = [ 'handler' => [
'name' => 'FooExtension-FooActionHandler',
'class' => 'FooExtension\\Hooks',
'services' => [] ]
];
$newHooks = [ 'FooActionComplete' => [ $handler ] ];
}
$mockObjectFactory = $this->getObjectFactory();
$registry = new StaticHookRegistry( $oldHooks, $newHooks, $deprecatedHooksArray );
$hookContainer = new HookContainer( $registry, $mockObjectFactory );
return $hookContainer;
}
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' ] ],
'Class name and static method' => [
'MWTestHook',
[ 'MediaWiki\HookContainer\FooClass', 'FooStaticMethod' ]
],
'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', static function () {
return true;
} ],
'Closure with data' => [ 'MWTestHook', static 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 ],
[ static 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();
$this->assertFalse( $accessibleHookContainer->isRegistered( 'TestHook' ) );
$accessibleHookContainer->salvage( $hookContainer );
$this->assertTrue( $accessibleHookContainer->isRegistered( 'TestHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::salvage
*/
public function testSalvageThrows() {
$this->expectException( \MWException::class );
$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 testScopedRegisterTwoHandlers() {
$hookContainer = $this->newHookContainer();
$called1 = $called2 = false;
$reset1 = $hookContainer->scopedRegister( 'MWTestHook',
static function () use ( &$called1 ) {
$called1 = true;
}, false
);
$reset2 = $hookContainer->scopedRegister( 'MWTestHook',
static 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 );
}
/**
* Register handlers with scopedReigster() and register()
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
*/
public function testHandlersRegisteredWithScopedRegisterAndRegister() {
$hookContainer = $this->newHookContainer();
$numCalls = 0;
$hookContainer->register( 'MWTestHook', static function () use ( &$numCalls ) {
$numCalls++;
} );
$reset = $hookContainer->scopedRegister( 'MWTestHook', static function () use ( &$numCalls ) {
$numCalls++;
} );
// handlers registered in 2 different ways
$this->assertCount( 2, $hookContainer->getLegacyHandlers( 'MWTestHook' ) );
$hookContainer->run( 'MWTestHook' );
$this->assertEquals( 2, $numCalls );
// Remove one of the handlers that increments $called
ScopedCallback::consume( $reset );
$this->assertCount( 1, $hookContainer->getLegacyHandlers( 'MWTestHook' ) );
$numCalls = 0;
$hookContainer->run( 'MWTestHook' );
$this->assertSame( 1, $numCalls );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
*/
public function testNotRegisteredLegacy() {
$hookContainer = $this->newHookContainer();
$this->assertFalse( $hookContainer->isRegistered( 'UnregisteredHook' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::getHandlers
* @dataProvider provideGetHandlers
*/
public function testGetHandlers( string $hook, ?array $handlerToRegister, array $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::class );
$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()
*/
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', static 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();
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::emitDeprecationWarnings
*/
public function testEmitDeprecationWarningsSilent() {
$hooks = [
'FooActionComplete' => [
[
'handler' => 'FooGlobalFunction',
'extensionPath' => 'fake-extension.json'
]
]
];
$deprecatedHooksArray = [
'FooActionComplete' => [
'deprecatedVersion' => '1.35',
'silent' => true
]
];
$hookContainer = $this->newHookContainer( [], $hooks, $deprecatedHooksArray );
$hookContainer->emitDeprecationWarnings();
$this->assertTrue( true );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::clear
*/
public function testClear() {
// Register handlers in three different ways
$hookContainer = $this->newHookContainer(
[ 'Increment' => [ [ new \FooExtension\Hooks(), 'onIncrement' ] ] ],
[ 'Increment' => [ [ 'handler' => [
'name' => 'TestIncrement',
'class' => 'FooExtension\Hooks'
] ] ] ]
);
$hookContainer->register( 'Increment', static function ( &$count ) {
$count++;
} );
// Sanity check: all three handlers should be called initially.
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( 3, $count );
$this->assertTrue( $hookContainer->isRegistered( 'Increment' ) );
$hookContainer->clear( 'Increment' );
// both handlers should now be disabled
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( 0, $count );
$this->assertFalse( $hookContainer->isRegistered( 'Increment' ) );
// When adding a handler again...
$hookContainer->register( 'Increment', static function ( &$count ) {
$count = 11;
} );
// ...the new handler should be called, but not the old ones.
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( 11, $count );
$this->assertTrue( $hookContainer->isRegistered( 'Increment' ) );
}
/**
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
*/
public function testScopedHandlerWithReplace() {
$hookContainer = $this->newHookContainer(
[ 'Increment' => [ [ new \FooExtension\Hooks(), 'onIncrement' ] ] ],
[ 'Increment' => [ [ 'handler' => [
'name' => 'TestIncrement',
'class' => 'FooExtension\Hooks'
] ] ] ]
);
$hookContainer->register( 'Increment', static function ( &$count ) {
$count++;
} );
// Sanity check: both handlers should be called initially.
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( 3, $count );
$this->assertTrue( $hookContainer->isRegistered( 'Increment' ) );
// Adding a scoped handler, with the $replace flag set.
$scope1 = $hookContainer->scopedRegister( 'Increment', static function ( &$count ) {
$count -= 3;
}, true );
// original handlers should now be disabled, the scoped handler active
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( -3, $count );
$this->assertTrue( $hookContainer->isRegistered( 'Increment' ) );
// Adding another permanent handler should work...
$hookContainer->register( 'Increment', static function ( &$count ) {
$count++;
} );
// ...so that now the temporary and the permanent handler are called.
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( -2, $count );
// Adding another scoped handler, with the $replace flag set.
$scope2 = $hookContainer->scopedRegister( 'Increment', static function ( &$count ) {
$count -= 10;
}, true );
// Only the new scoped callback should now be active
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( -10, $count );
// After consuming the first scoped callback, the second one still overrides...
ScopedCallback::consume( $scope1 );
// ...so that still only the new scoped callback should now be active
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( -10, $count );
// After also consuming the second scoped callback,
// all four permanent handlers should be active,
// since all scoped callbacks are out of the way.
ScopedCallback::consume( $scope2 );
$count = 0;
$hookContainer->run( 'Increment', [ &$count ] );
$this->assertSame( 4, $count );
$this->assertTrue( $hookContainer->isRegistered( 'Increment' ) );
}
}
// 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;
}
public function onIncrement( &$count ) {
$count++;
}
}
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;
}
}
}