@@ -47,6 +47,19 @@ public function setUp(): void {
4747 Router::init ();
4848 }
4949
50+ /**
51+ * Tear down test environment.
52+ */
53+ public function tear_down (): void {
54+ // Clean up common state that may be left by tests.
55+ unset( $ _SERVER ['HTTP_ACCEPT ' ] );
56+ \set_query_var ( 'preview ' , null );
57+ \set_query_var ( 'term_id ' , null );
58+ Query::get_instance ()->__destruct ();
59+
60+ parent ::tear_down ();
61+ }
62+
5063 /**
5164 * Test that ActivityPub requests for custom post types return 200.
5265 *
@@ -220,19 +233,271 @@ public function test_preview_template_filter() {
220233 $ _SERVER ['HTTP_ACCEPT ' ] = 'application/activity+json ' ;
221234 \set_query_var ( 'preview ' , true );
222235
236+ // Save callback to variable for proper removal.
237+ $ preview_template_callback = function () {
238+ return '/custom/template.php ' ;
239+ };
240+
223241 // Add filter before testing.
224- \add_filter (
225- 'activitypub_preview_template ' ,
226- function () {
227- return '/custom/template.php ' ;
228- }
229- );
242+ \add_filter ( 'activitypub_preview_template ' , $ preview_template_callback );
230243
231244 // Test that the filter is applied.
232245 $ template = Router::render_activitypub_template ( 'original.php ' );
233246 $ this ->assertEquals ( '/custom/template.php ' , $ template , 'Custom preview template should be used when filter is applied. ' );
234247
248+ // Clean up.
249+ \remove_filter ( 'activitypub_preview_template ' , $ preview_template_callback );
250+ }
251+
252+ /**
253+ * Test that the activitypub_supported_taxonomies filter has correct defaults.
254+ *
255+ * @covers ::template_redirect
256+ */
257+ public function test_supported_taxonomies_filter_defaults () {
258+ $ supported = \apply_filters ( 'activitypub_supported_taxonomies ' , array ( 'category ' , 'post_tag ' ) );
259+
260+ $ this ->assertContains ( 'category ' , $ supported , 'Category should be a supported taxonomy by default. ' );
261+ $ this ->assertContains ( 'post_tag ' , $ supported , 'Post tag should be a supported taxonomy by default. ' );
262+ $ this ->assertCount ( 2 , $ supported , 'Should have exactly 2 default supported taxonomies. ' );
263+ }
264+
265+ /**
266+ * Test that the activitypub_supported_taxonomies filter can be modified.
267+ *
268+ * @covers ::template_redirect
269+ */
270+ public function test_supported_taxonomies_filter_can_be_modified () {
271+ \add_filter (
272+ 'activitypub_supported_taxonomies ' ,
273+ function ( $ taxonomies ) {
274+ $ taxonomies [] = 'custom_taxonomy ' ;
275+ return $ taxonomies ;
276+ }
277+ );
278+
279+ $ supported = \apply_filters ( 'activitypub_supported_taxonomies ' , array ( 'category ' , 'post_tag ' ) );
280+
281+ $ this ->assertContains ( 'custom_taxonomy ' , $ supported , 'Custom taxonomy should be added via filter. ' );
282+ $ this ->assertCount ( 3 , $ supported , 'Should have 3 taxonomies after adding custom one. ' );
283+
284+ // Clean up.
285+ \remove_all_filters ( 'activitypub_supported_taxonomies ' );
286+ }
287+
288+ /**
289+ * Test that unsupported taxonomy terms don't trigger redirects.
290+ *
291+ * This test verifies the fix for #2730 (Polylang conflict) and #2725 (posts page redirect).
292+ * When a term_id belongs to an unsupported taxonomy, the router should return early
293+ * without redirecting. The unsupported taxonomy check happens before the ActivityPub
294+ * request check, so no HTTP_ACCEPT header is needed.
295+ *
296+ * @covers ::template_redirect
297+ */
298+ public function test_unsupported_taxonomy_does_not_redirect () {
299+ // Register a custom taxonomy (simulating Polylang's language taxonomy).
300+ \register_taxonomy (
301+ 'language ' ,
302+ 'post ' ,
303+ array (
304+ 'public ' => true ,
305+ 'label ' => 'Language ' ,
306+ )
307+ );
308+
309+ // Create a term in the custom taxonomy.
310+ $ term = \wp_insert_term ( 'English ' , 'language ' );
311+ $ this ->assertNotWPError ( $ term , 'Term creation should succeed. ' );
312+
313+ $ term_id = $ term ['term_id ' ];
314+
315+ // Set the term_id query var (simulating what might happen with Polylang).
316+ \set_query_var ( 'term_id ' , $ term_id );
317+
318+ global $ wp_query ;
319+
320+ // Call template_redirect - it should return early for unsupported taxonomy.
321+ // Note: No HTTP_ACCEPT header needed because the taxonomy check happens first.
322+ Router::template_redirect ();
323+
324+ // The query should not be set to 404 for valid but unsupported taxonomy terms.
325+ $ this ->assertFalse ( $ wp_query ->is_404 (), 'Should not set 404 for valid unsupported taxonomy terms. ' );
326+
327+ // Clean up.
328+ \set_query_var ( 'term_id ' , null );
329+ \wp_delete_term ( $ term_id , 'language ' );
330+ \unregister_taxonomy ( 'language ' );
331+ }
332+
333+ /**
334+ * Test that supported taxonomy terms are handled correctly for ActivityPub requests.
335+ *
336+ * @covers ::template_redirect
337+ */
338+ public function test_supported_taxonomy_activitypub_request_no_redirect () {
339+ // Create a category term.
340+ $ term = \wp_insert_term ( 'Test Category ' , 'category ' );
341+ $ this ->assertNotWPError ( $ term , 'Term creation should succeed. ' );
342+
343+ $ term_id = $ term ['term_id ' ];
344+
345+ // Set the term_id query var.
346+ \set_query_var ( 'term_id ' , $ term_id );
347+
348+ // Simulate an ActivityPub request - should return early without redirect.
349+ $ _SERVER ['HTTP_ACCEPT ' ] = 'application/activity+json ' ;
350+
351+ global $ wp_query ;
352+
353+ // Call template_redirect - it should return early for ActivityPub requests.
354+ Router::template_redirect ();
355+
356+ // The query should not be set to 404 for valid category terms.
357+ $ this ->assertFalse ( $ wp_query ->is_404 (), 'Should not set 404 for valid category terms. ' );
358+
235359 // Clean up.
236360 unset( $ _SERVER ['HTTP_ACCEPT ' ] );
361+ \set_query_var ( 'term_id ' , null );
362+ \wp_delete_term ( $ term_id , 'category ' );
363+ }
364+
365+ /**
366+ * Test that invalid term_id sets 404.
367+ *
368+ * @covers ::template_redirect
369+ */
370+ public function test_invalid_term_id_sets_404 () {
371+ // Set an invalid term_id query var.
372+ \set_query_var ( 'term_id ' , 999999 );
373+
374+ global $ wp_query ;
375+
376+ // Call template_redirect - it should set 404 for invalid term.
377+ Router::template_redirect ();
378+
379+ $ this ->assertTrue ( $ wp_query ->is_404 (), 'Should set 404 for invalid term_id. ' );
380+
381+ // Clean up.
382+ \set_query_var ( 'term_id ' , null );
383+ $ wp_query ->is_404 = false ;
384+ }
385+
386+ /**
387+ * Test that supported taxonomy terms trigger redirects for non-ActivityPub requests.
388+ *
389+ * This verifies the core redirect functionality still works after the taxonomy filtering fix.
390+ * Uses an exception in the wp_redirect filter to intercept before exit() is called.
391+ *
392+ * @covers ::template_redirect
393+ *
394+ * @throws \Exception If a non-redirect exception is caught during template_redirect.
395+ */
396+ public function test_supported_taxonomy_triggers_redirect () {
397+ // Create a category term.
398+ $ term = \wp_insert_term ( 'Redirect Test Category ' , 'category ' );
399+ $ this ->assertNotWPError ( $ term , 'Term creation should succeed. ' );
400+
401+ $ term_id = $ term ['term_id ' ];
402+ $ term_link = \get_term_link ( $ term_id , 'category ' );
403+
404+ // Set the term_id query var.
405+ \set_query_var ( 'term_id ' , $ term_id );
406+
407+ // Save callback to variable for proper removal.
408+ $ redirect_callback = function ( $ location ) {
409+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
410+ throw new \Exception ( 'REDIRECT: ' . $ location );
411+ };
412+
413+ // Use exception to intercept redirect before exit() is called.
414+ \add_filter ( 'wp_redirect ' , $ redirect_callback );
415+
416+ $ redirect_location = null ;
417+ try {
418+ Router::template_redirect ();
419+ } catch ( \Exception $ e ) {
420+ if ( 0 === strpos ( $ e ->getMessage (), 'REDIRECT: ' ) ) {
421+ $ redirect_location = substr ( $ e ->getMessage (), 9 );
422+ } else {
423+ throw $ e ;
424+ }
425+ }
426+
427+ // Verify redirect was attempted to the correct term link.
428+ $ this ->assertNotNull ( $ redirect_location , 'Should attempt redirect for supported taxonomy term. ' );
429+ $ this ->assertEquals ( $ term_link , $ redirect_location , 'Should redirect to the term link. ' );
430+
431+ // Clean up.
432+ \remove_filter ( 'wp_redirect ' , $ redirect_callback );
433+ \wp_delete_term ( $ term_id , 'category ' );
434+ }
435+
436+ /**
437+ * Test that the activitypub_supported_taxonomies filter is actually used by the Router.
438+ *
439+ * This verifies that adding a custom taxonomy via the filter allows redirects for that taxonomy.
440+ *
441+ * @covers ::template_redirect
442+ *
443+ * @throws \Exception If a non-redirect exception is caught during template_redirect.
444+ */
445+ public function test_filter_adds_custom_taxonomy_to_redirects () {
446+ // Register a custom taxonomy.
447+ \register_taxonomy (
448+ 'custom_tax ' ,
449+ 'post ' ,
450+ array (
451+ 'public ' => true ,
452+ 'label ' => 'Custom Tax ' ,
453+ )
454+ );
455+
456+ // Create a term in the custom taxonomy.
457+ $ term = \wp_insert_term ( 'Custom Term ' , 'custom_tax ' );
458+ $ this ->assertNotWPError ( $ term , 'Term creation should succeed. ' );
459+
460+ $ term_id = $ term ['term_id ' ];
461+ $ term_link = \get_term_link ( $ term_id , 'custom_tax ' );
462+
463+ // Set the term_id query var.
464+ \set_query_var ( 'term_id ' , $ term_id );
465+
466+ // Save callbacks to variables for proper removal.
467+ $ taxonomy_callback = function ( $ taxonomies ) {
468+ $ taxonomies [] = 'custom_tax ' ;
469+ return $ taxonomies ;
470+ };
471+ $ redirect_callback = function ( $ location ) {
472+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
473+ throw new \Exception ( 'REDIRECT: ' . $ location );
474+ };
475+
476+ // Add custom taxonomy to supported list via filter.
477+ \add_filter ( 'activitypub_supported_taxonomies ' , $ taxonomy_callback );
478+
479+ // Use exception to intercept redirect before exit() is called.
480+ \add_filter ( 'wp_redirect ' , $ redirect_callback );
481+
482+ $ redirect_location = null ;
483+ try {
484+ Router::template_redirect ();
485+ } catch ( \Exception $ e ) {
486+ if ( 0 === strpos ( $ e ->getMessage (), 'REDIRECT: ' ) ) {
487+ $ redirect_location = substr ( $ e ->getMessage (), 9 );
488+ } else {
489+ throw $ e ;
490+ }
491+ }
492+
493+ // Verify redirect was attempted.
494+ $ this ->assertNotNull ( $ redirect_location , 'Should attempt redirect for custom taxonomy added via filter. ' );
495+ $ this ->assertEquals ( $ term_link , $ redirect_location , 'Should redirect to the custom taxonomy term link. ' );
496+
497+ // Clean up.
498+ \remove_filter ( 'wp_redirect ' , $ redirect_callback );
499+ \remove_filter ( 'activitypub_supported_taxonomies ' , $ taxonomy_callback );
500+ \wp_delete_term ( $ term_id , 'custom_tax ' );
501+ \unregister_taxonomy ( 'custom_tax ' );
237502 }
238503}
0 commit comments