| 1 |
<?php |
| 2 |
/** |
| 3 |
* Unit tests for the AdminClass class. |
| 4 |
* |
| 5 |
* @package Custom_404_Pro |
| 6 |
*/ |
| 7 |
|
| 8 |
use PHPUnit\Framework\TestCase; |
| 9 |
|
| 10 |
/** |
| 11 |
* AdminClass test case. |
| 12 |
*/ |
| 13 |
class AdminClassTest extends TestCase { |
| 14 |
|
| 15 |
/** |
| 16 |
* Asserts that helpers is an explicitly declared property. |
| 17 |
*/ |
| 18 |
public function test_helpers_is_declared_property() { |
| 19 |
$ref = new ReflectionClass( AdminClass::class ); |
| 20 |
$this->assertTrue( |
| 21 |
$ref->hasProperty( 'helpers' ), |
| 22 |
'helpers must be explicitly declared to avoid PHP 8.2+ deprecation' |
| 23 |
); |
| 24 |
} |
| 25 |
|
| 26 |
/** |
| 27 |
* Asserts that helpers is an instance of Helpers after construction. |
| 28 |
*/ |
| 29 |
public function test_helpers_is_instance_of_helpers_after_construction() { |
| 30 |
$admin = new AdminClass(); |
| 31 |
$ref = new ReflectionClass( $admin ); |
| 32 |
$prop = $ref->getProperty( 'helpers' ); |
| 33 |
$prop->setAccessible( true ); |
| 34 |
$this->assertInstanceOf( Helpers::class, $prop->getValue( $admin ) ); |
| 35 |
} |
| 36 |
|
| 37 |
/** |
| 38 |
* Resets multilingual stubs, filter registry, redirect captures, and transient store before each test. |
| 39 |
*/ |
| 40 |
protected function setUp(): void { |
| 41 |
parent::setUp(); |
| 42 |
$GLOBALS['_test_filters'] = array(); |
| 43 |
$GLOBALS['_test_transients'] = array(); |
| 44 |
$GLOBALS['_test_options'] = array(); |
| 45 |
$GLOBALS['_test_is_404'] = false; |
| 46 |
$GLOBALS['_test_permalinks'] = array(); |
| 47 |
$GLOBALS['_test_redirect_url'] = null; |
| 48 |
$GLOBALS['_test_redirect_status'] = null; |
| 49 |
$GLOBALS['_pll_get_post_return'] = false; |
| 50 |
unset( $GLOBALS['_pll_current_language'] ); |
| 51 |
unset( $GLOBALS['_pll_default_language'] ); |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Asserts that the original page ID is returned when Polylang finds no translation. |
| 56 |
*/ |
| 57 |
public function test_resolve_multilingual_page_id_returns_original_when_polylang_finds_no_translation() { |
| 58 |
$GLOBALS['_pll_get_post_return'] = false; // pll_get_post returns false → no translation. |
| 59 |
$admin = new AdminClass(); |
| 60 |
$this->assertSame( 42, $admin->resolve_multilingual_page_id( 42 ) ); |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Asserts that the translated page ID is returned when Polylang finds a translation. |
| 65 |
*/ |
| 66 |
public function test_resolve_multilingual_page_id_returns_translated_id_from_polylang() { |
| 67 |
$GLOBALS['_pll_get_post_return'] = 99; |
| 68 |
$GLOBALS['_pll_current_language'] = 'fr'; |
| 69 |
$admin = new AdminClass(); |
| 70 |
$this->assertSame( 99, $admin->resolve_multilingual_page_id( 42 ) ); |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Asserts that the translated page ID is returned when a WPML filter is registered. |
| 75 |
*/ |
| 76 |
public function test_resolve_multilingual_page_id_returns_translated_id_from_wpml_filter() { |
| 77 |
add_filter( |
| 78 |
'wpml_object_id', |
| 79 |
function ( $page_id ) { |
| 80 |
return 77; // Simulate WPML returning the translated page ID. |
| 81 |
} |
| 82 |
); |
| 83 |
$admin = new AdminClass(); |
| 84 |
$this->assertSame( 77, $admin->resolve_multilingual_page_id( 42 ) ); |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Asserts that the original page ID is returned when no WPML filter is registered. |
| 89 |
*/ |
| 90 |
public function test_resolve_multilingual_page_id_returns_original_when_no_wpml_filter_registered() { |
| 91 |
$admin = new AdminClass(); |
| 92 |
$this->assertSame( 42, $admin->resolve_multilingual_page_id( 42 ) ); |
| 93 |
} |
| 94 |
|
| 95 |
// ------------------------------------------------------------------ |
| 96 |
// normalize_page_id_to_default_language |
| 97 |
// ------------------------------------------------------------------ |
| 98 |
|
| 99 |
/** |
| 100 |
* Calls the private normalize_page_id_to_default_language method via reflection. |
| 101 |
* |
| 102 |
* @param AdminClass $admin Instance to invoke on. |
| 103 |
* @param int $page_id Page ID to normalize. |
| 104 |
* @return int |
| 105 |
*/ |
| 106 |
private function normalize( AdminClass $admin, int $page_id ): int { |
| 107 |
$ref = new ReflectionClass( $admin ); |
| 108 |
$method = $ref->getMethod( 'normalize_page_id_to_default_language' ); |
| 109 |
$method->setAccessible( true ); |
| 110 |
return $method->invoke( $admin, $page_id ); |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Asserts that the original ID is returned when no multilingual plugin is active. |
| 115 |
*/ |
| 116 |
public function test_normalize_returns_original_id_when_no_multilingual_plugin_active() { |
| 117 |
$admin = new AdminClass(); |
| 118 |
$this->assertSame( 42, $this->normalize( $admin, 42 ) ); |
| 119 |
} |
| 120 |
|
| 121 |
/** |
| 122 |
* Asserts that the WPML default-language ID is returned when a WPML filter is registered. |
| 123 |
*/ |
| 124 |
public function test_normalize_returns_default_language_id_via_wpml() { |
| 125 |
// Register wpml_default_language filter. |
| 126 |
add_filter( 'wpml_default_language', function () { return 'en'; } ); |
| 127 |
// Register wpml_object_id filter that returns the default-language page ID. |
| 128 |
add_filter( |
| 129 |
'wpml_object_id', |
| 130 |
function ( $page_id ) { |
| 131 |
return 5; // Simulate WPML returning the English (default) page for any input. |
| 132 |
} |
| 133 |
); |
| 134 |
$admin = new AdminClass(); |
| 135 |
$this->assertSame( 5, $this->normalize( $admin, 20 ) ); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Asserts that the Polylang default-language ID is returned when pll_get_post finds a translation. |
| 140 |
*/ |
| 141 |
public function test_normalize_returns_default_language_id_via_polylang() { |
| 142 |
$GLOBALS['_pll_default_language'] = 'en'; |
| 143 |
$GLOBALS['_pll_get_post_return'] = 10; // pll_get_post returns the default-language page. |
| 144 |
$admin = new AdminClass(); |
| 145 |
$this->assertSame( 10, $this->normalize( $admin, 20 ) ); |
| 146 |
} |
| 147 |
|
| 148 |
/** |
| 149 |
* Asserts that the original ID is returned when Polylang finds no default-language translation. |
| 150 |
*/ |
| 151 |
public function test_normalize_returns_original_id_when_polylang_finds_no_default_translation() { |
| 152 |
$GLOBALS['_pll_default_language'] = 'en'; |
| 153 |
$GLOBALS['_pll_get_post_return'] = false; // pll_get_post returns false → no translation. |
| 154 |
$admin = new AdminClass(); |
| 155 |
$this->assertSame( 20, $this->normalize( $admin, 20 ) ); |
| 156 |
} |
| 157 |
|
| 158 |
// ------------------------------------------------------------------ |
| 159 |
// custom_404_pro_redirect — page mode |
| 160 |
// ------------------------------------------------------------------ |
| 161 |
|
| 162 |
/** |
| 163 |
* Helper: seed settings into the test options store. |
| 164 |
* |
| 165 |
* @param array $settings Settings to merge into defaults. |
| 166 |
*/ |
| 167 |
private function seed_settings( array $settings ): void { |
| 168 |
$defaults = array( |
| 169 |
'mode' => '', |
| 170 |
'mode_page' => '0', |
| 171 |
'mode_url' => '', |
| 172 |
'redirect_error_code' => 302, |
| 173 |
'logging_enabled' => false, |
| 174 |
'send_email' => false, |
| 175 |
'log_ip' => true, |
| 176 |
'email_cooldown' => HOUR_IN_SECONDS, |
| 177 |
'max_log_count' => 0, |
| 178 |
'max_log_age' => 0, |
| 179 |
); |
| 180 |
update_option( Helpers::OPTION_KEY, array_merge( $defaults, $settings ) ); |
| 181 |
} |
| 182 |
|
| 183 |
/** |
| 184 |
* Asserts that page mode redirects to the URL returned by get_permalink(), not any other value. |
| 185 |
*/ |
| 186 |
public function test_redirect_page_mode_uses_get_permalink() { |
| 187 |
$GLOBALS['_test_is_404'] = true; |
| 188 |
$GLOBALS['_test_permalinks'][42] = 'https://example.com/custom-404/'; |
| 189 |
$this->seed_settings( array( 'mode' => 'page', 'mode_page' => '42' ) ); |
| 190 |
|
| 191 |
$admin = new AdminClass(); |
| 192 |
$admin->custom_404_pro_redirect(); |
| 193 |
|
| 194 |
$this->assertSame( |
| 195 |
'https://example.com/custom-404/', |
| 196 |
$GLOBALS['_test_redirect_url'], |
| 197 |
'Redirect URL must come from get_permalink(), not post->guid or any other property.' |
| 198 |
); |
| 199 |
} |
| 200 |
|
| 201 |
/** |
| 202 |
* Asserts that no redirect fires in page mode when get_permalink() returns false |
| 203 |
* (e.g. the configured page ID no longer exists). |
| 204 |
*/ |
| 205 |
public function test_redirect_page_mode_does_not_redirect_when_permalink_is_false() { |
| 206 |
$GLOBALS['_test_is_404'] = true; |
| 207 |
$GLOBALS['_test_permalinks'][99] = false; // page deleted / unpublished |
| 208 |
$this->seed_settings( array( 'mode' => 'page', 'mode_page' => '99' ) ); |
| 209 |
|
| 210 |
$admin = new AdminClass(); |
| 211 |
$admin->custom_404_pro_redirect(); |
| 212 |
|
| 213 |
$this->assertNull( |
| 214 |
$GLOBALS['_test_redirect_url'], |
| 215 |
'No redirect should fire when get_permalink() returns false.' |
| 216 |
); |
| 217 |
} |
| 218 |
|
| 219 |
/** |
| 220 |
* Asserts that no redirect fires when the current request is not a 404. |
| 221 |
*/ |
| 222 |
public function test_redirect_page_mode_does_not_fire_when_not_404() { |
| 223 |
$GLOBALS['_test_is_404'] = false; |
| 224 |
$GLOBALS['_test_permalinks'][42] = 'https://example.com/custom-404/'; |
| 225 |
$this->seed_settings( array( 'mode' => 'page', 'mode_page' => '42' ) ); |
| 226 |
|
| 227 |
$admin = new AdminClass(); |
| 228 |
$admin->custom_404_pro_redirect(); |
| 229 |
|
| 230 |
$this->assertNull( |
| 231 |
$GLOBALS['_test_redirect_url'], |
| 232 |
'No redirect should fire for non-404 requests.' |
| 233 |
); |
| 234 |
} |
| 235 |
|
| 236 |
// ------------------------------------------------------------------ |
| 237 |
// is_email_on_cooldown |
| 238 |
// ------------------------------------------------------------------ |
| 239 |
|
| 240 |
/** |
| 241 |
* Asserts that is_email_on_cooldown() returns false when no transient has been set. |
| 242 |
*/ |
| 243 |
public function test_is_email_on_cooldown_returns_false_when_no_transient_set() { |
| 244 |
$admin = new AdminClass(); |
| 245 |
$this->assertFalse( $admin->is_email_on_cooldown() ); |
| 246 |
} |
| 247 |
|
| 248 |
/** |
| 249 |
* Asserts that is_email_on_cooldown() returns true when the cooldown transient is set. |
| 250 |
*/ |
| 251 |
public function test_is_email_on_cooldown_returns_true_when_transient_is_set() { |
| 252 |
set_transient( 'custom_404_pro_email_cooldown', true, HOUR_IN_SECONDS ); |
| 253 |
$admin = new AdminClass(); |
| 254 |
$this->assertTrue( $admin->is_email_on_cooldown() ); |
| 255 |
} |
| 256 |
|
| 257 |
/** |
| 258 |
* Asserts that is_email_on_cooldown() returns false after the cooldown transient is deleted. |
| 259 |
*/ |
| 260 |
public function test_is_email_on_cooldown_returns_false_after_transient_deleted() { |
| 261 |
set_transient( 'custom_404_pro_email_cooldown', true, HOUR_IN_SECONDS ); |
| 262 |
delete_transient( 'custom_404_pro_email_cooldown' ); |
| 263 |
$admin = new AdminClass(); |
| 264 |
$this->assertFalse( $admin->is_email_on_cooldown() ); |
| 265 |
} |
| 266 |
|
| 267 |
// ------------------------------------------------------------------ |
| 268 |
// Asset cache busting |
| 269 |
// ------------------------------------------------------------------ |
| 270 |
|
| 271 |
/** |
| 272 |
* Admin assets must be versioned with the plugin version so that each |
| 273 |
* release invalidates the browser cache. This was pinned to a hardcoded |
| 274 |
* '3.2.0' for many releases, leaving users on stale CSS and JS. |
| 275 |
*/ |
| 276 |
public function test_asset_version_tracks_the_plugin_version() { |
| 277 |
$this->assertSame( CUSTOM_404_PRO_VERSION, AdminClass::asset_version() ); |
| 278 |
} |
| 279 |
|
| 280 |
/** |
| 281 |
* The asset version must no longer be the stale hardcoded value. |
| 282 |
*/ |
| 283 |
public function test_asset_version_is_not_the_stale_hardcoded_value() { |
| 284 |
$this->assertNotSame( '3.2.0', AdminClass::asset_version() ); |
| 285 |
} |
| 286 |
|
| 287 |
/** |
| 288 |
* asset_version() must be callable statically from the enqueue callbacks. |
| 289 |
*/ |
| 290 |
public function test_asset_version_is_public_static() { |
| 291 |
$ref = new ReflectionClass( AdminClass::class ); |
| 292 |
$method = $ref->getMethod( 'asset_version' ); |
| 293 |
$this->assertTrue( $method->isPublic(), 'asset_version must be public' ); |
| 294 |
$this->assertTrue( $method->isStatic(), 'asset_version must be static' ); |
| 295 |
} |
| 296 |
} |
| 297 |
|