From b2a2d3dc666ff091748c88d4f996ab4be5dda67f Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Wed, 16 Aug 2023 10:04:56 +0200 Subject: [PATCH 1/2] MDL-79338 core: add support for hook callback redirection in tests --- lib/classes/hook/manager.php | 37 +++++++++++++++++++++++ lib/phpunit/classes/advanced_testcase.php | 20 ++++++++++++ lib/phpunit/classes/util.php | 3 ++ 3 files changed, 60 insertions(+) diff --git a/lib/classes/hook/manager.php b/lib/classes/hook/manager.php index ca275516f12..1b29e4f2add 100644 --- a/lib/classes/hook/manager.php +++ b/lib/classes/hook/manager.php @@ -54,6 +54,9 @@ final class manager implements /** @var array list of all deprecated lib.php plugin callbacks. */ private $alldeprecations = []; + /** @var array list of redirected callbacks in PHPUnit tests */ + private $redirectedcallbacks = []; + /** * Constructor can be used only from factory methods. */ @@ -89,6 +92,32 @@ final class manager implements return $instance; } + /** + * Override hook callbacks for testing purposes. + * + * @param string $hookname + * @param callable $callback + * @return void + */ + public function phpunit_redirect_hook(string $hookname, callable $callback): void { + if (!PHPUNIT_TEST) { + throw new \coding_exception('Invalid call of manager::phpunit_redirect_hook() outside of tests'); + } + $this->redirectedcallbacks[$hookname] = $callback; + } + + /** + * Cancel all redirections of hook callbacks. + * + * @return void + */ + public function phpunit_stop_redirections(): void { + if (!PHPUNIT_TEST) { + throw new \coding_exception('Invalid call of manager::phpunit_stop_redirections() outside of tests'); + } + $this->redirectedcallbacks = []; + } + /** * Returns list of callbacks for given hook name. * @@ -215,6 +244,14 @@ final class manager implements return $event; } + if (PHPUNIT_TEST) { + $hookclassname = get_class($event); + if (isset($this->redirectedcallbacks[$hookclassname])) { + call_user_func($this->redirectedcallbacks[$hookclassname], $event); + return $event; + } + } + $callbacks = $this->getListenersForEvent($event); if (empty($callbacks)) { diff --git a/lib/phpunit/classes/advanced_testcase.php b/lib/phpunit/classes/advanced_testcase.php index fa7f8a14439..25ceef15107 100644 --- a/lib/phpunit/classes/advanced_testcase.php +++ b/lib/phpunit/classes/advanced_testcase.php @@ -499,6 +499,26 @@ abstract class advanced_testcase extends base_testcase { return phpunit_util::start_event_redirection(); } + /** + * Override hook callbacks. + * + * @param string $hookname + * @param callable $callback + * @return void + */ + public function redirectHook(string $hookname, callable $callback): void { + \core\hook\manager::get_instance()->phpunit_redirect_hook($hookname, $callback); + } + + /** + * Remove all hook overrides. + * + * @return void + */ + public function stopHookRedirections(): void { + \core\hook\manager::get_instance()->phpunit_stop_redirections(); + } + /** * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir. * diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 59b92725a26..fc131dc2614 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -104,6 +104,9 @@ class phpunit_util extends testing_util { public static function reset_all_data($detectchanges = false) { global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME, $FILTERLIB_PRIVATE; + // Stop all hook redirections. + \core\hook\manager::get_instance()->phpunit_stop_redirections(); + // Stop any message redirection. self::stop_message_redirection(); From f94ad6f65fcb2228042a763ba5ad3a146858e9a2 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Tue, 12 Sep 2023 17:30:46 +0200 Subject: [PATCH 2/2] MDL-79338 core: add primary navigation hook tests --- .../hook/navigation/primary_extend_test.php | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/lib/tests/hook/navigation/primary_extend_test.php b/lib/tests/hook/navigation/primary_extend_test.php index 0a7d208da91..6bb27fc8a43 100644 --- a/lib/tests/hook/navigation/primary_extend_test.php +++ b/lib/tests/hook/navigation/primary_extend_test.php @@ -46,4 +46,127 @@ class primary_extend_test extends \advanced_testcase { $hook->stop_propagation(); $this->assertTrue($hook->isPropagationStopped()); } + + /** + * Test hook is triggered when initialising primary navigation menu. + * @covers \core\navigation\views\primary::initialise + */ + public function test_trigggering() { + global $PAGE; + $this->resetAfterTest(); + + $PAGE = new \moodle_page(); + $PAGE->set_url('/'); + + $count = 0; + $receivedhook = null; + $testcallback = function(primary_extend $hook) use (&$receivedhook, &$count): void { + $count++; + $receivedhook = $hook; + }; + $this->redirectHook(primary_extend::class, $testcallback); + + $primarynav = new \core\navigation\views\primary($PAGE); + $this->assertSame(0, $count); + $this->assertNull($receivedhook); + + $primarynav->initialise(); + $this->assertSame(1, $count); + $this->assertInstanceOf(primary_extend::class, $receivedhook); + } + + /** + * Verify that nothing except this hook modifies the primary menu. + * @covers \core\navigation\views\primary::initialise + */ + public function test_unsupported_hacks() { + global $PAGE; + $this->resetAfterTest(); + + $PAGE = new \moodle_page(); + $PAGE->set_url('/'); + + $testcallback = function(primary_extend $hook): void { + // Nothing to do, propagation is stopped by hook redirection. + }; + $this->redirectHook(primary_extend::class, $testcallback); + + $primarynav = new \core\navigation\views\primary($PAGE); + $primarynav->initialise(); + $this->assertSame(['home'], $primarynav->get_children_key_list(), + 'Unsupported primary menu modification detected, use new primary_extend hook instead.'); + + $this->setAdminUser(); + $primarynav = new \core\navigation\views\primary($PAGE); + $primarynav->initialise(); + $this->assertSame(['home', 'myhome', 'mycourses'], $primarynav->get_children_key_list(), + 'Unsupported primary menu modification detected, use new primary_extend hook instead.'); + } + + /** + * Test adding of primary menu items via hook. + * @covers \core\navigation\views\primary::initialise + */ + public function test_primary_menu_extending() { + global $PAGE; + $this->resetAfterTest(); + + $PAGE = new \moodle_page(); + $PAGE->set_url('/'); + + $testcallback = function(primary_extend $hook): void { + $primaryview = $hook->get_primaryview(); + $primaryview->add('Pokus', null); + }; + $this->redirectHook(primary_extend::class, $testcallback); + + $primarynav = new \core\navigation\views\primary($PAGE); + $primarynav->initialise(); + $keys = $primarynav->get_children_key_list(); + $this->assertCount(2, $keys); + $firstkey = array_shift($keys); + $this->assertSame('home', $firstkey); + $secondkey = array_shift($keys); + /** @var \navigation_node $pokus */ + $pokus = $primarynav->get($secondkey); + $this->assertInstanceOf(\navigation_node::class, $pokus); + $this->assertSame('Pokus', $pokus->text); + } + + /** + * Test replacing of the whole primary menu. + * @covers \core\navigation\views\primary::initialise + */ + public function test_primary_menu_replacing() { + global $PAGE; + $this->resetAfterTest(); + + $PAGE = new \moodle_page(); + $PAGE->set_url('/'); + + $testcallback = function(primary_extend $hook): void { + $primaryview = $hook->get_primaryview(); + $keys = $primaryview->get_children_key_list(); + foreach ($keys as $key) { + $item = $primaryview->get($key); + $item->remove(); + } + $primaryview->add('Pokus', null); + // Technically we do not need to stop because observers are overridden, + // but this can be used as an example for plugin that wants to stop + // adding of primary menu items from plugins. + $hook->stop_propagation(); + }; + $this->redirectHook(primary_extend::class, $testcallback); + + $primarynav = new \core\navigation\views\primary($PAGE); + $primarynav->initialise(); + $keys = $primarynav->get_children_key_list(); + $this->assertCount(1, $keys); + $firstkey = array_shift($keys); + /** @var \navigation_node $pokus */ + $pokus = $primarynav->get($firstkey); + $this->assertInstanceOf(\navigation_node::class, $pokus); + $this->assertSame('Pokus', $pokus->text); + } }