assertTrue( $ref->hasProperty( 'helpers' ), 'helpers must be explicitly declared to avoid PHP 8.2+ deprecation' ); } /** * Asserts that helpers is an instance of Helpers after construction. */ public function test_helpers_is_instance_of_helpers_after_construction() { $admin = new AdminClass(); $ref = new ReflectionClass( $admin ); $prop = $ref->getProperty( 'helpers' ); $prop->setAccessible( true ); $this->assertInstanceOf( Helpers::class, $prop->getValue( $admin ) ); } /** * Resets multilingual stubs, filter registry, redirect captures, and transient store before each test. */ protected function setUp(): void { parent::setUp(); $GLOBALS['_test_filters'] = array(); $GLOBALS['_test_transients'] = array(); $GLOBALS['_test_options'] = array(); $GLOBALS['_test_is_404'] = false; $GLOBALS['_test_permalinks'] = array(); $GLOBALS['_test_redirect_url'] = null; $GLOBALS['_test_redirect_status'] = null; $GLOBALS['_pll_get_post_return'] = false; unset( $GLOBALS['_pll_current_language'] ); unset( $GLOBALS['_pll_default_language'] ); } /** * Asserts that the original page ID is returned when Polylang finds no translation. */ public function test_resolve_multilingual_page_id_returns_original_when_polylang_finds_no_translation() { $GLOBALS['_pll_get_post_return'] = false; // pll_get_post returns false → no translation. $admin = new AdminClass(); $this->assertSame( 42, $admin->resolve_multilingual_page_id( 42 ) ); } /** * Asserts that the translated page ID is returned when Polylang finds a translation. */ public function test_resolve_multilingual_page_id_returns_translated_id_from_polylang() { $GLOBALS['_pll_get_post_return'] = 99; $GLOBALS['_pll_current_language'] = 'fr'; $admin = new AdminClass(); $this->assertSame( 99, $admin->resolve_multilingual_page_id( 42 ) ); } /** * Asserts that the translated page ID is returned when a WPML filter is registered. */ public function test_resolve_multilingual_page_id_returns_translated_id_from_wpml_filter() { add_filter( 'wpml_object_id', function ( $page_id ) { return 77; // Simulate WPML returning the translated page ID. } ); $admin = new AdminClass(); $this->assertSame( 77, $admin->resolve_multilingual_page_id( 42 ) ); } /** * Asserts that the original page ID is returned when no WPML filter is registered. */ public function test_resolve_multilingual_page_id_returns_original_when_no_wpml_filter_registered() { $admin = new AdminClass(); $this->assertSame( 42, $admin->resolve_multilingual_page_id( 42 ) ); } // ------------------------------------------------------------------ // normalize_page_id_to_default_language // ------------------------------------------------------------------ /** * Calls the private normalize_page_id_to_default_language method via reflection. * * @param AdminClass $admin Instance to invoke on. * @param int $page_id Page ID to normalize. * @return int */ private function normalize( AdminClass $admin, int $page_id ): int { $ref = new ReflectionClass( $admin ); $method = $ref->getMethod( 'normalize_page_id_to_default_language' ); $method->setAccessible( true ); return $method->invoke( $admin, $page_id ); } /** * Asserts that the original ID is returned when no multilingual plugin is active. */ public function test_normalize_returns_original_id_when_no_multilingual_plugin_active() { $admin = new AdminClass(); $this->assertSame( 42, $this->normalize( $admin, 42 ) ); } /** * Asserts that the WPML default-language ID is returned when a WPML filter is registered. */ public function test_normalize_returns_default_language_id_via_wpml() { // Register wpml_default_language filter. add_filter( 'wpml_default_language', function () { return 'en'; } ); // Register wpml_object_id filter that returns the default-language page ID. add_filter( 'wpml_object_id', function ( $page_id ) { return 5; // Simulate WPML returning the English (default) page for any input. } ); $admin = new AdminClass(); $this->assertSame( 5, $this->normalize( $admin, 20 ) ); } /** * Asserts that the Polylang default-language ID is returned when pll_get_post finds a translation. */ public function test_normalize_returns_default_language_id_via_polylang() { $GLOBALS['_pll_default_language'] = 'en'; $GLOBALS['_pll_get_post_return'] = 10; // pll_get_post returns the default-language page. $admin = new AdminClass(); $this->assertSame( 10, $this->normalize( $admin, 20 ) ); } /** * Asserts that the original ID is returned when Polylang finds no default-language translation. */ public function test_normalize_returns_original_id_when_polylang_finds_no_default_translation() { $GLOBALS['_pll_default_language'] = 'en'; $GLOBALS['_pll_get_post_return'] = false; // pll_get_post returns false → no translation. $admin = new AdminClass(); $this->assertSame( 20, $this->normalize( $admin, 20 ) ); } // ------------------------------------------------------------------ // custom_404_pro_redirect — page mode // ------------------------------------------------------------------ /** * Helper: seed settings into the test options store. * * @param array $settings Settings to merge into defaults. */ private function seed_settings( array $settings ): void { $defaults = array( 'mode' => '', 'mode_page' => '0', 'mode_url' => '', 'redirect_error_code' => 302, 'logging_enabled' => false, 'send_email' => false, 'log_ip' => true, 'email_cooldown' => HOUR_IN_SECONDS, 'max_log_count' => 0, 'max_log_age' => 0, ); update_option( Helpers::OPTION_KEY, array_merge( $defaults, $settings ) ); } /** * Asserts that page mode redirects to the URL returned by get_permalink(), not any other value. */ public function test_redirect_page_mode_uses_get_permalink() { $GLOBALS['_test_is_404'] = true; $GLOBALS['_test_permalinks'][42] = 'https://example.com/custom-404/'; $this->seed_settings( array( 'mode' => 'page', 'mode_page' => '42' ) ); $admin = new AdminClass(); $admin->custom_404_pro_redirect(); $this->assertSame( 'https://example.com/custom-404/', $GLOBALS['_test_redirect_url'], 'Redirect URL must come from get_permalink(), not post->guid or any other property.' ); } /** * Asserts that no redirect fires in page mode when get_permalink() returns false * (e.g. the configured page ID no longer exists). */ public function test_redirect_page_mode_does_not_redirect_when_permalink_is_false() { $GLOBALS['_test_is_404'] = true; $GLOBALS['_test_permalinks'][99] = false; // page deleted / unpublished $this->seed_settings( array( 'mode' => 'page', 'mode_page' => '99' ) ); $admin = new AdminClass(); $admin->custom_404_pro_redirect(); $this->assertNull( $GLOBALS['_test_redirect_url'], 'No redirect should fire when get_permalink() returns false.' ); } /** * Asserts that no redirect fires when the current request is not a 404. */ public function test_redirect_page_mode_does_not_fire_when_not_404() { $GLOBALS['_test_is_404'] = false; $GLOBALS['_test_permalinks'][42] = 'https://example.com/custom-404/'; $this->seed_settings( array( 'mode' => 'page', 'mode_page' => '42' ) ); $admin = new AdminClass(); $admin->custom_404_pro_redirect(); $this->assertNull( $GLOBALS['_test_redirect_url'], 'No redirect should fire for non-404 requests.' ); } // ------------------------------------------------------------------ // is_email_on_cooldown // ------------------------------------------------------------------ /** * Asserts that is_email_on_cooldown() returns false when no transient has been set. */ public function test_is_email_on_cooldown_returns_false_when_no_transient_set() { $admin = new AdminClass(); $this->assertFalse( $admin->is_email_on_cooldown() ); } /** * Asserts that is_email_on_cooldown() returns true when the cooldown transient is set. */ public function test_is_email_on_cooldown_returns_true_when_transient_is_set() { set_transient( 'custom_404_pro_email_cooldown', true, HOUR_IN_SECONDS ); $admin = new AdminClass(); $this->assertTrue( $admin->is_email_on_cooldown() ); } /** * Asserts that is_email_on_cooldown() returns false after the cooldown transient is deleted. */ public function test_is_email_on_cooldown_returns_false_after_transient_deleted() { set_transient( 'custom_404_pro_email_cooldown', true, HOUR_IN_SECONDS ); delete_transient( 'custom_404_pro_email_cooldown' ); $admin = new AdminClass(); $this->assertFalse( $admin->is_email_on_cooldown() ); } // ------------------------------------------------------------------ // Asset cache busting // ------------------------------------------------------------------ /** * Admin assets must be versioned with the plugin version so that each * release invalidates the browser cache. This was pinned to a hardcoded * '3.2.0' for many releases, leaving users on stale CSS and JS. */ public function test_asset_version_tracks_the_plugin_version() { $this->assertSame( CUSTOM_404_PRO_VERSION, AdminClass::asset_version() ); } /** * The asset version must no longer be the stale hardcoded value. */ public function test_asset_version_is_not_the_stale_hardcoded_value() { $this->assertNotSame( '3.2.0', AdminClass::asset_version() ); } /** * asset_version() must be callable statically from the enqueue callbacks. */ public function test_asset_version_is_public_static() { $ref = new ReflectionClass( AdminClass::class ); $method = $ref->getMethod( 'asset_version' ); $this->assertTrue( $method->isPublic(), 'asset_version must be public' ); $this->assertTrue( $method->isStatic(), 'asset_version must be static' ); } }