From fd936f5a5fe794c03d9083d75bace593c4f41c75 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 10:37:19 +0530 Subject: [PATCH 01/18] tests: added tests for Search/Index module --- tests/php/Unit/Modules/Search/IndexTest.php | 148 ++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 tests/php/Unit/Modules/Search/IndexTest.php diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php new file mode 100644 index 0000000..de2e4a9 --- /dev/null +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -0,0 +1,148 @@ +get_index(); + + $this->assertWPError( $result ); + } + + /** + * Returns SearchIndex when credentials are valid. + */ + public function test_get_index_returns_search_index_with_valid_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ] + ); + + $index = new Index(); + $result = $index->get_index(); + + $this->assertInstanceOf( \OneSearch\Vendor\Algolia\AlgoliaSearch\SearchIndex::class, $result ); + } + + /** + * Caches the index instance on subsequent calls. + */ + public function test_get_index_returns_same_instance_on_second_call(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ] + ); + + $index = new Index(); + $first = $index->get_index(); + $second = $index->get_index(); + + $this->assertSame( $first, $second ); + } + + // ── delete_index ──────────────────────────────────────────────────── + + /** + * Returns WP_Error when credentials are missing. + */ + public function test_delete_index_returns_error_without_credentials(): void { + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $result = ( new Index() )->delete_index(); + + $this->assertWPError( $result ); + } + + // ── delete_by ─────────────────────────────────────────────────────── + + /** + * Returns WP_Error when credentials are missing. + */ + public function test_delete_by_returns_error_without_credentials(): void { + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $result = ( new Index() )->delete_by( [ 'filters' => 'site_url:"http://test.com"' ] ); + + $this->assertWPError( $result ); + } + + // ── save_records ──────────────────────────────────────────────────── + + /** + * Returns WP_Error when credentials are missing. + */ + public function test_save_records_returns_error_without_credentials(): void { + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $result = ( new Index() )->save_records( [] ); + + $this->assertWPError( $result ); + } + + // ── search ────────────────────────────────────────────────────────── + + /** + * Returns WP_Error when credentials are missing. + */ + public function test_search_returns_error_without_credentials(): void { + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $result = ( new Index() )->search( 'test query' ); + + $this->assertWPError( $result ); + } + + // ── index_all_posts ───────────────────────────────────────────────── + + /** + * Returns WP_Error when delete_by fails (no credentials). + */ + public function test_index_all_posts_returns_error_when_delete_fails(): void { + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $result = ( new Index() )->index_all_posts( [ 'post' ] ); + + $this->assertWPError( $result ); + } +} From f63d2961c63a236267b763bf78d7a3a284e77a3f Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 10:37:46 +0530 Subject: [PATCH 02/18] tests: added tests for Search/Search module --- tests/php/Unit/Modules/Search/SearchTest.php | 293 +++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 tests/php/Unit/Modules/Search/SearchTest.php diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php new file mode 100644 index 0000000..aaff00e --- /dev/null +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -0,0 +1,293 @@ +register_hooks(); + + $this->assertNotFalse( has_filter( 'posts_pre_query', [ $search, 'get_algolia_results' ] ) ); + } + + /** + * Ensures register_hooks adds permalink filters. + */ + public function test_register_hooks_adds_permalink_filters(): void { + $search = new Search(); + $search->register_hooks(); + + $this->assertNotFalse( has_filter( 'post_link', [ $search, 'get_post_type_permalink' ] ) ); + $this->assertNotFalse( has_filter( 'page_link', [ $search, 'get_post_type_permalink' ] ) ); + $this->assertNotFalse( has_filter( 'post_type_link', [ $search, 'get_post_type_permalink' ] ) ); + $this->assertNotFalse( has_filter( 'page_type_link', [ $search, 'get_post_type_permalink' ] ) ); + $this->assertNotFalse( has_filter( 'attachment_link', [ $search, 'get_post_type_permalink' ] ) ); + } + + /** + * Ensures register_hooks adds author data filters. + */ + public function test_register_hooks_adds_author_filters(): void { + $search = new Search(); + $search->register_hooks(); + + $this->assertNotFalse( has_filter( 'get_the_author_display_name', [ $search, 'get_post_author' ] ) ); + $this->assertNotFalse( has_filter( 'author_link', [ $search, 'get_post_author_link' ] ) ); + $this->assertNotFalse( has_filter( 'get_avatar_url', [ $search, 'get_post_author_avatar' ] ) ); + } + + /** + * Ensures register_hooks adds term/taxonomy filters. + */ + public function test_register_hooks_adds_term_filters(): void { + $search = new Search(); + $search->register_hooks(); + + $this->assertNotFalse( has_filter( 'term_link', [ $search, 'get_term_link' ] ) ); + $this->assertNotFalse( has_filter( 'category_link', [ $search, 'get_category_link' ] ) ); + $this->assertNotFalse( has_filter( 'tag_link', [ $search, 'get_tag_link' ] ) ); + $this->assertNotFalse( has_filter( 'get_the_terms', [ $search, 'get_post_terms' ] ) ); + $this->assertNotFalse( has_filter( 'wp_get_post_terms', [ $search, 'get_post_terms' ] ) ); + } + + /** + * Ensures register_hooks adds block render filter. + */ + public function test_register_hooks_adds_render_block_filter(): void { + $search = new Search(); + $search->register_hooks(); + + $this->assertNotFalse( has_filter( 'render_block', [ $search, 'filter_render_block' ] ) ); + } + + // ── get_algolia_results ───────────────────────────────────────────── + + /** + * Returns original posts when search is not enabled. + */ + public function test_get_algolia_results_returns_original_posts_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + delete_option( Settings::OPTION_SITE_TYPE ); + + $search = new Search(); + $query = new \WP_Query(); + $original = [ self::factory()->post->create_and_get() ]; + + $result = $search->get_algolia_results( $original, $query ); + + $this->assertSame( $original, $result ); + } + + /** + * Returns original posts when query is not a search query. + */ + public function test_get_algolia_results_returns_original_posts_for_non_search_query(): void { + $this->enable_search_for_governing_site(); + + $search = new Search(); + $query = new \WP_Query(); + $query->init(); + + $original = [ self::factory()->post->create_and_get() ]; + $result = $search->get_algolia_results( $original, $query ); + + $this->assertSame( $original, $result ); + } + + /** + * Returns original posts when the query is not a WP_Query instance. + */ + public function test_get_algolia_results_returns_original_posts_for_non_wp_query(): void { + $search = new Search(); + $original = [ self::factory()->post->create_and_get() ]; + + // @phpstan-ignore argument.type + $result = $search->get_algolia_results( $original, 'not-a-query' ); + + $this->assertSame( $original, $result ); + } + + // ── get_post_type_permalink ───────────────────────────────────────── + + /** + * Returns default permalink for local posts (positive ID). + */ + public function test_get_post_type_permalink_returns_default_for_local_post(): void { + $search = new Search(); + $post = self::factory()->post->create_and_get(); + + $result = $search->get_post_type_permalink( 'https://example.com/test/', $post ); + + $this->assertSame( 'https://example.com/test/', $result ); + } + + // ── get_post_author ───────────────────────────────────────────────── + + /** + * Returns default author name when search is not enabled. + */ + public function test_get_post_author_returns_default_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $result = $search->get_post_author( 'Default Author' ); + + $this->assertSame( 'Default Author', $result ); + } + + // ── get_post_author_link ──────────────────────────────────────────── + + /** + * Returns default author link when search is not enabled. + */ + public function test_get_post_author_link_returns_default_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $result = $search->get_post_author_link( 'https://example.com/author/admin/' ); + + $this->assertSame( 'https://example.com/author/admin/', $result ); + } + + // ── get_post_author_avatar ────────────────────────────────────────── + + /** + * Returns default avatar URL when search is not enabled. + */ + public function test_get_post_author_avatar_returns_default_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $result = $search->get_post_author_avatar( 'https://example.com/avatar.jpg' ); + + $this->assertSame( 'https://example.com/avatar.jpg', $result ); + } + + // ── get_term_link ─────────────────────────────────────────────────── + + /** + * Returns default term link when search is not enabled. + */ + public function test_get_term_link_returns_default_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $result = $search->get_term_link( 'https://example.com/category/news/', 1, 'category' ); + + $this->assertSame( 'https://example.com/category/news/', $result ); + } + + // ── get_category_link ─────────────────────────────────────────────── + + /** + * Delegates to get_term_link with 'category' taxonomy. + */ + public function test_get_category_link_returns_default_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $result = $search->get_category_link( 'https://example.com/category/tech/', 5 ); + + $this->assertSame( 'https://example.com/category/tech/', $result ); + } + + // ── get_tag_link ──────────────────────────────────────────────────── + + /** + * Delegates to get_term_link with 'post_tag' taxonomy. + */ + public function test_get_tag_link_returns_default_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $result = $search->get_tag_link( 'https://example.com/tag/php/', 3 ); + + $this->assertSame( 'https://example.com/tag/php/', $result ); + } + + // ── get_post_terms ────────────────────────────────────────────────── + + /** + * Returns original terms when search is not enabled. + */ + public function test_get_post_terms_returns_original_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $original = [ new \WP_Term( new \stdClass() ) ]; + + $result = $search->get_post_terms( $original, 1, 'category' ); + $this->assertSame( $original, $result ); + } + + // ── filter_render_block ───────────────────────────────────────────── + + /** + * Returns unchanged block content when search is not enabled. + */ + public function test_filter_render_block_returns_original_when_search_disabled(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $search = new Search(); + $content = '

Title

'; + $block = [ 'blockName' => 'core/post-title' ]; + + $result = $search->filter_render_block( $content, $block ); + + $this->assertSame( $content, $result ); + } + + /** + * Enables Algolia for the governing site in options. + */ + private function enable_search_for_governing_site(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + $site_url = Utils::normalize_url( get_site_url() ); + update_option( + Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, + [ + $site_url => [ + 'algolia_enabled' => true, + 'searchable_sites' => [ $site_url ], + ], + ] + ); + } +} From f7d866aff2cd539226ebe0a5e662f9116079b0bb Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 11:49:35 +0530 Subject: [PATCH 03/18] tests: improved tests for Search/Search module --- tests/php/Unit/Modules/Search/SearchTest.php | 169 +++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index aaff00e..81f98c2 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -26,6 +26,12 @@ final class SearchTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { + global $post, $wp_query, $wp_the_query; + + $post = null; + $wp_query = null; + $wp_the_query = null; + delete_option( Settings::OPTION_SITE_TYPE ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); @@ -158,6 +164,27 @@ public function test_get_post_type_permalink_returns_default_for_local_post(): v $this->assertSame( 'https://example.com/test/', $result ); } + /** + * Returns remote permalink for placeholder posts during enabled search. + */ + public function test_get_post_type_permalink_returns_remote_for_placeholder_post(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + global $wp_query; + + $remote_post = new \WP_Post( new \stdClass() ); + $remote_post->onesearch_original_id = 17; + $remote_post->guid = 'https://remote.example.com/posts/17/'; + + $wp_query->posts = [ $remote_post ]; + + $search = new Search(); + $result = $search->get_post_type_permalink( 'https://example.com/local/', -18 ); + + $this->assertSame( 'https://remote.example.com/posts/17/', $result ); + } + // ── get_post_author ───────────────────────────────────────────────── /** @@ -172,6 +199,25 @@ public function test_get_post_author_returns_default_when_search_disabled(): voi $this->assertSame( 'Default Author', $result ); } + /** + * Returns remote author name when search is enabled for remote posts. + */ + public function test_get_post_author_returns_remote_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + global $post; + + $post = new \WP_Post( new \stdClass() ); + $post->ID = -10; + $post->onesearch_remote_post_author_display_name = 'Remote Author'; + + $search = new Search(); + $result = $search->get_post_author( 'Default Author' ); + + $this->assertSame( 'Remote Author', $result ); + } + // ── get_post_author_link ──────────────────────────────────────────── /** @@ -186,6 +232,25 @@ public function test_get_post_author_link_returns_default_when_search_disabled() $this->assertSame( 'https://example.com/author/admin/', $result ); } + /** + * Returns remote author link when search is enabled for remote posts. + */ + public function test_get_post_author_link_returns_remote_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + global $post; + + $post = new \WP_Post( new \stdClass() ); + $post->ID = -11; + $post->onesearch_remote_post_author_link = 'https://remote.example.com/authors/john/'; + + $search = new Search(); + $result = $search->get_post_author_link( 'https://example.com/author/admin/' ); + + $this->assertSame( 'https://remote.example.com/authors/john/', $result ); + } + // ── get_post_author_avatar ────────────────────────────────────────── /** @@ -200,6 +265,25 @@ public function test_get_post_author_avatar_returns_default_when_search_disabled $this->assertSame( 'https://example.com/avatar.jpg', $result ); } + /** + * Returns remote author avatar when search is enabled for remote posts. + */ + public function test_get_post_author_avatar_returns_remote_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + global $post; + + $post = new \WP_Post( new \stdClass() ); + $post->ID = -12; + $post->onesearch_remote_post_author_gravatar = 'https://remote.example.com/avatar.jpg'; + + $search = new Search(); + $result = $search->get_post_author_avatar( 'https://example.com/avatar.jpg' ); + + $this->assertSame( 'https://remote.example.com/avatar.jpg', $result ); + } + // ── get_term_link ─────────────────────────────────────────────────── /** @@ -214,6 +298,32 @@ public function test_get_term_link_returns_default_when_search_disabled(): void $this->assertSame( 'https://example.com/category/news/', $result ); } + /** + * Returns remote term link when search is enabled and taxonomy data exists. + */ + public function test_get_term_link_returns_remote_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + global $post; + + $post = new \WP_Post( new \stdClass() ); + $post->ID = -13; + $post->onesearch_remote_taxonomies = [ + [ + 'taxonomy' => 'category', + 'term_id' => 7, + 'slug' => 'news', + 'term_link' => 'https://remote.example.com/category/news/', + ], + ]; + + $search = new Search(); + $result = $search->get_term_link( 'https://example.com/category/news/', 7, 'category' ); + + $this->assertSame( 'https://remote.example.com/category/news/', $result ); + } + // ── get_category_link ─────────────────────────────────────────────── /** @@ -274,6 +384,51 @@ public function test_filter_render_block_returns_original_when_search_disabled() $this->assertSame( $content, $result ); } + /** + * Rewrites title block link for remote posts when search is enabled. + */ + public function test_filter_render_block_rewrites_title_link_for_remote_post(): void { + $this->enable_search_for_governing_site(); + + global $post; + + $post = new \WP_Post( new \stdClass() ); + $post->ID = -14; + $post->guid = 'https://remote.example.com/post/14/'; + + $search = new Search(); + $content = '

Title

'; + $block = [ 'blockName' => 'core/post-title' ]; + + $result = $search->filter_render_block( $content, $block ); + + $this->assertStringContainsString( 'https://remote.example.com/post/14/', $result ); + $this->assertStringNotContainsString( 'https://example.com/old/', $result ); + } + + /** + * Rewrites excerpt block text for remote posts when search is enabled. + */ + public function test_filter_render_block_rewrites_excerpt_for_remote_post(): void { + $this->enable_search_for_governing_site(); + + global $post; + + $post = new \WP_Post( new \stdClass() ); + $post->ID = -15; + $post->guid = 'https://remote.example.com/post/15/'; + $post->post_excerpt = 'Remote excerpt body'; + + $search = new Search(); + $content = '

Old excerpt

'; + $block = [ 'blockName' => 'core/post-excerpt' ]; + + $result = $search->filter_render_block( $content, $block ); + + $this->assertStringContainsString( 'Remote excerpt body', $result ); + $this->assertStringNotContainsString( 'Old excerpt', $result ); + } + /** * Enables Algolia for the governing site in options. */ @@ -290,4 +445,18 @@ private function enable_search_for_governing_site(): void { ] ); } + + /** + * Prime the global main query as a frontend search query. + */ + private function prime_main_search_query( string $term ): void { + global $wp_query, $wp_the_query; + + set_current_screen( 'front' ); + + $wp_query = new \WP_Query(); + $wp_the_query = $wp_query; + + $wp_query->query( [ 's' => $term ] ); + } } From c089dfbcbcc134994d271336f2c645e52a499c51 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 12:15:52 +0530 Subject: [PATCH 04/18] fix: resolved phpcs errors --- tests/php/Unit/Modules/Search/SearchTest.php | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index 81f98c2..7e23793 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -26,11 +26,12 @@ final class SearchTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Resetting test state. global $post, $wp_query, $wp_the_query; - $post = null; - $wp_query = null; - $wp_the_query = null; + $post = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_the_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited delete_option( Settings::OPTION_SITE_TYPE ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); @@ -171,7 +172,7 @@ public function test_get_post_type_permalink_returns_remote_for_placeholder_post $this->enable_search_for_governing_site(); $this->prime_main_search_query( 'test query' ); - global $wp_query; + global $wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $remote_post = new \WP_Post( new \stdClass() ); $remote_post->onesearch_original_id = 17; @@ -272,9 +273,10 @@ public function test_get_post_author_avatar_returns_remote_when_search_enabled() $this->enable_search_for_governing_site(); $this->prime_main_search_query( 'test query' ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $post; - $post = new \WP_Post( new \stdClass() ); + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post->ID = -12; $post->onesearch_remote_post_author_gravatar = 'https://remote.example.com/avatar.jpg'; @@ -305,9 +307,10 @@ public function test_get_term_link_returns_remote_when_search_enabled(): void { $this->enable_search_for_governing_site(); $this->prime_main_search_query( 'test query' ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $post; - $post = new \WP_Post( new \stdClass() ); + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post->ID = -13; $post->onesearch_remote_taxonomies = [ [ @@ -390,9 +393,10 @@ public function test_filter_render_block_returns_original_when_search_disabled() public function test_filter_render_block_rewrites_title_link_for_remote_post(): void { $this->enable_search_for_governing_site(); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $post; - $post = new \WP_Post( new \stdClass() ); + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post->ID = -14; $post->guid = 'https://remote.example.com/post/14/'; @@ -412,9 +416,10 @@ public function test_filter_render_block_rewrites_title_link_for_remote_post(): public function test_filter_render_block_rewrites_excerpt_for_remote_post(): void { $this->enable_search_for_governing_site(); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $post; - $post = new \WP_Post( new \stdClass() ); + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post->ID = -15; $post->guid = 'https://remote.example.com/post/15/'; $post->post_excerpt = 'Remote excerpt body'; @@ -448,14 +453,17 @@ private function enable_search_for_governing_site(): void { /** * Prime the global main query as a frontend search query. + * + * @param string $term Search term. */ private function prime_main_search_query( string $term ): void { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $wp_query, $wp_the_query; set_current_screen( 'front' ); - $wp_query = new \WP_Query(); - $wp_the_query = $wp_query; + $wp_query = new \WP_Query(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_the_query = $wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_query->query( [ 's' => $term ] ); } From 1cbec4ab357979df6248d28a4c53bbbf02b38866 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 12:57:12 +0530 Subject: [PATCH 05/18] tests: improved tests for Search/Watcher module --- tests/php/Unit/Modules/Search/WatcherTest.php | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 tests/php/Unit/Modules/Search/WatcherTest.php diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php new file mode 100644 index 0000000..b2ee815 --- /dev/null +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -0,0 +1,204 @@ +register_hooks(); + + $this->assertNotFalse( has_action( 'transition_post_status', [ $watcher, 'on_post_transition' ] ) ); + } + + // ── on_post_transition ────────────────────────────────────────────── + + /** + * Skips when post is not a WP_Post instance. + */ + public function test_on_post_transition_skips_non_wp_post(): void { + $watcher = new Watcher(); + + // Should not throw or error. + // @phpstan-ignore argument.type + $watcher->on_post_transition( 'publish', 'draft', 'not-a-post' ); + + $this->assertTrue( true ); + } + + /** + * Skips when post type is not indexable (no entities configured). + */ + public function test_on_post_transition_skips_non_indexable_post_type(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + // Set indexable entities to only 'page', not 'post'. + update_option( + Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, + [ + 'entities' => [ + Utils::normalize_url( get_site_url() ) => [ 'page' ], + ], + ] + ); + + $post = self::factory()->post->create_and_get( [ 'post_type' => 'post' ] ); + $watcher = new Watcher(); + + // Should exit early without hitting Algolia since 'post' is not indexable. + $watcher->on_post_transition( 'publish', 'draft', $post ); + + $this->assertTrue( true ); + } + + /** + * Skips when no indexable entities are configured at all. + */ + public function test_on_post_transition_skips_when_no_entities_configured(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + delete_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); + + $post = self::factory()->post->create_and_get(); + $watcher = new Watcher(); + + // Should exit early without hitting Algolia since no indexable entities are configured. + $watcher->on_post_transition( 'publish', 'draft', $post ); + + $this->assertTrue( true ); + } + + /** + * Processes indexable post types (though Algolia call will fail without credentials, + * we verify it gets past the post_type guard). + */ + public function test_on_post_transition_processes_indexable_post_type(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + update_option( + Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, + [ + 'entities' => [ + Utils::normalize_url( get_site_url() ) => [ 'post' ], + ], + ] + ); + // No Algolia credentials → delete_by will return WP_Error, so on_post_transition + // will return early after the failed delete. This tests the flow gets past the + // post_type check. + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); + $watcher = new Watcher(); + + $watcher->on_post_transition( 'publish', 'draft', $post ); + + $this->assertTrue( true ); + } + + /** + * Skips reindexing when new status is not an allowed status (e.g., trashed). + * The delete still attempts (and fails without creds), but the reindex does not happen. + */ + public function test_on_post_transition_does_not_reindex_disallowed_status(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + update_option( + Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, + [ + 'entities' => [ + Utils::normalize_url( get_site_url() ) => [ 'post' ], + ], + ] + ); + + $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); + $watcher = new Watcher(); + + // Transitioning to 'trash' — should not reindex (only delete). + $watcher->on_post_transition( 'trash', 'publish', $post ); + + $this->assertTrue( true ); + } + + /** + * Consumer site attempts to fetch allowed post types from brand config. + */ + public function test_on_post_transition_consumer_site_checks_brand_config(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); + delete_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL ); + delete_transient( Governing_Data_Handler::TRANSIENT_KEY ); + + $post = self::factory()->post->create_and_get(); + $watcher = new Watcher(); + + // No parent URL → get_brand_config returns WP_Error → post type is not indexable. + $watcher->on_post_transition( 'publish', 'draft', $post ); + + $this->assertTrue( true ); + } + + /** + * Consumer site with cached brand config recognizes indexable post types. + */ + public function test_on_post_transition_consumer_with_cached_config(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); + update_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL, 'https://governing.example.com' ); + + $cached_config = [ + 'algolia_credentials' => [ + 'app_id' => 'test-app', + 'write_key' => 'test-key', + ], + 'search_settings' => [ + 'algolia_enabled' => true, + 'searchable_sites' => [], + ], + 'indexable_entities' => [ 'post' ], + 'available_sites' => [], + ]; + $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); + $method->invoke( null, $cached_config ); + + $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); + $watcher = new Watcher(); + + // 'post' is in indexable_entities, so it passes the guard. + // Algolia SDK will fail with test credentials, but the guard logic is tested. + $watcher->on_post_transition( 'publish', 'draft', $post ); + + $this->assertTrue( true ); + } +} From 3994c0f53bbe6efed6ae702fcd8c9415480b3947 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 13:48:05 +0530 Subject: [PATCH 06/18] tests: improved tests for Search/Settings module --- .../php/Unit/Modules/Search/SettingsTest.php | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 tests/php/Unit/Modules/Search/SettingsTest.php diff --git a/tests/php/Unit/Modules/Search/SettingsTest.php b/tests/php/Unit/Modules/Search/SettingsTest.php new file mode 100644 index 0000000..78b501d --- /dev/null +++ b/tests/php/Unit/Modules/Search/SettingsTest.php @@ -0,0 +1,404 @@ +register_hooks(); + + $this->assertNotFalse( has_action( 'admin_init', [ $settings, 'register_settings' ] ) ); + $this->assertNotFalse( has_action( 'rest_api_init', [ $settings, 'register_settings' ] ) ); + } + + /** + * Ensures register_hooks listens to site_type, shared_sites option updates. + */ + public function test_register_hooks_listens_to_site_type_changes(): void { + $settings = new Search_Settings(); + $settings->register_hooks(); + + $this->assertNotFalse( + has_action( 'update_option_' . Settings::OPTION_SITE_TYPE, [ $settings, 'on_site_type_change' ] ) + ); + $this->assertNotFalse( + has_action( 'update_option_' . Settings::OPTION_GOVERNING_SHARED_SITES, [ $settings, 'on_shared_sites_change' ] ) + ); + } + + /** + * Ensures register_hooks sets up cache purge hooks. + */ + public function test_register_hooks_sets_up_cache_purge_actions(): void { + $settings = new Search_Settings(); + $settings->register_hooks(); + + $this->assertNotFalse( + has_action( 'update_option_' . Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS, [ $settings, 'purge_cache_on_update' ] ) + ); + $this->assertNotFalse( + has_action( 'update_option_' . Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, [ $settings, 'purge_cache_on_update' ] ) + ); + $this->assertNotFalse( + has_action( 'update_option_' . Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, [ $settings, 'purge_cache_on_update' ] ) + ); + } + + // ── register_settings ─────────────────────────────────────────────── + + /** + * Ensures register_settings registers governing settings when governing. + */ + public function test_register_settings_registers_governing_options(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + + $settings = new Search_Settings(); + $settings->register_settings(); + + $registered = get_registered_settings(); + + $this->assertArrayHasKey( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS, $registered ); + $this->assertArrayHasKey( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, $registered ); + $this->assertArrayHasKey( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, $registered ); + } + + /** + * Ensures register_settings sanitizes algolia credentials payload. + */ + public function test_register_settings_sanitizes_algolia_credentials(): void { + $settings = new Search_Settings(); + $settings->register_settings(); + + $registered = get_registered_settings(); + $sanitize = $registered[ Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ]['sanitize_callback'] ?? null; + + $this->assertIsCallable( $sanitize ); + + $sanitized = $sanitize( + [ + 'app_id' => ' app-id ', + 'write_key' => "\nwrite-key\t", + ] + ); + + $this->assertSame( 'app-id', $sanitized['app_id'] ); + $this->assertSame( 'write-key', $sanitized['write_key'] ); + } + + /** + * Ensures register_settings sanitizes search settings payload and normalizes keys. + */ + public function test_register_settings_sanitizes_search_settings_payload(): void { + $settings = new Search_Settings(); + $settings->register_settings(); + + $registered = get_registered_settings(); + $sanitize = $registered[ Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ]['sanitize_callback'] ?? null; + + $this->assertIsCallable( $sanitize ); + + $sanitized = $sanitize( + [ + ' https://example.com/site ' => [ + 'algolia_enabled' => 1, + 'searchable_sites' => [ ' https://child.example.com/x ' ], + ], + 'https://example.com/invalid' => 'not-array', + ] + ); + + $this->assertArrayHasKey( 'https://example.com/site/', $sanitized ); + $this->assertTrue( $sanitized['https://example.com/site/']['algolia_enabled'] ); + $this->assertSame( [ 'https://child.example.com/x' ], $sanitized['https://example.com/site/']['searchable_sites'] ); + + $this->assertArrayHasKey( 'https://example.com/invalid/', $sanitized ); + $this->assertFalse( $sanitized['https://example.com/invalid/']['algolia_enabled'] ); + $this->assertSame( [], $sanitized['https://example.com/invalid/']['searchable_sites'] ); + } + + // ── get_algolia_credentials ───────────────────────────────────────── + + /** + * Returns null values when no credentials are stored. + */ + public function test_get_algolia_credentials_returns_nulls_when_empty(): void { + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $creds = Search_Settings::get_algolia_credentials(); + + $this->assertNull( $creds['app_id'] ); + $this->assertNull( $creds['write_key'] ); + } + + /** + * Returns stored credentials after setting them. + */ + public function test_get_algolia_credentials_returns_stored_values(): void { + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'my-app', + 'write_key' => 'my-key', + ] + ); + + $creds = Search_Settings::get_algolia_credentials(); + + $this->assertSame( 'my-app', $creds['app_id'] ); + $this->assertSame( 'my-key', $creds['write_key'] ); + } + + // ── set_algolia_credentials ───────────────────────────────────────── + + /** + * Returns true on successful save. + */ + public function test_set_algolia_credentials_returns_true_on_success(): void { + $result = Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'app', + 'write_key' => 'key', + ] + ); + + $this->assertTrue( $result ); + } + + /** + * Returns false when value is not an array. + */ + public function test_set_algolia_credentials_returns_false_for_non_array(): void { + // @phpstan-ignore argument.type + $result = Search_Settings::set_algolia_credentials( 'invalid' ); + + $this->assertFalse( $result ); + } + + /** + * Stores null when app_id is missing. + */ + public function test_set_algolia_credentials_stores_null_for_missing_app_id(): void { + Search_Settings::set_algolia_credentials( [ 'write_key' => 'a-key' ] ); + + $creds = Search_Settings::get_algolia_credentials(); + + $this->assertNull( $creds['app_id'] ); + } + + // ── get_indexable_entities ─────────────────────────────────────────── + + /** + * Returns empty array when nothing is stored. + */ + public function test_get_indexable_entities_returns_empty_when_unset(): void { + delete_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); + + $this->assertSame( [], Search_Settings::get_indexable_entities() ); + } + + /** + * Returns stored value. + */ + public function test_get_indexable_entities_returns_stored_value(): void { + $entities = [ + 'entities' => [ + 'https://example.com/' => [ 'post', 'page' ], + ], + ]; + update_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, $entities ); + + $this->assertSame( $entities, Search_Settings::get_indexable_entities() ); + } + + // ── get_search_settings ───────────────────────────────────────────── + + /** + * Returns empty array when nothing is stored. + */ + public function test_get_search_settings_returns_empty_when_unset(): void { + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $this->assertSame( [], Search_Settings::get_search_settings() ); + } + + /** + * Returns stored search settings. + */ + public function test_get_search_settings_returns_stored_value(): void { + $value = [ + 'https://example.com/' => [ + 'algolia_enabled' => true, + 'searchable_sites' => [ 'https://child.example.com/' ], + ], + ]; + update_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, $value ); + + $this->assertSame( $value, Search_Settings::get_search_settings() ); + } + + // ── on_site_type_change ───────────────────────────────────────────── + + /** + * Does nothing when new value is not consumer. + */ + public function test_on_site_type_change_skips_non_consumer(): void { + $settings = new Search_Settings(); + + // Should not throw or error out. + $settings->on_site_type_change( '', Settings::SITE_TYPE_GOVERNING ); + + $this->assertTrue( true ); + } + + // ── on_shared_sites_change ────────────────────────────────────────── + + /** + * Does nothing when old value is empty. + */ + public function test_on_shared_sites_change_skips_empty_old_value(): void { + $settings = new Search_Settings(); + + $settings->on_shared_sites_change( [], [ [ 'url' => 'https://new.example.com' ] ] ); + + $this->assertTrue( true ); + } + + /** + * Does nothing when old value is not an array. + */ + public function test_on_shared_sites_change_skips_non_array_old_value(): void { + $settings = new Search_Settings(); + + $settings->on_shared_sites_change( 'not-array', [ [ 'url' => 'https://new.example.com' ] ] ); + + $this->assertTrue( true ); + } + + /** + * Does nothing when no sites have been removed. + */ + public function test_on_shared_sites_change_skips_when_no_sites_removed(): void { + $sites = [ [ 'url' => 'https://child.example.com' ] ]; + $settings = new Search_Settings(); + + $settings->on_shared_sites_change( $sites, $sites ); + + $this->assertTrue( true ); + } + + /** + * Keeps indexable entities unchanged when site removal delete fails. + */ + public function test_on_shared_sites_change_keeps_entities_when_delete_fails(): void { + $settings = new Search_Settings(); + + $initial_entities = [ + 'entities' => [ + 'post' => [ + 'https://child-a.example.com/', + 'https://child-b.example.com/', + ], + ], + ]; + + update_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, $initial_entities ); + delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $old_sites = [ + [ 'url' => 'https://child-a.example.com/' ], + [ 'url' => 'https://child-b.example.com/' ], + ]; + $new_sites = [ + [ 'url' => 'https://child-b.example.com/' ], + ]; + + $settings->on_shared_sites_change( $old_sites, $new_sites ); + + $this->assertSame( $initial_entities, get_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ) ); + } + + // ── purge_cache_on_update ─────────────────────────────────────────── + + /** + * Purges cache when algolia credentials option updates. + */ + public function test_purge_cache_on_update_clears_cache_for_credentials(): void { + set_transient( Governing_Data_Handler::TRANSIENT_KEY, [ 'test' => true ], 3600 ); + + $settings = new Search_Settings(); + $settings->purge_cache_on_update( [], [], Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); + + $this->assertFalse( get_transient( Governing_Data_Handler::TRANSIENT_KEY ) ); + } + + /** + * Purges cache when indexable sites option updates. + */ + public function test_purge_cache_on_update_clears_cache_for_indexable_sites(): void { + set_transient( Governing_Data_Handler::TRANSIENT_KEY, [ 'test' => true ], 3600 ); + + $settings = new Search_Settings(); + $settings->purge_cache_on_update( [], [], Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); + + $this->assertFalse( get_transient( Governing_Data_Handler::TRANSIENT_KEY ) ); + } + + /** + * Purges cache when search settings option updates. + */ + public function test_purge_cache_on_update_clears_cache_for_search_settings(): void { + set_transient( Governing_Data_Handler::TRANSIENT_KEY, [ 'test' => true ], 3600 ); + + $settings = new Search_Settings(); + $settings->purge_cache_on_update( [], [], Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); + + $this->assertFalse( get_transient( Governing_Data_Handler::TRANSIENT_KEY ) ); + } + + /** + * Does not purge cache for unknown option. + */ + public function test_purge_cache_on_update_ignores_unknown_option(): void { + set_transient( Governing_Data_Handler::TRANSIENT_KEY, [ 'test' => true ], 3600 ); + + $settings = new Search_Settings(); + $settings->purge_cache_on_update( [], [], 'some_unrelated_option' ); + + $this->assertNotFalse( get_transient( Governing_Data_Handler::TRANSIENT_KEY ) ); + } +} From 861d9c3e332006804ec1a33811cf6ed9e3e3970c Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 13:58:44 +0530 Subject: [PATCH 07/18] fix: resolved PHPCS errors --- tests/php/Unit/Modules/Search/SearchTest.php | 16 +++++++++------- tests/php/Unit/Modules/Search/WatcherTest.php | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index 7e23793..aed2360 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -26,12 +26,12 @@ final class SearchTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Resetting test state. + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable -- Resetting test state. global $post, $wp_query, $wp_the_query; $post = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wp_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wp_the_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable + $wp_the_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable delete_option( Settings::OPTION_SITE_TYPE ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); @@ -207,9 +207,10 @@ public function test_get_post_author_returns_remote_when_search_enabled(): void $this->enable_search_for_governing_site(); $this->prime_main_search_query( 'test query' ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $post; - $post = new \WP_Post( new \stdClass() ); + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post->ID = -10; $post->onesearch_remote_post_author_display_name = 'Remote Author'; @@ -240,9 +241,10 @@ public function test_get_post_author_link_returns_remote_when_search_enabled(): $this->enable_search_for_governing_site(); $this->prime_main_search_query( 'test query' ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. global $post; - $post = new \WP_Post( new \stdClass() ); + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post->ID = -11; $post->onesearch_remote_post_author_link = 'https://remote.example.com/authors/john/'; @@ -457,13 +459,13 @@ private function enable_search_for_governing_site(): void { * @param string $term Search term. */ private function prime_main_search_query( string $term ): void { - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable -- Setting up test global state. global $wp_query, $wp_the_query; set_current_screen( 'front' ); $wp_query = new \WP_Query(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wp_the_query = $wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_the_query = $wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable $wp_query->query( [ 's' => $term ] ); } diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index b2ee815..dd12087 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -55,8 +55,8 @@ public function test_register_hooks_adds_transition_post_status_action(): void { public function test_on_post_transition_skips_non_wp_post(): void { $watcher = new Watcher(); - // Should not throw or error. - // @phpstan-ignore argument.type + // Should not throw or error out. + // @phpstan-ignore argument.type -- Non-WP_Post passed intentionally. $watcher->on_post_transition( 'publish', 'draft', 'not-a-post' ); $this->assertTrue( true ); From 37c3beec65d2433209fde5f55ec21ad43fb07651 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 8 May 2026 16:41:32 +0530 Subject: [PATCH 08/18] tests: enhance error handling tests for Index and Search modules --- tests/php/Unit/Modules/Search/IndexTest.php | 13 ++++ tests/php/Unit/Modules/Search/SearchTest.php | 73 +++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index de2e4a9..83d9193 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -36,12 +36,15 @@ protected function tearDown(): void { * Returns WP_Error when Algolia credentials are missing. */ public function test_get_index_returns_error_without_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $index = new Index(); $result = $index->get_index(); $this->assertWPError( $result ); + $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); + $this->assertSame( 'Algolia admin credentials missing.', $result->get_error_message() ); } /** @@ -87,11 +90,13 @@ public function test_get_index_returns_same_instance_on_second_call(): void { * Returns WP_Error when credentials are missing. */ public function test_delete_index_returns_error_without_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $result = ( new Index() )->delete_index(); $this->assertWPError( $result ); + $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } // ── delete_by ─────────────────────────────────────────────────────── @@ -100,11 +105,13 @@ public function test_delete_index_returns_error_without_credentials(): void { * Returns WP_Error when credentials are missing. */ public function test_delete_by_returns_error_without_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $result = ( new Index() )->delete_by( [ 'filters' => 'site_url:"http://test.com"' ] ); $this->assertWPError( $result ); + $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } // ── save_records ──────────────────────────────────────────────────── @@ -113,11 +120,13 @@ public function test_delete_by_returns_error_without_credentials(): void { * Returns WP_Error when credentials are missing. */ public function test_save_records_returns_error_without_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $result = ( new Index() )->save_records( [] ); $this->assertWPError( $result ); + $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } // ── search ────────────────────────────────────────────────────────── @@ -126,11 +135,13 @@ public function test_save_records_returns_error_without_credentials(): void { * Returns WP_Error when credentials are missing. */ public function test_search_returns_error_without_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $result = ( new Index() )->search( 'test query' ); $this->assertWPError( $result ); + $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } // ── index_all_posts ───────────────────────────────────────────────── @@ -139,10 +150,12 @@ public function test_search_returns_error_without_credentials(): void { * Returns WP_Error when delete_by fails (no credentials). */ public function test_index_all_posts_returns_error_when_delete_fails(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $result = ( new Index() )->index_all_posts( [ 'post' ] ); $this->assertWPError( $result ); + $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } } diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index aed2360..a07897a 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -190,8 +190,24 @@ public function test_get_post_type_permalink_returns_remote_for_placeholder_post /** * Returns default author name when search is not enabled. + * + * Primes $wp_query and sets a remote $post so that should_filter_query() and + * the negative-ID guard both pass — ensuring the default is returned solely + * because search is disabled, not because of a missing query or local post. */ public function test_get_post_author_returns_default_when_search_disabled(): void { + // Set up everything search needs EXCEPT the search settings option itself. + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + $this->prime_main_search_query( 'test query' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -99; + $post->onesearch_remote_post_author_display_name = 'Remote Author'; + + // Now disable search — this is the sole reason the default should be returned. delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); $search = new Search(); @@ -224,8 +240,20 @@ public function test_get_post_author_returns_remote_when_search_enabled(): void /** * Returns default author link when search is not enabled. + * + * Primes query and remote $post so the only exit condition is search being disabled. */ public function test_get_post_author_link_returns_default_when_search_disabled(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + $this->prime_main_search_query( 'test query' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -99; + $post->onesearch_remote_post_author_link = 'https://remote.example.com/authors/john/'; + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); $search = new Search(); @@ -258,8 +286,20 @@ public function test_get_post_author_link_returns_remote_when_search_enabled(): /** * Returns default avatar URL when search is not enabled. + * + * Primes query and remote $post so the only exit condition is search being disabled. */ public function test_get_post_author_avatar_returns_default_when_search_disabled(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + $this->prime_main_search_query( 'test query' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -99; + $post->onesearch_remote_post_author_gravatar = 'https://remote.example.com/avatar.jpg'; + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); $search = new Search(); @@ -292,12 +332,31 @@ public function test_get_post_author_avatar_returns_remote_when_search_enabled() /** * Returns default term link when search is not enabled. + * + * Primes query and remote $post so the only exit condition is search being disabled. */ public function test_get_term_link_returns_default_when_search_disabled(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + $this->prime_main_search_query( 'test query' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -99; + $post->onesearch_remote_taxonomies = [ + [ + 'taxonomy' => 'category', + 'term_id' => 7, + 'slug' => 'news', + 'term_link' => 'https://remote.example.com/category/news/', + ], + ]; + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); $search = new Search(); - $result = $search->get_term_link( 'https://example.com/category/news/', 1, 'category' ); + $result = $search->get_term_link( 'https://example.com/category/news/', 7, 'category' ); $this->assertSame( 'https://example.com/category/news/', $result ); } @@ -376,8 +435,20 @@ public function test_get_post_terms_returns_original_when_search_disabled(): voi /** * Returns unchanged block content when search is not enabled. + * + * Sets a negative-ID $post with a guid so the only exit condition is + * search being disabled, not a missing/local post. */ public function test_filter_render_block_returns_original_when_search_disabled(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -99; + $post->guid = 'https://remote.example.com/post/99/'; + delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); $search = new Search(); From 424dce4b45e971ee3af7a1d9489c138e0b885f1e Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Mon, 11 May 2026 11:33:38 +0530 Subject: [PATCH 09/18] tests: enhance WatcherTest to intercept Algolia SDK requests and validate reindexing behavior --- tests/php/Unit/Modules/Search/WatcherTest.php | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index dd12087..d0e7fa1 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -15,6 +15,9 @@ use OneSearch\Modules\Settings\Settings; use OneSearch\Tests\TestCase; use OneSearch\Utils; +use OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia as AlgoliaSDK; +use OneSearch\Vendor\Psr\Http\Message\RequestInterface; +use OneSearch\Vendor\Psr\Http\Message\ResponseInterface; use PHPUnit\Framework\Attributes\CoversClass; /** @@ -26,6 +29,8 @@ final class WatcherTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { + AlgoliaSDK::resetHttpClient(); + delete_option( Settings::OPTION_SITE_TYPE ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); delete_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); @@ -131,26 +136,64 @@ public function test_on_post_transition_processes_indexable_post_type(): void { /** * Skips reindexing when new status is not an allowed status (e.g., trashed). - * The delete still attempts (and fails without creds), but the reindex does not happen. + * + * Injects a fake Algolia HTTP client to intercept SDK-level requests (the SDK + * does not use wp_remote_*, so pre_http_request cannot be used here). After + * transitioning to 'trash', only the deleteBy call should have been made — no + * /batch (saveObjects) request should appear. */ public function test_on_post_transition_does_not_reindex_disallowed_status(): void { - update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); - update_option( - Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, - [ - 'entities' => [ - Utils::normalize_url( get_site_url() ) => [ 'post' ], - ], - ] + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); + update_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL, 'https://governing.example.com' ); + + $cached_config = [ + 'algolia_credentials' => [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ], + 'search_settings' => [ + 'algolia_enabled' => true, + 'searchable_sites' => [], + ], + 'indexable_entities' => [ 'post' ], + 'available_sites' => [], + ]; + $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); + $method->invoke( null, $cached_config ); + + // Intercept every Algolia SDK HTTP call and record the request paths. + $recorded_paths = []; + AlgoliaSDK::setHttpClient( + new class( $recorded_paths ) implements \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\HttpClientInterface { + /** @param list $paths */ + public function __construct( private array &$paths ) {} + + public function sendRequest( RequestInterface $request, $timeout, $connectTimeout ): ResponseInterface { + $path = (string) $request->getUri()->getPath(); + $this->paths[] = $path; + + // getTask polling → mark published so wait() completes immediately. + // All other requests (deleteBy) → return a taskID. + $body = str_contains( $path, '/task/' ) + ? '{"status":"published","pendingTask":false}' + : '{"taskID":1,"updatedAt":"2024-01-01T00:00:00.000Z"}'; + + // @phpstan-ignore return.type + return new \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\Psr7\Response( 200, [], $body ); + } + } ); $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); $watcher = new Watcher(); - // Transitioning to 'trash' — should not reindex (only delete). + // Transitioning to 'trash' should delete from the index but must not reindex. $watcher->on_post_transition( 'trash', 'publish', $post ); - $this->assertTrue( true ); + // deleteBy was called (some path was recorded), but no /batch (saveObjects) call + // should have been made — the status guard must have stopped reindexing. + $batch_calls = array_filter( $recorded_paths, static fn ( $p ) => str_contains( $p, '/batch' ) ); + $this->assertEmpty( $batch_calls, 'Trash transition should perform delete only and must not reindex.' ); } /** From 9a6730cdf7b865eafb022044d4354f5c91a01317 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Mon, 11 May 2026 11:49:05 +0530 Subject: [PATCH 10/18] fix: resolved PHPCS errors --- tests/php/Unit/Modules/Search/WatcherTest.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index d0e7fa1..ee432d0 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -165,10 +165,24 @@ public function test_on_post_transition_does_not_reindex_disallowed_status(): vo $recorded_paths = []; AlgoliaSDK::setHttpClient( new class( $recorded_paths ) implements \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\HttpClientInterface { - /** @param list $paths */ - public function __construct( private array &$paths ) {} + /** @var array */ + private array $paths; + + /** + * @param array $paths Reference to the array that records intercepted request paths. + */ + public function __construct( array &$paths ) { + $this->paths = &$paths; + } - public function sendRequest( RequestInterface $request, $timeout, $connectTimeout ): ResponseInterface { + /** + * {@inheritDoc} + * + * @param \OneSearch\Vendor\Psr\Http\Message\RequestInterface $request The PSR-7 request. + * @param mixed $timeout Request timeout. + * @param mixed $connect_timeout Connection timeout. + */ + public function sendRequest( RequestInterface $request, mixed $timeout, mixed $connect_timeout ): ResponseInterface { // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter $path = (string) $request->getUri()->getPath(); $this->paths[] = $path; From 89562cbf7a134c688f505dfe79fa02f025b207ed Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Tue, 12 May 2026 17:26:02 +0530 Subject: [PATCH 11/18] tests: enhance Algolia SDK request interception and add new tests for Index and Search modules --- tests/php/TestCase.php | 52 ++++++ tests/php/Unit/Modules/Search/IndexTest.php | 119 ++++++++++-- tests/php/Unit/Modules/Search/SearchTest.php | 170 +++++++++++++++++- tests/php/Unit/Modules/Search/WatcherTest.php | 89 +++------ 4 files changed, 352 insertions(+), 78 deletions(-) diff --git a/tests/php/TestCase.php b/tests/php/TestCase.php index b8f036d..7bb01d5 100644 --- a/tests/php/TestCase.php +++ b/tests/php/TestCase.php @@ -81,5 +81,57 @@ protected function checkRequirements(): void { // phpcs:ignore Generic.CodeAnaly parent::checkRequirements(); } + /** + * Intercept Algolia SDK HTTP calls and collect request paths. + * + * @param array $recorded_paths Paths captured from outgoing SDK requests. + * @param (callable(string): string)|null $body_for_path Optional callback to provide a response body for a given request path. + */ + public function mock_algolia_http_client( array &$recorded_paths, ?callable $body_for_path = null ): void { + \OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia::setHttpClient( + new class( $recorded_paths, $body_for_path ) implements \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\HttpClientInterface { + /** @var array */ + private array $paths; + + /** @var (callable(string): string)|null */ + private $body_for_path; + + /** + * @param array $paths Reference to the array that records intercepted request paths. + * @param (callable(string): string)|null $body_for_path Optional callback to generate mock response bodies. + */ + public function __construct( array &$paths, ?callable $body_for_path ) { + $this->paths = &$paths; + $this->body_for_path = $body_for_path; + } + + /** + * {@inheritDoc} + * + * @param \OneSearch\Vendor\Psr\Http\Message\RequestInterface $request The PSR-7 request. + * @param mixed $timeout Request timeout. + * @param mixed $connect_timeout Connection timeout. + */ + public function sendRequest( \OneSearch\Vendor\Psr\Http\Message\RequestInterface $request, mixed $timeout, mixed $connect_timeout ): \OneSearch\Vendor\Psr\Http\Message\ResponseInterface { // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter + $path = (string) $request->getUri()->getPath(); + $this->paths[] = $path; + + if ( null !== $this->body_for_path ) { + $body = (string) call_user_func( $this->body_for_path, $path ); + } elseif ( str_contains( $path, '/task/' ) ) { + $body = '{"status":"published","pendingTask":false}'; + } elseif ( str_contains( $path, '/query' ) ) { + $body = '{"hits":[{"objectID":"1"}],"nbHits":1,"page":0,"hitsPerPage":20}'; + } else { + $body = '{"taskID":1,"updatedAt":"2024-01-01T00:00:00.000Z"}'; + } + + // @phpstan-ignore return.type + return new \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\Psr7\Response( 200, [], $body ); + } + } + ); + } + // Add any common setup or utility methods for tests here. } diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index 83d9193..4752710 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -13,6 +13,7 @@ use OneSearch\Modules\Search\Settings as Search_Settings; use OneSearch\Modules\Settings\Settings; use OneSearch\Tests\TestCase; +use OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia as AlgoliaSDK; use PHPUnit\Framework\Attributes\CoversClass; /** @@ -24,6 +25,8 @@ final class IndexTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { + AlgoliaSDK::resetHttpClient(); + delete_option( Settings::OPTION_SITE_TYPE ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); @@ -51,13 +54,7 @@ public function test_get_index_returns_error_without_credentials(): void { * Returns SearchIndex when credentials are valid. */ public function test_get_index_returns_search_index_with_valid_credentials(): void { - update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); - Search_Settings::set_algolia_credentials( - [ - 'app_id' => 'TEST_APP', - 'write_key' => 'TEST_KEY', - ] - ); + self::set_governing_credentials(); $index = new Index(); $result = $index->get_index(); @@ -69,13 +66,7 @@ public function test_get_index_returns_search_index_with_valid_credentials(): vo * Caches the index instance on subsequent calls. */ public function test_get_index_returns_same_instance_on_second_call(): void { - update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); - Search_Settings::set_algolia_credentials( - [ - 'app_id' => 'TEST_APP', - 'write_key' => 'TEST_KEY', - ] - ); + self::set_governing_credentials(); $index = new Index(); $first = $index->get_index(); @@ -99,6 +90,21 @@ public function test_delete_index_returns_error_without_credentials(): void { $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } + /** + * Returns true for delete_index with valid credentials. + */ + public function test_delete_index_returns_true_with_valid_credentials(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $result = ( new Index() )->delete_index(); + + $this->assertTrue( $result ); + $this->assertNotEmpty( $recorded_paths ); + } + // ── delete_by ─────────────────────────────────────────────────────── /** @@ -114,6 +120,21 @@ public function test_delete_by_returns_error_without_credentials(): void { $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } + /** + * Returns true for delete_by with valid credentials. + */ + public function test_delete_by_returns_true_with_valid_credentials(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $result = ( new Index() )->delete_by( [ 'filters' => 'site_url:"http://test.com"' ] ); + + $this->assertTrue( $result ); + $this->assertNotEmpty( $recorded_paths ); + } + // ── save_records ──────────────────────────────────────────────────── /** @@ -129,6 +150,28 @@ public function test_save_records_returns_error_without_credentials(): void { $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } + /** + * Returns true for save_records with valid credentials. + */ + public function test_save_records_returns_true_with_valid_credentials(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $result = ( new Index() )->save_records( + [ + [ + 'objectID' => '1', + 'post_title' => 'Test', + ], + ] + ); + + $this->assertTrue( $result ); + $this->assertNotEmpty( $recorded_paths ); + } + // ── search ────────────────────────────────────────────────────────── /** @@ -144,6 +187,24 @@ public function test_search_returns_error_without_credentials(): void { $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } + /** + * Returns search payload for search with valid credentials. + */ + public function test_search_returns_results_with_valid_credentials(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + + $this->mock_algolia_http_client( $recorded_paths ); + + $result = ( new Index() )->search( 'test query' ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'hits', $result ); + $this->assertSame( '1', $result['hits'][0]['objectID'] ?? '' ); + $this->assertNotEmpty( $recorded_paths ); + } + // ── index_all_posts ───────────────────────────────────────────────── /** @@ -158,4 +219,34 @@ public function test_index_all_posts_returns_error_when_delete_fails(): void { $this->assertWPError( $result ); $this->assertSame( 'algolia_credentials_missing', $result->get_error_code() ); } + + /** + * Returns true when index_all_posts is called with no post types and valid credentials. + */ + public function test_index_all_posts_returns_true_with_valid_credentials_and_no_post_types(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $result = ( new Index() )->index_all_posts( [] ); + + $this->assertTrue( $result ); + $this->assertNotEmpty( $recorded_paths ); + } + + // ── helpers ──────────────────────────────────────────────────────── + + /** + * Set governing-site context with valid Algolia credentials. + */ + private function set_governing_credentials(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ] + ); + } } diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index a07897a..776b528 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -15,6 +15,7 @@ use OneSearch\Modules\Settings\Settings; use OneSearch\Tests\TestCase; use OneSearch\Utils; +use OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia as AlgoliaSDK; use PHPUnit\Framework\Attributes\CoversClass; /** @@ -33,6 +34,8 @@ protected function tearDown(): void { $wp_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable $wp_the_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable + AlgoliaSDK::resetHttpClient(); + delete_option( Settings::OPTION_SITE_TYPE ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); @@ -151,6 +154,72 @@ public function test_get_algolia_results_returns_original_posts_for_non_wp_query $this->assertSame( $original, $result ); } + /** + * Returns mapped remote posts when search is enabled and query is a main search query. + */ + public function test_get_algolia_results_returns_remote_posts_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ] + ); + + $recorded_paths = []; + $this->mock_algolia_http_client( + $recorded_paths, + static function ( string $path ): string { + if ( str_contains( $path, '/query' ) ) { + return wp_json_encode( + [ + 'hits' => [ + [ + 'objectID' => '17', + 'site_post_id' => '17', + 'post_id' => 17, + 'post_title' => 'Remote Post', + 'post_type' => 'post', + 'permalink' => 'https://remote.example.com/posts/17/', + 'site_url' => 'https://remote.example.com/', + 'site_name' => 'Remote', + 'total_chunks' => 1, + 'post_date_gmt' => 1710000000, + 'post_modified_gmt' => 1710000000, + ], + ], + 'nbHits' => 1, + 'page' => 0, + 'hitsPerPage' => 10, + ] + ) ?: '{}'; + } + + if ( str_contains( $path, '/task/' ) ) { + return '{"status":"published","pendingTask":false}'; + } + + return '{"taskID":1,"updatedAt":"2024-01-01T00:00:00.000Z"}'; + } + ); + + $this->prime_main_search_query( 'remote test' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reading query prepared by helper. + global $wp_query; + + $search = new Search(); + $result = $search->get_algolia_results( [], $wp_query ); + + $this->assertNotEmpty( $result ); + $this->assertInstanceOf( \WP_Post::class, $result[0] ); + $this->assertSame( -18, $result[0]->ID ); + $this->assertSame( 'https://remote.example.com/posts/17/', $result[0]->guid ); + $this->assertSame( 1, $wp_query->post_count ); + $this->assertTrue( (bool) ( $wp_query->is_algolia_search ?? false ) ); + $this->assertNotEmpty( $recorded_paths ); + } + // ── get_post_type_permalink ───────────────────────────────────────── /** @@ -402,6 +471,33 @@ public function test_get_category_link_returns_default_when_search_disabled(): v $this->assertSame( 'https://example.com/category/tech/', $result ); } + /** + * Delegates to get_term_link with 'category' taxonomy for enabled remote search. + */ + public function test_get_category_link_returns_remote_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -105; + $post->onesearch_remote_taxonomies = [ + [ + 'taxonomy' => 'category', + 'term_id' => 5, + 'slug' => 'tech', + 'term_link' => 'https://remote.example.com/category/tech/', + ], + ]; + + $search = new Search(); + $result = $search->get_category_link( 'https://example.com/category/tech/', 5 ); + + $this->assertSame( 'https://remote.example.com/category/tech/', $result ); + } + // ── get_tag_link ──────────────────────────────────────────────────── /** @@ -416,6 +512,33 @@ public function test_get_tag_link_returns_default_when_search_disabled(): void { $this->assertSame( 'https://example.com/tag/php/', $result ); } + /** + * Delegates to get_term_link with 'post_tag' taxonomy for enabled remote search. + */ + public function test_get_tag_link_returns_remote_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Setting up test global state. + global $post; + + $post = new \WP_Post( new \stdClass() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->ID = -106; + $post->onesearch_remote_taxonomies = [ + [ + 'taxonomy' => 'post_tag', + 'term_id' => 3, + 'slug' => 'php', + 'term_link' => 'https://remote.example.com/tag/php/', + ], + ]; + + $search = new Search(); + $result = $search->get_tag_link( 'https://example.com/tag/php/', 3 ); + + $this->assertSame( 'https://remote.example.com/tag/php/', $result ); + } + // ── get_post_terms ────────────────────────────────────────────────── /** @@ -431,6 +554,44 @@ public function test_get_post_terms_returns_original_when_search_disabled(): voi $this->assertSame( $original, $result ); } + /** + * Returns mapped terms for enabled search when remote taxonomy metadata exists. + */ + public function test_get_post_terms_returns_remote_terms_when_search_enabled(): void { + $this->enable_search_for_governing_site(); + $this->prime_main_search_query( 'test query' ); + + $post_id = self::factory()->post->create(); + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + + $post->onesearch_remote_taxonomies = [ + [ + 'taxonomy' => 'category', + 'term_id' => 22, + 'slug' => 'remote-news', + 'name' => 'Remote News', + 'term_link' => 'https://remote.example.com/category/remote-news/', + 'count' => 3, + 'description' => 'Remote category', + 'parent' => 0, + ], + ]; + + wp_cache_set( $post_id, $post, 'posts' ); + + $search = new Search(); + $result = $search->get_post_terms( [], $post_id, 'category' ); + + $this->assertIsArray( $result ); + $this->assertCount( 1, $result ); + $this->assertInstanceOf( \WP_Term::class, $result[0] ); + $this->assertSame( 'Remote News', $result[0]->name ); + $this->assertSame( 'category', $result[0]->taxonomy ); + $this->assertSame( 22, $result[0]->term_id ); + } + // ── filter_render_block ───────────────────────────────────────────── /** @@ -512,11 +673,16 @@ public function test_filter_render_block_rewrites_excerpt_for_remote_post(): voi */ private function enable_search_for_governing_site(): void { update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); - $site_url = Utils::normalize_url( get_site_url() ); + $site_url = Utils::normalize_url( get_site_url() ); + $site_url_with_trailing = trailingslashit( get_site_url() ); update_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, [ - $site_url => [ + $site_url => [ + 'algolia_enabled' => true, + 'searchable_sites' => [ $site_url ], + ], + $site_url_with_trailing => [ 'algolia_enabled' => true, 'searchable_sites' => [ $site_url ], ], diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index ee432d0..919e49d 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -16,8 +16,6 @@ use OneSearch\Tests\TestCase; use OneSearch\Utils; use OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia as AlgoliaSDK; -use OneSearch\Vendor\Psr\Http\Message\RequestInterface; -use OneSearch\Vendor\Psr\Http\Message\ResponseInterface; use PHPUnit\Framework\Attributes\CoversClass; /** @@ -146,57 +144,11 @@ public function test_on_post_transition_does_not_reindex_disallowed_status(): vo update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); update_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL, 'https://governing.example.com' ); - $cached_config = [ - 'algolia_credentials' => [ - 'app_id' => 'TEST_APP', - 'write_key' => 'TEST_KEY', - ], - 'search_settings' => [ - 'algolia_enabled' => true, - 'searchable_sites' => [], - ], - 'indexable_entities' => [ 'post' ], - 'available_sites' => [], - ]; - $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); - $method->invoke( null, $cached_config ); + $this->set_consumer_brand_config_cache(); // Intercept every Algolia SDK HTTP call and record the request paths. $recorded_paths = []; - AlgoliaSDK::setHttpClient( - new class( $recorded_paths ) implements \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\HttpClientInterface { - /** @var array */ - private array $paths; - - /** - * @param array $paths Reference to the array that records intercepted request paths. - */ - public function __construct( array &$paths ) { - $this->paths = &$paths; - } - - /** - * {@inheritDoc} - * - * @param \OneSearch\Vendor\Psr\Http\Message\RequestInterface $request The PSR-7 request. - * @param mixed $timeout Request timeout. - * @param mixed $connect_timeout Connection timeout. - */ - public function sendRequest( RequestInterface $request, mixed $timeout, mixed $connect_timeout ): ResponseInterface { // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter - $path = (string) $request->getUri()->getPath(); - $this->paths[] = $path; - - // getTask polling → mark published so wait() completes immediately. - // All other requests (deleteBy) → return a taskID. - $body = str_contains( $path, '/task/' ) - ? '{"status":"published","pendingTask":false}' - : '{"taskID":1,"updatedAt":"2024-01-01T00:00:00.000Z"}'; - - // @phpstan-ignore return.type - return new \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\Psr7\Response( 200, [], $body ); - } - } - ); + $this->mock_algolia_http_client( $recorded_paths ); $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); $watcher = new Watcher(); @@ -234,10 +186,31 @@ public function test_on_post_transition_consumer_with_cached_config(): void { update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); update_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL, 'https://governing.example.com' ); + $this->set_consumer_brand_config_cache( 'test-app', 'test-key' ); + + $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); + $watcher = new Watcher(); + + // 'post' is in indexable_entities, so it passes the guard. + // Algolia SDK will fail with test credentials, but the guard logic is tested. + $watcher->on_post_transition( 'publish', 'draft', $post ); + + $this->assertTrue( true ); + } + + // ── helpers ──────────────────────────────────────────────────────── + + /** + * Prime the consumer brand config cache used by Watcher guards. + * + * @param string $app_id Algolia app ID. + * @param string $write_key Algolia write key. + */ + private function set_consumer_brand_config_cache( string $app_id = 'TEST_APP', string $write_key = 'TEST_KEY' ): void { $cached_config = [ 'algolia_credentials' => [ - 'app_id' => 'test-app', - 'write_key' => 'test-key', + 'app_id' => $app_id, + 'write_key' => $write_key, ], 'search_settings' => [ 'algolia_enabled' => true, @@ -246,16 +219,8 @@ public function test_on_post_transition_consumer_with_cached_config(): void { 'indexable_entities' => [ 'post' ], 'available_sites' => [], ]; - $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); - $method->invoke( null, $cached_config ); - $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); - $watcher = new Watcher(); - - // 'post' is in indexable_entities, so it passes the guard. - // Algolia SDK will fail with test credentials, but the guard logic is tested. - $watcher->on_post_transition( 'publish', 'draft', $post ); - - $this->assertTrue( true ); + $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); + $method->invoke( null, $cached_config ); } } From 72fdc71d20bbedb1f6441e20e737dd6f88355a18 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Tue, 12 May 2026 18:13:47 +0530 Subject: [PATCH 12/18] tests: enhance IndexTest to validate error handling for SDK exceptions during delete, save, and search operations --- tests/php/TestCase.php | 21 +++++-- tests/php/Unit/Modules/Search/IndexTest.php | 67 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/tests/php/TestCase.php b/tests/php/TestCase.php index 7bb01d5..19e2463 100644 --- a/tests/php/TestCase.php +++ b/tests/php/TestCase.php @@ -86,23 +86,29 @@ protected function checkRequirements(): void { // phpcs:ignore Generic.CodeAnaly * * @param array $recorded_paths Paths captured from outgoing SDK requests. * @param (callable(string): string)|null $body_for_path Optional callback to provide a response body for a given request path. + * @param string|null $throw_on_path_segment Optional path segment that triggers a RuntimeException when matched. */ - public function mock_algolia_http_client( array &$recorded_paths, ?callable $body_for_path = null ): void { + public function mock_algolia_http_client( array &$recorded_paths, ?callable $body_for_path = null, ?string $throw_on_path_segment = null ): void { \OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia::setHttpClient( - new class( $recorded_paths, $body_for_path ) implements \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\HttpClientInterface { + new class( $recorded_paths, $body_for_path, $throw_on_path_segment ) implements \OneSearch\Vendor\Algolia\AlgoliaSearch\Http\HttpClientInterface { /** @var array */ private array $paths; /** @var (callable(string): string)|null */ private $body_for_path; + /** @var string|null */ + private ?string $throw_on_path_segment; + /** * @param array $paths Reference to the array that records intercepted request paths. * @param (callable(string): string)|null $body_for_path Optional callback to generate mock response bodies. + * @param string|null $throw_on_path_segment Optional path segment that triggers a RuntimeException when matched. */ - public function __construct( array &$paths, ?callable $body_for_path ) { - $this->paths = &$paths; - $this->body_for_path = $body_for_path; + public function __construct( array &$paths, ?callable $body_for_path, ?string $throw_on_path_segment ) { + $this->paths = &$paths; + $this->body_for_path = $body_for_path; + $this->throw_on_path_segment = $throw_on_path_segment; } /** @@ -111,11 +117,16 @@ public function __construct( array &$paths, ?callable $body_for_path ) { * @param \OneSearch\Vendor\Psr\Http\Message\RequestInterface $request The PSR-7 request. * @param mixed $timeout Request timeout. * @param mixed $connect_timeout Connection timeout. + * @throws \RuntimeException When the configured path segment is encountered. */ public function sendRequest( \OneSearch\Vendor\Psr\Http\Message\RequestInterface $request, mixed $timeout, mixed $connect_timeout ): \OneSearch\Vendor\Psr\Http\Message\ResponseInterface { // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter $path = (string) $request->getUri()->getPath(); $this->paths[] = $path; + if ( null !== $this->throw_on_path_segment && str_contains( $path, $this->throw_on_path_segment ) ) { + throw new \RuntimeException( 'forced test exception' ); + } + if ( null !== $this->body_for_path ) { $body = (string) call_user_func( $this->body_for_path, $path ); } elseif ( str_contains( $path, '/task/' ) ) { diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index 4752710..9364673 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -135,6 +135,21 @@ public function test_delete_by_returns_true_with_valid_credentials(): void { $this->assertNotEmpty( $recorded_paths ); } + /** + * Returns WP_Error when SDK throws during delete_by failure. + */ + public function test_delete_by_returns_error_when_sdk_throws(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths, null, '/deleteBy' ); + + $result = ( new Index() )->delete_by( [ 'filters' => 'site_url:"http://test.com"' ] ); + + $this->assertWPError( $result ); + $this->assertSame( 'onesearch_algolia_delete_by_failed', $result->get_error_code() ); + } + // ── save_records ──────────────────────────────────────────────────── /** @@ -172,6 +187,28 @@ public function test_save_records_returns_true_with_valid_credentials(): void { $this->assertNotEmpty( $recorded_paths ); } + /** + * Returns WP_Error when SDK throws during save_records. + */ + public function test_save_records_returns_error_when_sdk_throws(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths, null, '/batch' ); + + $result = ( new Index() )->save_records( + [ + [ + 'objectID' => '1', + 'post_title' => 'Test', + ], + ] + ); + + $this->assertWPError( $result ); + $this->assertSame( 'onesearch_algolia_save_records_failed', $result->get_error_code() ); + } + // ── search ────────────────────────────────────────────────────────── /** @@ -205,6 +242,36 @@ public function test_search_returns_results_with_valid_credentials(): void { $this->assertNotEmpty( $recorded_paths ); } + /** + * Returns WP_Error when SDK throws during search. + */ + public function test_search_returns_error_when_sdk_throws(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths, null, '/query' ); + + $result = ( new Index() )->search( 'test query' ); + + $this->assertWPError( $result ); + $this->assertSame( 'onesearch_algolia_search_failed', $result->get_error_code() ); + } + + /** + * Returns WP_Error when SDK throws while setting index settings. + */ + public function test_delete_by_returns_set_settings_error_when_sdk_throws_on_settings(): void { + $this->set_governing_credentials(); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths, null, '/settings' ); + + $result = ( new Index() )->delete_by( [ 'filters' => 'site_url:"http://test.com"' ] ); + + $this->assertWPError( $result ); + $this->assertSame( 'algolia_set_settings_failed', $result->get_error_code() ); + } + // ── index_all_posts ───────────────────────────────────────────────── /** From faac006be3daa8d36089e37243816188262bc0b3 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Tue, 12 May 2026 18:41:51 +0530 Subject: [PATCH 13/18] chore: added minor testcase fixes --- tests/php/Unit/Modules/Search/IndexTest.php | 4 ++-- tests/php/Unit/Modules/Search/SearchTest.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index 9364673..ae463fb 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -54,7 +54,7 @@ public function test_get_index_returns_error_without_credentials(): void { * Returns SearchIndex when credentials are valid. */ public function test_get_index_returns_search_index_with_valid_credentials(): void { - self::set_governing_credentials(); + $this->set_governing_credentials(); $index = new Index(); $result = $index->get_index(); @@ -66,7 +66,7 @@ public function test_get_index_returns_search_index_with_valid_credentials(): vo * Caches the index instance on subsequent calls. */ public function test_get_index_returns_same_instance_on_second_call(): void { - self::set_governing_credentials(); + $this->set_governing_credentials(); $index = new Index(); $first = $index->get_index(); diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index 776b528..695e68a 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -216,6 +216,7 @@ static function ( string $path ): string { $this->assertSame( -18, $result[0]->ID ); $this->assertSame( 'https://remote.example.com/posts/17/', $result[0]->guid ); $this->assertSame( 1, $wp_query->post_count ); + $this->assertSame( 1, $wp_query->found_posts ); $this->assertTrue( (bool) ( $wp_query->is_algolia_search ?? false ) ); $this->assertNotEmpty( $recorded_paths ); } From daab8dcab2101f9dd25130bbf7c5cd41cd3e183b Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Tue, 19 May 2026 00:49:57 +0530 Subject: [PATCH 14/18] tests: remove unwanted comments in IndexTest, SearchTest, SettingsTest, and WatcherTest --- tests/php/Unit/Modules/Search/IndexTest.php | 14 ------------ tests/php/Unit/Modules/Search/SearchTest.php | 22 ------------------- .../php/Unit/Modules/Search/SettingsTest.php | 18 --------------- tests/php/Unit/Modules/Search/WatcherTest.php | 6 ----- 4 files changed, 60 deletions(-) diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index ae463fb..54d84d6 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -33,8 +33,6 @@ protected function tearDown(): void { parent::tearDown(); } - // ── get_index ─────────────────────────────────────────────────────── - /** * Returns WP_Error when Algolia credentials are missing. */ @@ -75,8 +73,6 @@ public function test_get_index_returns_same_instance_on_second_call(): void { $this->assertSame( $first, $second ); } - // ── delete_index ──────────────────────────────────────────────────── - /** * Returns WP_Error when credentials are missing. */ @@ -105,8 +101,6 @@ public function test_delete_index_returns_true_with_valid_credentials(): void { $this->assertNotEmpty( $recorded_paths ); } - // ── delete_by ─────────────────────────────────────────────────────── - /** * Returns WP_Error when credentials are missing. */ @@ -150,8 +144,6 @@ public function test_delete_by_returns_error_when_sdk_throws(): void { $this->assertSame( 'onesearch_algolia_delete_by_failed', $result->get_error_code() ); } - // ── save_records ──────────────────────────────────────────────────── - /** * Returns WP_Error when credentials are missing. */ @@ -209,8 +201,6 @@ public function test_save_records_returns_error_when_sdk_throws(): void { $this->assertSame( 'onesearch_algolia_save_records_failed', $result->get_error_code() ); } - // ── search ────────────────────────────────────────────────────────── - /** * Returns WP_Error when credentials are missing. */ @@ -272,8 +262,6 @@ public function test_delete_by_returns_set_settings_error_when_sdk_throws_on_set $this->assertSame( 'algolia_set_settings_failed', $result->get_error_code() ); } - // ── index_all_posts ───────────────────────────────────────────────── - /** * Returns WP_Error when delete_by fails (no credentials). */ @@ -302,8 +290,6 @@ public function test_index_all_posts_returns_true_with_valid_credentials_and_no_ $this->assertNotEmpty( $recorded_paths ); } - // ── helpers ──────────────────────────────────────────────────────── - /** * Set governing-site context with valid Algolia credentials. */ diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index 695e68a..59c35ed 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -45,8 +45,6 @@ protected function tearDown(): void { parent::tearDown(); } - // ── register_hooks ────────────────────────────────────────────────── - /** * Ensures register_hooks adds the posts_pre_query filter. */ @@ -107,8 +105,6 @@ public function test_register_hooks_adds_render_block_filter(): void { $this->assertNotFalse( has_filter( 'render_block', [ $search, 'filter_render_block' ] ) ); } - // ── get_algolia_results ───────────────────────────────────────────── - /** * Returns original posts when search is not enabled. */ @@ -221,8 +217,6 @@ static function ( string $path ): string { $this->assertNotEmpty( $recorded_paths ); } - // ── get_post_type_permalink ───────────────────────────────────────── - /** * Returns default permalink for local posts (positive ID). */ @@ -256,8 +250,6 @@ public function test_get_post_type_permalink_returns_remote_for_placeholder_post $this->assertSame( 'https://remote.example.com/posts/17/', $result ); } - // ── get_post_author ───────────────────────────────────────────────── - /** * Returns default author name when search is not enabled. * @@ -306,8 +298,6 @@ public function test_get_post_author_returns_remote_when_search_enabled(): void $this->assertSame( 'Remote Author', $result ); } - // ── get_post_author_link ──────────────────────────────────────────── - /** * Returns default author link when search is not enabled. * @@ -352,8 +342,6 @@ public function test_get_post_author_link_returns_remote_when_search_enabled(): $this->assertSame( 'https://remote.example.com/authors/john/', $result ); } - // ── get_post_author_avatar ────────────────────────────────────────── - /** * Returns default avatar URL when search is not enabled. * @@ -398,8 +386,6 @@ public function test_get_post_author_avatar_returns_remote_when_search_enabled() $this->assertSame( 'https://remote.example.com/avatar.jpg', $result ); } - // ── get_term_link ─────────────────────────────────────────────────── - /** * Returns default term link when search is not enabled. * @@ -458,8 +444,6 @@ public function test_get_term_link_returns_remote_when_search_enabled(): void { $this->assertSame( 'https://remote.example.com/category/news/', $result ); } - // ── get_category_link ─────────────────────────────────────────────── - /** * Delegates to get_term_link with 'category' taxonomy. */ @@ -499,8 +483,6 @@ public function test_get_category_link_returns_remote_when_search_enabled(): voi $this->assertSame( 'https://remote.example.com/category/tech/', $result ); } - // ── get_tag_link ──────────────────────────────────────────────────── - /** * Delegates to get_term_link with 'post_tag' taxonomy. */ @@ -540,8 +522,6 @@ public function test_get_tag_link_returns_remote_when_search_enabled(): void { $this->assertSame( 'https://remote.example.com/tag/php/', $result ); } - // ── get_post_terms ────────────────────────────────────────────────── - /** * Returns original terms when search is not enabled. */ @@ -593,8 +573,6 @@ public function test_get_post_terms_returns_remote_terms_when_search_enabled(): $this->assertSame( 22, $result[0]->term_id ); } - // ── filter_render_block ───────────────────────────────────────────── - /** * Returns unchanged block content when search is not enabled. * diff --git a/tests/php/Unit/Modules/Search/SettingsTest.php b/tests/php/Unit/Modules/Search/SettingsTest.php index 78b501d..7e3b830 100644 --- a/tests/php/Unit/Modules/Search/SettingsTest.php +++ b/tests/php/Unit/Modules/Search/SettingsTest.php @@ -34,8 +34,6 @@ protected function tearDown(): void { parent::tearDown(); } - // ── register_hooks ────────────────────────────────────────────────── - /** * Ensures register_hooks adds expected actions. */ @@ -80,8 +78,6 @@ public function test_register_hooks_sets_up_cache_purge_actions(): void { ); } - // ── register_settings ─────────────────────────────────────────────── - /** * Ensures register_settings registers governing settings when governing. */ @@ -152,8 +148,6 @@ public function test_register_settings_sanitizes_search_settings_payload(): void $this->assertSame( [], $sanitized['https://example.com/invalid/']['searchable_sites'] ); } - // ── get_algolia_credentials ───────────────────────────────────────── - /** * Returns null values when no credentials are stored. */ @@ -183,8 +177,6 @@ public function test_get_algolia_credentials_returns_stored_values(): void { $this->assertSame( 'my-key', $creds['write_key'] ); } - // ── set_algolia_credentials ───────────────────────────────────────── - /** * Returns true on successful save. */ @@ -220,8 +212,6 @@ public function test_set_algolia_credentials_stores_null_for_missing_app_id(): v $this->assertNull( $creds['app_id'] ); } - // ── get_indexable_entities ─────────────────────────────────────────── - /** * Returns empty array when nothing is stored. */ @@ -245,8 +235,6 @@ public function test_get_indexable_entities_returns_stored_value(): void { $this->assertSame( $entities, Search_Settings::get_indexable_entities() ); } - // ── get_search_settings ───────────────────────────────────────────── - /** * Returns empty array when nothing is stored. */ @@ -271,8 +259,6 @@ public function test_get_search_settings_returns_stored_value(): void { $this->assertSame( $value, Search_Settings::get_search_settings() ); } - // ── on_site_type_change ───────────────────────────────────────────── - /** * Does nothing when new value is not consumer. */ @@ -285,8 +271,6 @@ public function test_on_site_type_change_skips_non_consumer(): void { $this->assertTrue( true ); } - // ── on_shared_sites_change ────────────────────────────────────────── - /** * Does nothing when old value is empty. */ @@ -352,8 +336,6 @@ public function test_on_shared_sites_change_keeps_entities_when_delete_fails(): $this->assertSame( $initial_entities, get_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ) ); } - // ── purge_cache_on_update ─────────────────────────────────────────── - /** * Purges cache when algolia credentials option updates. */ diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index 919e49d..2ac9615 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -38,8 +38,6 @@ protected function tearDown(): void { parent::tearDown(); } - // ── register_hooks ────────────────────────────────────────────────── - /** * Ensures register_hooks adds transition_post_status action. */ @@ -50,8 +48,6 @@ public function test_register_hooks_adds_transition_post_status_action(): void { $this->assertNotFalse( has_action( 'transition_post_status', [ $watcher, 'on_post_transition' ] ) ); } - // ── on_post_transition ────────────────────────────────────────────── - /** * Skips when post is not a WP_Post instance. */ @@ -198,8 +194,6 @@ public function test_on_post_transition_consumer_with_cached_config(): void { $this->assertTrue( true ); } - // ── helpers ──────────────────────────────────────────────────────── - /** * Prime the consumer brand config cache used by Watcher guards. * From af78be24838668b235431b6e2fddbcda3ebbcfc8 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Thu, 21 May 2026 19:00:58 +0530 Subject: [PATCH 15/18] tests: refactor IndexTest and SearchTest for improved credential handling and remove unused code --- tests/php/Unit/Modules/Search/IndexTest.php | 88 +++++++--- tests/php/Unit/Modules/Search/SearchTest.php | 153 +++++++++++------- .../php/Unit/Modules/Search/SettingsTest.php | 22 ++- 3 files changed, 168 insertions(+), 95 deletions(-) diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index 54d84d6..915e60b 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -9,6 +9,7 @@ namespace OneSearch\Tests\Unit\Modules\Search; +use OneSearch\Modules\Rest\Governing_Data_Handler; use OneSearch\Modules\Search\Index; use OneSearch\Modules\Search\Settings as Search_Settings; use OneSearch\Modules\Settings\Settings; @@ -27,9 +28,6 @@ final class IndexTest extends TestCase { protected function tearDown(): void { AlgoliaSDK::resetHttpClient(); - delete_option( Settings::OPTION_SITE_TYPE ); - delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); - parent::tearDown(); } @@ -52,7 +50,7 @@ public function test_get_index_returns_error_without_credentials(): void { * Returns SearchIndex when credentials are valid. */ public function test_get_index_returns_search_index_with_valid_credentials(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $index = new Index(); $result = $index->get_index(); @@ -64,7 +62,7 @@ public function test_get_index_returns_search_index_with_valid_credentials(): vo * Caches the index instance on subsequent calls. */ public function test_get_index_returns_same_instance_on_second_call(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $index = new Index(); $first = $index->get_index(); @@ -87,10 +85,25 @@ public function test_delete_index_returns_error_without_credentials(): void { } /** - * Returns true for delete_index with valid credentials. + * Returns true for delete_index with valid credentials on governing sites. */ public function test_delete_index_returns_true_with_valid_credentials(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $result = ( new Index() )->delete_index(); + + $this->assertTrue( $result ); + $this->assertNotEmpty( $recorded_paths ); + } + + /** + * Returns true for delete_index with valid credentials on consumer site. + */ + public function test_delete_index_returns_true_with_valid_credentials_on_consumer(): void { + $this->set_credentials( Settings::SITE_TYPE_CONSUMER ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths ); @@ -118,7 +131,7 @@ public function test_delete_by_returns_error_without_credentials(): void { * Returns true for delete_by with valid credentials. */ public function test_delete_by_returns_true_with_valid_credentials(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths ); @@ -133,7 +146,7 @@ public function test_delete_by_returns_true_with_valid_credentials(): void { * Returns WP_Error when SDK throws during delete_by failure. */ public function test_delete_by_returns_error_when_sdk_throws(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths, null, '/deleteBy' ); @@ -161,7 +174,7 @@ public function test_save_records_returns_error_without_credentials(): void { * Returns true for save_records with valid credentials. */ public function test_save_records_returns_true_with_valid_credentials(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths ); @@ -183,7 +196,7 @@ public function test_save_records_returns_true_with_valid_credentials(): void { * Returns WP_Error when SDK throws during save_records. */ public function test_save_records_returns_error_when_sdk_throws(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths, null, '/batch' ); @@ -218,7 +231,7 @@ public function test_search_returns_error_without_credentials(): void { * Returns search payload for search with valid credentials. */ public function test_search_returns_results_with_valid_credentials(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; @@ -236,7 +249,7 @@ public function test_search_returns_results_with_valid_credentials(): void { * Returns WP_Error when SDK throws during search. */ public function test_search_returns_error_when_sdk_throws(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths, null, '/query' ); @@ -251,7 +264,7 @@ public function test_search_returns_error_when_sdk_throws(): void { * Returns WP_Error when SDK throws while setting index settings. */ public function test_delete_by_returns_set_settings_error_when_sdk_throws_on_settings(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths, null, '/settings' ); @@ -279,7 +292,7 @@ public function test_index_all_posts_returns_error_when_delete_fails(): void { * Returns true when index_all_posts is called with no post types and valid credentials. */ public function test_index_all_posts_returns_true_with_valid_credentials_and_no_post_types(): void { - $this->set_governing_credentials(); + $this->set_credentials( Settings::SITE_TYPE_GOVERNING ); $recorded_paths = []; $this->mock_algolia_http_client( $recorded_paths ); @@ -291,15 +304,42 @@ public function test_index_all_posts_returns_true_with_valid_credentials_and_no_ } /** - * Set governing-site context with valid Algolia credentials. + * Set site context with valid Algolia credentials. */ - private function set_governing_credentials(): void { - update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); - Search_Settings::set_algolia_credentials( - [ - 'app_id' => 'TEST_APP', - 'write_key' => 'TEST_KEY', - ] - ); + private function set_credentials( $site_type ): void { + + if ( $site_type === Settings::SITE_TYPE_GOVERNING ) { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ] + ); + } + if ( $site_type === Settings::SITE_TYPE_CONSUMER ) { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); + Settings::set_parent_site_url( 'https://governing.example.com' ); + + // Pre-populate the brand-config cache so Algolia::get_algolia_credentials() + // on a consumer site resolves locally instead of making an HTTP request + // to the governing site. + set_transient( + Governing_Data_Handler::TRANSIENT_KEY, + [ + 'algolia_credentials' => [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ], + 'search_settings' => [ + 'algolia_enabled' => true, + 'searchable_sites' => [], + ], + 'indexable_entities' => [], + 'available_sites' => [], + ], + HOUR_IN_SECONDS + ); + } } } diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index 59c35ed..5354a60 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -28,11 +28,6 @@ final class SearchTest extends TestCase { */ protected function tearDown(): void { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable -- Resetting test state. - global $post, $wp_query, $wp_the_query; - - $post = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wp_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable - $wp_the_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable AlgoliaSDK::resetHttpClient(); @@ -46,63 +41,13 @@ protected function tearDown(): void { } /** - * Ensures register_hooks adds the posts_pre_query filter. - */ - public function test_register_hooks_adds_posts_pre_query_filter(): void { - $search = new Search(); - $search->register_hooks(); - - $this->assertNotFalse( has_filter( 'posts_pre_query', [ $search, 'get_algolia_results' ] ) ); - } - - /** - * Ensures register_hooks adds permalink filters. - */ - public function test_register_hooks_adds_permalink_filters(): void { - $search = new Search(); - $search->register_hooks(); - - $this->assertNotFalse( has_filter( 'post_link', [ $search, 'get_post_type_permalink' ] ) ); - $this->assertNotFalse( has_filter( 'page_link', [ $search, 'get_post_type_permalink' ] ) ); - $this->assertNotFalse( has_filter( 'post_type_link', [ $search, 'get_post_type_permalink' ] ) ); - $this->assertNotFalse( has_filter( 'page_type_link', [ $search, 'get_post_type_permalink' ] ) ); - $this->assertNotFalse( has_filter( 'attachment_link', [ $search, 'get_post_type_permalink' ] ) ); - } - - /** - * Ensures register_hooks adds author data filters. - */ - public function test_register_hooks_adds_author_filters(): void { - $search = new Search(); - $search->register_hooks(); - - $this->assertNotFalse( has_filter( 'get_the_author_display_name', [ $search, 'get_post_author' ] ) ); - $this->assertNotFalse( has_filter( 'author_link', [ $search, 'get_post_author_link' ] ) ); - $this->assertNotFalse( has_filter( 'get_avatar_url', [ $search, 'get_post_author_avatar' ] ) ); - } - - /** - * Ensures register_hooks adds term/taxonomy filters. - */ - public function test_register_hooks_adds_term_filters(): void { - $search = new Search(); - $search->register_hooks(); - - $this->assertNotFalse( has_filter( 'term_link', [ $search, 'get_term_link' ] ) ); - $this->assertNotFalse( has_filter( 'category_link', [ $search, 'get_category_link' ] ) ); - $this->assertNotFalse( has_filter( 'tag_link', [ $search, 'get_tag_link' ] ) ); - $this->assertNotFalse( has_filter( 'get_the_terms', [ $search, 'get_post_terms' ] ) ); - $this->assertNotFalse( has_filter( 'wp_get_post_terms', [ $search, 'get_post_terms' ] ) ); - } - - /** - * Ensures register_hooks adds block render filter. + * Ensures class can be instantiated. */ - public function test_register_hooks_adds_render_block_filter(): void { + public function test_class_instantiation(): void { $search = new Search(); $search->register_hooks(); - $this->assertNotFalse( has_filter( 'render_block', [ $search, 'filter_render_block' ] ) ); + $this->assertTrue( true ); } /** @@ -217,6 +162,98 @@ static function ( string $path ): string { $this->assertNotEmpty( $recorded_paths ); } + /** + * Sorts hits by Algolia ranking score (descending) without raising notices. + */ + public function test_get_algolia_results_sorts_hits_by_ranking_score(): void { + $this->enable_search_for_governing_site(); + Search_Settings::set_algolia_credentials( + [ + 'app_id' => 'TEST_APP', + 'write_key' => 'TEST_KEY', + ] + ); + + $recorded_paths = []; + $this->mock_algolia_http_client( + $recorded_paths, + static function ( string $path ): string { + if ( str_contains( $path, '/query' ) ) { + return wp_json_encode( + [ + 'hits' => [ + [ + 'objectID' => '20', + 'site_post_id' => '20', + 'post_id' => 20, + 'post_title' => 'Middle Score', + 'post_type' => 'post', + 'permalink' => 'https://remote.example.com/posts/20/', + 'site_url' => 'https://remote.example.com/', + 'site_name' => 'Remote', + 'total_chunks' => 1, + 'post_date_gmt' => 1710000000, + 'post_modified_gmt' => 1710000000, + '_rankingInfo' => [ 'rankingScore' => 0.5 ], + ], + [ + 'objectID' => '30', + 'site_post_id' => '30', + 'post_id' => 30, + 'post_title' => 'Lowest Score', + 'post_type' => 'post', + 'permalink' => 'https://remote.example.com/posts/30/', + 'site_url' => 'https://remote.example.com/', + 'site_name' => 'Remote', + 'total_chunks' => 1, + 'post_date_gmt' => 1710000000, + 'post_modified_gmt' => 1710000000, + '_rankingInfo' => [ 'rankingScore' => 0.1 ], + ], + [ + 'objectID' => '40', + 'site_post_id' => '40', + 'post_id' => 40, + 'post_title' => 'Highest Score', + 'post_type' => 'post', + 'permalink' => 'https://remote.example.com/posts/40/', + 'site_url' => 'https://remote.example.com/', + 'site_name' => 'Remote', + 'total_chunks' => 1, + 'post_date_gmt' => 1710000000, + 'post_modified_gmt' => 1710000000, + '_rankingInfo' => [ 'rankingScore' => 0.9 ], + ], + ], + 'nbHits' => 3, + 'page' => 0, + 'hitsPerPage' => 10, + ] + ) ?: '{}'; + } + + if ( str_contains( $path, '/task/' ) ) { + return '{"status":"published","pendingTask":false}'; + } + + return '{"taskID":1,"updatedAt":"2024-01-01T00:00:00.000Z"}'; + } + ); + + $this->prime_main_search_query( 'remote test' ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reading query prepared by helper. + global $wp_query; + + $search = new Search(); + $result = $search->get_algolia_results( [], $wp_query ); + + $this->assertCount( 3, $result ); + $this->assertSame( 'Highest Score', $result[0]->post_title ); + $this->assertSame( 'Middle Score', $result[1]->post_title ); + $this->assertSame( 'Lowest Score', $result[2]->post_title ); + } + /** * Returns default permalink for local posts (positive ID). */ diff --git a/tests/php/Unit/Modules/Search/SettingsTest.php b/tests/php/Unit/Modules/Search/SettingsTest.php index 7e3b830..852444f 100644 --- a/tests/php/Unit/Modules/Search/SettingsTest.php +++ b/tests/php/Unit/Modules/Search/SettingsTest.php @@ -94,24 +94,20 @@ public function test_register_settings_registers_governing_options(): void { $this->assertArrayHasKey( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, $registered ); } - /** - * Ensures register_settings sanitizes algolia credentials payload. - */ + /** + * Ensures register_settings sanitizes algolia credentials payload. + */ public function test_register_settings_sanitizes_algolia_credentials(): void { $settings = new Search_Settings(); $settings->register_settings(); - $registered = get_registered_settings(); - $sanitize = $registered[ Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ]['sanitize_callback'] ?? null; - - $this->assertIsCallable( $sanitize ); + $raw = [ + 'app_id' => ' app-id ', + 'write_key' => "\nwrite-key\t", + ]; - $sanitized = $sanitize( - [ - 'app_id' => ' app-id ', - 'write_key' => "\nwrite-key\t", - ] - ); + update_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS, $raw ); + $sanitized = get_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); $this->assertSame( 'app-id', $sanitized['app_id'] ); $this->assertSame( 'write-key', $sanitized['write_key'] ); From 37794ddfb9143ce2ecaf46ca987bae3e82f8e8d3 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Thu, 21 May 2026 22:17:27 +0530 Subject: [PATCH 16/18] tests: add integration tests for Algolia reindexing on site type changes in SettingsTest and WatcherTest --- .../php/Unit/Modules/Search/SettingsTest.php | 40 ++++++++++++ tests/php/Unit/Modules/Search/WatcherTest.php | 61 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/tests/php/Unit/Modules/Search/SettingsTest.php b/tests/php/Unit/Modules/Search/SettingsTest.php index 852444f..3c115fd 100644 --- a/tests/php/Unit/Modules/Search/SettingsTest.php +++ b/tests/php/Unit/Modules/Search/SettingsTest.php @@ -25,6 +25,7 @@ final class SettingsTest extends TestCase { */ protected function tearDown(): void { delete_option( Settings::OPTION_SITE_TYPE ); + delete_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL ); delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); delete_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); @@ -267,6 +268,45 @@ public function test_on_site_type_change_skips_non_consumer(): void { $this->assertTrue( true ); } + /** + * Deletes Algolia index when site type changes to consumer. + * + * When the site type is updated to 'consumer', on_site_type_change should + * call delete_index() which triggers an Algolia delete request. + */ + public function test_on_site_type_change_deletes_index_for_consumer(): void { + // Set site type to consumer (simulates the option already being saved + // before the update_option_ hook fires). + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); + update_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL, 'https://governing.example.com' ); + + // Prime brand config cache so Algolia credentials resolve via consumer path. + $cached_config = [ + 'algolia_credentials' => [ + 'app_id' => 'test-app', + 'write_key' => 'test-key', + ], + 'search_settings' => [ + 'algolia_enabled' => true, + 'searchable_sites' => [], + ], + 'indexable_entities' => [ 'post' ], + 'available_sites' => [], + ]; + + $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); + $method->invoke( null, $cached_config ); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $settings = new Search_Settings(); + $settings->on_site_type_change( Settings::SITE_TYPE_GOVERNING, Settings::SITE_TYPE_CONSUMER ); + + // delete_index() on a consumer site calls deleteBy, which hits Algolia. + $this->assertNotEmpty( $recorded_paths, 'Changing to consumer should trigger Algolia delete_index call.' ); + } + /** * Does nothing when old value is empty. */ diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index 2ac9615..5a0d67a 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -217,4 +217,65 @@ private function set_consumer_brand_config_cache( string $app_id = 'TEST_APP', s $method = new \ReflectionMethod( Governing_Data_Handler::class, 'set_brand_config_cache' ); $method->invoke( null, $cached_config ); } + + /** + * Indexable post triggers Algolia saveObjects (reindex) call. + * This verifies the full integration flow for a governing site. + */ + public function test_on_post_transition_triggers_algolia_reindex(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); + update_option( + Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS, + [ + 'app_id' => 'test-app', + 'write_key' => 'test-key', + ] + ); + update_option( + Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES, + [ + 'entities' => [ + Utils::normalize_url( get_site_url() ) => [ 'post' ], + ], + ] + ); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); + $watcher = new Watcher(); + + // Transition from 'draft' to 'publish' (should trigger reindex). + $watcher->on_post_transition( 'publish', 'draft', $post ); + + // Assert that a /batch (saveObjects) call was made. + $batch_calls = array_filter( $recorded_paths, static fn ( $p ) => str_contains( $p, '/batch' ) ); + $this->assertNotEmpty( $batch_calls, 'Happy path should trigger Algolia reindex (saveObjects).' ); + } + + /** + * Indexable post triggers Algolia saveObjects (reindex) call on a consumer (brand) site. + * This verifies the full integration flow where credentials and indexable entities + * are resolved from the governing site's brand config cache. + */ + public function test_on_post_transition_triggers_algolia_reindex_consumer_site(): void { + update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); + update_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL, 'https://governing.example.com' ); + + $this->set_consumer_brand_config_cache( 'test-app', 'test-key' ); + + $recorded_paths = []; + $this->mock_algolia_http_client( $recorded_paths ); + + $post = self::factory()->post->create_and_get( [ 'post_status' => 'publish' ] ); + $watcher = new Watcher(); + + // Transition from 'draft' to 'publish' (should trigger reindex). + $watcher->on_post_transition( 'publish', 'draft', $post ); + + // Assert that a /batch (saveObjects) call was made. + $batch_calls = array_filter( $recorded_paths, static fn ( $p ) => str_contains( $p, '/batch' ) ); + $this->assertNotEmpty( $batch_calls, 'Consumer site happy path should trigger Algolia reindex (saveObjects).' ); + } } From 34d94e2f0af241bbe53c7f843e328028c810b816 Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Thu, 21 May 2026 23:25:20 +0530 Subject: [PATCH 17/18] fix: phpcs errors resolved --- tests/php/Unit/Modules/Search/IndexTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/php/Unit/Modules/Search/IndexTest.php b/tests/php/Unit/Modules/Search/IndexTest.php index 915e60b..5a0a263 100644 --- a/tests/php/Unit/Modules/Search/IndexTest.php +++ b/tests/php/Unit/Modules/Search/IndexTest.php @@ -305,10 +305,12 @@ public function test_index_all_posts_returns_true_with_valid_credentials_and_no_ /** * Set site context with valid Algolia credentials. + * + * @param string $site_type Site type (governing or consumer). */ - private function set_credentials( $site_type ): void { + private function set_credentials( string $site_type ): void { - if ( $site_type === Settings::SITE_TYPE_GOVERNING ) { + if ( Settings::SITE_TYPE_GOVERNING === $site_type ) { update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_GOVERNING ); Search_Settings::set_algolia_credentials( [ @@ -317,7 +319,7 @@ private function set_credentials( $site_type ): void { ] ); } - if ( $site_type === Settings::SITE_TYPE_CONSUMER ) { + if ( Settings::SITE_TYPE_CONSUMER === $site_type ) { update_option( Settings::OPTION_SITE_TYPE, Settings::SITE_TYPE_CONSUMER ); Settings::set_parent_site_url( 'https://governing.example.com' ); From c5f46ea770c1070417d900c5bf68b0af603fda4d Mon Sep 17 00:00:00 2001 From: Kallyan Singha Date: Fri, 22 May 2026 01:48:27 +0530 Subject: [PATCH 18/18] refactor: clean up test teardown methods by removing redundant option resets and updating settings test logic --- tests/php/Unit/Modules/Search/SearchTest.php | 9 ------ .../php/Unit/Modules/Search/SettingsTest.php | 31 +++++++------------ tests/php/Unit/Modules/Search/WatcherTest.php | 6 ---- 3 files changed, 11 insertions(+), 35 deletions(-) diff --git a/tests/php/Unit/Modules/Search/SearchTest.php b/tests/php/Unit/Modules/Search/SearchTest.php index 5354a60..ab2e6f5 100644 --- a/tests/php/Unit/Modules/Search/SearchTest.php +++ b/tests/php/Unit/Modules/Search/SearchTest.php @@ -9,7 +9,6 @@ namespace OneSearch\Tests\Unit\Modules\Search; -use OneSearch\Modules\Rest\Governing_Data_Handler; use OneSearch\Modules\Search\Search; use OneSearch\Modules\Search\Settings as Search_Settings; use OneSearch\Modules\Settings\Settings; @@ -27,16 +26,8 @@ final class SearchTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable -- Resetting test state. - AlgoliaSDK::resetHttpClient(); - delete_option( Settings::OPTION_SITE_TYPE ); - delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); - delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); - delete_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL ); - delete_transient( Governing_Data_Handler::TRANSIENT_KEY ); - parent::tearDown(); } diff --git a/tests/php/Unit/Modules/Search/SettingsTest.php b/tests/php/Unit/Modules/Search/SettingsTest.php index 3c115fd..b93e6d9 100644 --- a/tests/php/Unit/Modules/Search/SettingsTest.php +++ b/tests/php/Unit/Modules/Search/SettingsTest.php @@ -13,6 +13,7 @@ use OneSearch\Modules\Search\Settings as Search_Settings; use OneSearch\Modules\Settings\Settings; use OneSearch\Tests\TestCase; +use OneSearch\Vendor\Algolia\AlgoliaSearch\Algolia as AlgoliaSDK; use PHPUnit\Framework\Attributes\CoversClass; /** @@ -24,13 +25,7 @@ final class SettingsTest extends TestCase { * {@inheritDoc} */ protected function tearDown(): void { - delete_option( Settings::OPTION_SITE_TYPE ); - delete_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL ); - delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); - delete_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); - delete_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); - delete_option( Settings::OPTION_GOVERNING_SHARED_SITES ); - delete_transient( Governing_Data_Handler::TRANSIENT_KEY ); + AlgoliaSDK::resetHttpClient(); parent::tearDown(); } @@ -121,20 +116,16 @@ public function test_register_settings_sanitizes_search_settings_payload(): void $settings = new Search_Settings(); $settings->register_settings(); - $registered = get_registered_settings(); - $sanitize = $registered[ Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ]['sanitize_callback'] ?? null; - - $this->assertIsCallable( $sanitize ); + $raw = [ + ' https://example.com/site ' => [ + 'algolia_enabled' => 1, + 'searchable_sites' => [ ' https://child.example.com/x ' ], + ], + 'https://example.com/invalid' => 'not-array', + ]; - $sanitized = $sanitize( - [ - ' https://example.com/site ' => [ - 'algolia_enabled' => 1, - 'searchable_sites' => [ ' https://child.example.com/x ' ], - ], - 'https://example.com/invalid' => 'not-array', - ] - ); + update_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS, $raw ); + $sanitized = get_option( Search_Settings::OPTION_GOVERNING_SEARCH_SETTINGS ); $this->assertArrayHasKey( 'https://example.com/site/', $sanitized ); $this->assertTrue( $sanitized['https://example.com/site/']['algolia_enabled'] ); diff --git a/tests/php/Unit/Modules/Search/WatcherTest.php b/tests/php/Unit/Modules/Search/WatcherTest.php index 5a0d67a..be4a8d1 100644 --- a/tests/php/Unit/Modules/Search/WatcherTest.php +++ b/tests/php/Unit/Modules/Search/WatcherTest.php @@ -29,12 +29,6 @@ final class WatcherTest extends TestCase { protected function tearDown(): void { AlgoliaSDK::resetHttpClient(); - delete_option( Settings::OPTION_SITE_TYPE ); - delete_option( Search_Settings::OPTION_GOVERNING_ALGOLIA_CREDENTIALS ); - delete_option( Search_Settings::OPTION_GOVERNING_INDEXABLE_SITES ); - delete_option( Settings::OPTION_CONSUMER_PARENT_SITE_URL ); - delete_transient( Governing_Data_Handler::TRANSIENT_KEY ); - parent::tearDown(); }