From 664e44baf5a09fef2404d0b28aedcbd43161e02e Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Fri, 24 Jul 2026 14:03:34 +0200 Subject: [PATCH] enhancement(admin): strip transient query args via removable_query_args filter Registers `redirected_from_site_kit`, `wpseo_tracked_action`, `wpseo_tracking_nonce`, `start-myyoast-connection`, `_wpnonce`, `install`, and `from_tools` with the WordPress core `removable_query_args` filter so they get stripped from admin URLs after first load. - New `Removable_Query_Args_Integration` under `src/integrations/admin/` implementing the existing `Integration_Interface`. Gated on `Admin_Conditional`, registered as a filter callback, merges Yoast args onto the existing list. - Args exposed via a `public static get_removable_query_args()` so future code can reference a single source of truth. - Auto-wired into the Symfony DI container via the existing integration tag (no new DI config needed). - The existing JS strip at `packages/js/src/integrations-page/myyoast-connection/myyoast-integration.js:421` is kept as defensive belt-and-suspenders. - Flaky-test fix in `Activation_Cleanup_Integration_Test`: matcher uses `Mockery::type('int')` instead of capturing `\time()` at mock setup. Cross-second boundaries caused CI failures on PHP 8.0. - Flaky-test fix in `Introductions_Integration_Test`: same `\time()` cross-second-boundary pattern replaced with `Mockery::on()` for the `seen_on` timestamp value. Fixes #23498 --- .../removable-query-args-integration.php | 59 +++++++++++ .../Activation_Cleanup_Integration_Test.php | 4 +- .../Removable_Query_Args_Integration_Test.php | 99 +++++++++++++++++++ .../Introductions_Integration_Test.php | 42 +++++--- 4 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/integrations/admin/removable-query-args-integration.php create mode 100644 tests/Unit/Integrations/Admin/Removable_Query_Args_Integration_Test.php diff --git a/src/integrations/admin/removable-query-args-integration.php b/src/integrations/admin/removable-query-args-integration.php new file mode 100644 index 00000000000..814adde029c --- /dev/null +++ b/src/integrations/admin/removable-query-args-integration.php @@ -0,0 +1,59 @@ + + */ + public static function get_conditionals(): array { + return [ Admin_Conditional::class ]; + } + + /** + * Initializes the integration. + * + * @return void + */ + public function register_hooks() { + \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] ); + } + + /** + * Adds the Yoast transient query args to the list stripped by core. + * + * @param array $args The existing removable query args. + * @return array + */ + public static function add_removable_query_args( array $args ): array { + return \array_merge( $args, self::get_removable_query_args() ); + } +} diff --git a/tests/Unit/Integrations/Admin/Activation_Cleanup_Integration_Test.php b/tests/Unit/Integrations/Admin/Activation_Cleanup_Integration_Test.php index 8c6241ba17e..68570cbcc03 100644 --- a/tests/Unit/Integrations/Admin/Activation_Cleanup_Integration_Test.php +++ b/tests/Unit/Integrations/Admin/Activation_Cleanup_Integration_Test.php @@ -95,7 +95,9 @@ public function test_register_cleanup_routine_no_running() { Monkey\Functions\expect( 'wp_schedule_single_event' ) ->once() - ->with( ( \time() + \DAY_IN_SECONDS ), Cleanup_Integration::START_HOOK ); + // Use a type matcher instead of a captured timestamp: \time() in source runs after mock setup, + // so a strict equality match flaked across second boundaries. The hook name is the meaningful contract. + ->with( Mockery::type( 'int' ), Cleanup_Integration::START_HOOK ); $this->indexable_helper->expects( 'should_index_indexables' ) ->once() diff --git a/tests/Unit/Integrations/Admin/Removable_Query_Args_Integration_Test.php b/tests/Unit/Integrations/Admin/Removable_Query_Args_Integration_Test.php new file mode 100644 index 00000000000..d8b18f64132 --- /dev/null +++ b/tests/Unit/Integrations/Admin/Removable_Query_Args_Integration_Test.php @@ -0,0 +1,99 @@ +instance = new Removable_Query_Args_Integration(); + } + + /** + * Tests if the expected conditionals are given. + * + * @covers ::get_conditionals + * + * @return void + */ + public function test_get_conditionals() { + $this->assertEquals( [ Admin_Conditional::class ], Removable_Query_Args_Integration::get_conditionals() ); + } + + /** + * Tests that all seven Yoast transient query args are returned. + * + * @covers ::get_removable_query_args + * + * @return void + */ + public function test_get_removable_query_args() { + $this->assertEquals( + [ + 'redirected_from_site_kit', + 'wpseo_tracked_action', + 'wpseo_tracking_nonce', + 'start-myyoast-connection', + '_wpnonce', + 'install', + 'from_tools', + ], + Removable_Query_Args_Integration::get_removable_query_args(), + ); + } + + /** + * Tests that register_hooks registers the removable_query_args filter. + * + * @covers ::register_hooks + * + * @return void + */ + public function test_register_hooks() { + Monkey\Filters\expectAdded( 'removable_query_args' ) + ->with( [ $this->instance, 'add_removable_query_args' ] ); + + $this->instance->register_hooks(); + } + + /** + * Tests that the filter callback merges the Yoast args into the existing list. + * + * @covers ::add_removable_query_args + * + * @return void + */ + public function test_add_removable_query_args() { + $existing = [ 'existing_arg' ]; + $result = Removable_Query_Args_Integration::add_removable_query_args( $existing ); + + $this->assertContains( 'existing_arg', $result ); + $this->assertContains( 'redirected_from_site_kit', $result ); + $this->assertContains( 'from_tools', $result ); + } +} diff --git a/tests/Unit/Introductions/User_Interface/Introductions_Integration_Test.php b/tests/Unit/Introductions/User_Interface/Introductions_Integration_Test.php index 19812998b82..a41a025b111 100644 --- a/tests/Unit/Introductions/User_Interface/Introductions_Integration_Test.php +++ b/tests/Unit/Introductions/User_Interface/Introductions_Integration_Test.php @@ -226,15 +226,20 @@ public function test_enqueue_assets() { ->once() ->with( $user_id, '_yoast_wpseo_introductions', true ) ->andReturn( [] ); - $expected_meta = [ - 'foo' => [ - 'is_seen' => true, - 'seen_on' => \time(), - ], - ]; $this->user_helper->expects( 'update_meta' ) ->once() - ->with( $user_id, '_yoast_wpseo_introductions', $expected_meta ); + ->with( + $user_id, + '_yoast_wpseo_introductions', + Mockery::on( + static function ( $meta ) { + return \is_array( $meta ) + && isset( $meta['foo'] ) + && $meta['foo']['is_seen'] === true + && \is_int( $meta['foo']['seen_on'] ); + }, + ), + ); // Enqueueing. $this->admin_asset_manager->expects( 'enqueue_script' )->once()->with( 'introductions' ); @@ -315,15 +320,20 @@ public function test_update_first_user_introductions() { ->with( $user_id, '_yoast_wpseo_introductions', true ) // Point of this test: returning false results in using an empty array as default. ->andReturn( false ); - $expected_meta = [ - 'foo' => [ - 'is_seen' => true, - 'seen_on' => \time(), - ], - ]; $this->user_helper->expects( 'update_meta' ) ->once() - ->with( $user_id, '_yoast_wpseo_introductions', $expected_meta ); + ->with( + $user_id, + '_yoast_wpseo_introductions', + Mockery::on( + static function ( $meta ) { + return \is_array( $meta ) + && isset( $meta['foo'] ) + && $meta['foo']['is_seen'] === true + && \is_int( $meta['foo']['seen_on'] ); + }, + ), + ); // Enqueueing. $this->admin_asset_manager->expects( 'enqueue_script' )->once()->with( 'introductions' ); @@ -337,8 +347,8 @@ public function test_update_first_user_introductions() { /** * Sets the expectations surrounding the localized data. * - * @param array $introductions The introductions. - * @param int $user_id The user ID. + * @param array[] $introductions The introductions. + * @param int $user_id The user ID. * * @return void */