Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion tests/phpunit/includes/abstract-testcase.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase {
protected static $hooks_saved = array();
protected static $ignore_files;

/**
* Metadata state before each class's fixtures are created, keyed by class.
*
* @var array[]
*/
private static $meta_before_class = array();

/**
* Metadata state before the current test, or null when setup was bypassed.
*
* @var array|null
*/
private $original_meta_state;

/**
* The value of $GLOBALS['locale'] before each test, or null if it was unset.
*
Expand Down Expand Up @@ -95,7 +109,8 @@ public static function set_up_before_class() {
$wpdb->db_connect();
ini_set( 'display_errors', 1 );

$class = get_called_class();
$class = get_called_class();
self::$meta_before_class[ $class ] = self::backup_meta_state();

if ( method_exists( $class, 'wpSetUpBeforeClass' ) ) {
call_user_func( array( $class, 'wpSetUpBeforeClass' ), static::factory() );
Expand All @@ -114,6 +129,11 @@ public static function tear_down_after_class() {
call_user_func( array( $class, 'wpTearDownAfterClass' ) );
}

if ( isset( self::$meta_before_class[ $class ] ) ) {
self::restore_meta_state( self::$meta_before_class[ $class ] );
unset( self::$meta_before_class[ $class ] );
}

_delete_all_data();
self::flush_cache();

Expand Down Expand Up @@ -142,6 +162,8 @@ public function set_up() {
$this->_backup_hooks();
}

$this->original_meta_state = self::backup_meta_state();

$this->clean_up_global_scope();

$this->original_locale = $GLOBALS['locale'] ?? null;
Expand Down Expand Up @@ -268,6 +290,9 @@ public function tear_down() {
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
remove_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) );
$this->_restore_hooks();
if ( null !== $this->original_meta_state ) {
self::restore_meta_state( $this->original_meta_state );
}
wp_set_current_user( 0 );

// Restore the locale captured in set_up(); skip it for tests that bypass parent::set_up().
Expand Down Expand Up @@ -512,6 +537,57 @@ public static function flush_cache() {
wp_cache_add_non_persistent_groups( array( 'counts', 'plugins', 'theme_json' ) );
}

/**
* Captures registrations and their hooks without re-registering metadata.
*
* The hooks also include legacy registrations without a registry entry and
* callbacks added independently of register_meta().
*
* @return array Metadata registry and cloned metadata hooks.
*/
private static function backup_meta_state() {
$state = array(
'keys' => $GLOBALS['wp_meta_keys'] ?? array(),
'hooks' => array(),
);

foreach ( $GLOBALS['wp_filter'] as $name => $hook ) {
if ( self::is_meta_hook( $name ) ) {
$state['hooks'][ $name ] = clone $hook;
}
}

return $state;
}

/**
* Restores metadata hooks after the ordinary hook baseline has been restored.
*
* @param array $state Metadata state captured before the test or class.
*/
private static function restore_meta_state( $state ) {
$GLOBALS['wp_meta_keys'] = $state['keys'];

foreach ( array_keys( $GLOBALS['wp_filter'] ) as $name ) {
if ( self::is_meta_hook( $name ) ) {
unset( $GLOBALS['wp_filter'][ $name ] );
}
}
foreach ( $state['hooks'] as $name => $hook ) {
$GLOBALS['wp_filter'][ $name ] = clone $hook;
}
}

/**
* Identifies the dynamic hooks used by metadata registrations and defaults.
*
* @param string $name Hook name.
* @return bool Whether the hook belongs to the metadata state.
*/
private static function is_meta_hook( $name ) {
return (bool) preg_match( '/^(?:sanitize|auth)_.+_meta_|^default_.+_metadata$/', $name );
}

/**
* Cleans up any registered meta keys.
*
Expand Down
212 changes: 212 additions & 0 deletions tests/phpunit/tests/includes/registeredMeta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<?php

/**
* Tests the metadata isolation provided by the WordPress test case.
*
* The outer test has no WordPress transaction. The inner test cases exercise
* their real setup and teardown routines in sequence.
*
* @group phpunit
* @group meta
* @ticket 48300
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class Tests_Includes_RegisteredMeta extends PHPUnit_Adapter_TestCase {

/**
* @dataProvider data_registered_metadata
*
* @param string $object_type Metadata object type.
* @param string $object_subtype Metadata object subtype.
* @param bool $unregister Whether to remove rather than replace the registration.
*/
public function test_restores_metadata_registered_before_test_methods( $object_type, $object_subtype, $unregister ) {
$key = '_test_registered_meta';
$args = array(
'object_subtype' => $object_subtype,
'single' => true,
'default' => 'Original default',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => '__return_true',
);
register_meta( $object_type, $key, $args );
$expected = get_registered_meta_keys( $object_type, $object_subtype )[ $key ];
$case = $this->create_test_case();
$class = get_class( $case );
$class::set_up_before_class();

try {
$case->set_up();
try {
if ( $unregister ) {
unregister_meta_key( $object_type, $key, $object_subtype );
} else {
register_meta(
$object_type,
$key,
array_merge(
$args,
array(
'default' => 'Changed default',
'auth_callback' => '__return_false',
)
)
);
}
register_meta( $object_type, '_test_temporary_meta', $args );
} finally {
$case->tear_down();
}

$next_case = new $class( 'test_second' );
$next_case->set_up();
try {
$registered = get_registered_meta_keys( $object_type, $object_subtype );
$this->assertArrayHasKey( $key, $registered );
$this->assertSame( $expected, $registered[ $key ] );
$this->assertSame( 'Example', sanitize_meta( $key, '<em>Example</em>', $object_type, $object_subtype ) );
$hook = "auth_{$object_type}_meta_{$key}";
if ( $object_subtype ) {
$hook .= "_for_{$object_subtype}";
}
$this->assertTrue( apply_filters( $hook, false, $key, 1, 1, 'edit_post_meta', array() ) );
$this->assertFalse( registered_meta_key_exists( $object_type, '_test_temporary_meta', $object_subtype ) );
} finally {
$next_case->tear_down();
}
} finally {
$class::tear_down_after_class();
}
}

/**
* @return array[]
*/
public static function data_registered_metadata() {
// Each case boots WordPress separately. Cover the scopes and mutation modes
// without repeating their full cross-product in isolated processes.
return array(
'post replaced' => array( 'post', '', false ),
'post subtype removed' => array( 'post', 'page', true ),
'term subtype replaced' => array( 'term', 'category', false ),
'user removed' => array( 'user', '', true ),
);
}

public function test_restores_class_registrations_and_their_callbacks() {
// Capture the ordinary hook baseline before the class fixture exists.
$previous_case = $this->create_test_case();
$previous_case->set_up();
$previous_case->tear_down();

register_post_meta( 'post', '_test_plugin_meta', array( 'default' => 'Plugin default' ) );
$case = new class( 'test_first' ) extends WP_UnitTestCase {
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
register_post_meta(
'post',
'_test_class_meta',
array(
'default' => 'Class default',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => '__return_true',
)
);
register_post_meta( 'post', '_test_plugin_meta', array( 'default' => 'Class override' ) );
// Old-style registrations have callbacks but no registry entry.
register_meta( 'post', '_test_legacy_meta', 'sanitize_text_field' );
}

public function test_first() {}
public function test_second() {}
};
$class = get_class( $case );
$class::set_up_before_class();

try {
foreach ( array( $case, new $class( 'test_second' ) ) as $test_case ) {
$test_case->set_up();
try {
$registered = get_registered_meta_keys( 'post', 'post' );
$this->assertArrayHasKey( '_test_class_meta', $registered );
$this->assertArrayHasKey( '_test_plugin_meta', $registered );
$this->assertSame( 'Class default', $registered['_test_class_meta']['default'] );
$this->assertSame( 'Class override', $registered['_test_plugin_meta']['default'] );
$this->assertSame( 'Example', sanitize_meta( '_test_class_meta', '<em>Example</em>', 'post', 'post' ) );
$this->assertSame( 'Example', sanitize_meta( '_test_legacy_meta', '<em>Example</em>', 'post' ) );
unregister_post_meta( 'post', '_test_class_meta' );
remove_filter( 'sanitize_post_meta__test_legacy_meta', 'sanitize_text_field' );
} finally {
$test_case->tear_down();
}
}
} finally {
$class::tear_down_after_class();
}

$this->assertFalse( registered_meta_key_exists( 'post', '_test_class_meta', 'post' ) );
$registered = get_registered_meta_keys( 'post', 'post' );
$this->assertArrayHasKey( '_test_plugin_meta', $registered );
$this->assertSame( 'Plugin default', $registered['_test_plugin_meta']['default'] );
$this->assertSame( '<em>Example</em>', sanitize_meta( '_test_legacy_meta', '<em>Example</em>', 'post' ) );
$this->assertFalse( has_filter( 'sanitize_post_meta__test_class_meta_for_post' ) );
}

public function test_restores_defaults_sanitization_and_authorization_for_real_posts() {
register_post_meta(
'post',
'_test_post_meta',
array(
'single' => true,
'default' => 'Original default',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => '__return_true',
)
);
$case = new class( 'test_first' ) extends WP_UnitTestCase {
public static $author_id;
public static $post_id;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$author_id = $factory->user->create( array( 'role' => 'author' ) );
self::$post_id = $factory->post->create( array( 'post_author' => self::$author_id ) );
}

public function test_first() {}
public function test_second() {}
};
$class = get_class( $case );
$class::set_up_before_class();

try {
foreach ( array( $case, new $class( 'test_second' ) ) as $test_case ) {
$test_case->set_up();
try {
wp_set_current_user( $class::$author_id );
$this->assertTrue( current_user_can( 'edit_post_meta', $class::$post_id, '_test_post_meta' ) );
$this->assertSame( 'Original default', get_post_meta( $class::$post_id, '_test_post_meta', true ) );
update_post_meta( $class::$post_id, '_test_post_meta', '<em>Example</em>' );
$this->assertSame( 'Example', get_post_meta( $class::$post_id, '_test_post_meta', true ) );
register_post_meta( 'post', '_test_post_meta', array( 'auth_callback' => '__return_false' ) );
$this->assertFalse( current_user_can( 'edit_post_meta', $class::$post_id, '_test_post_meta' ) );
} finally {
$test_case->tear_down();
}
}
} finally {
$class::tear_down_after_class();
}
}

/**
* Creates a test case with valid method names for the real setup routines.
*
* @return WP_UnitTestCase
*/
private function create_test_case() {
return new class( 'test_first' ) extends WP_UnitTestCase {
public function test_first() {}
public function test_second() {}
};
}
}
7 changes: 7 additions & 0 deletions tests/phpunit/tests/meta/registerMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase {
protected static $user_id;
protected static $blog_id;

public function set_up() {
parent::set_up();

// These tests inspect the complete registry, independently of bootstrap registrations.
$this->unregister_all_meta_keys();
}

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$post_id = $factory->post->create( array( 'post_type' => 'page' ) );
self::$term_id = $factory->term->create( array( 'taxonomy' => 'category' ) );
Expand Down
7 changes: 7 additions & 0 deletions tests/phpunit/tests/post/meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ class Tests_Post_Meta extends WP_UnitTestCase {
protected static $post_id;
protected static $post_id_2;

public function set_up() {
parent::set_up();

// These tests inspect the complete registry, independently of bootstrap registrations.
$this->unregister_all_meta_keys();
}

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$author = $factory->user->create_and_get( array( 'role' => 'editor' ) );

Expand Down
3 changes: 3 additions & 0 deletions tests/phpunit/tests/term/meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class Tests_Term_Meta extends WP_UnitTestCase {

public function set_up() {
parent::set_up();

// These tests inspect the complete registry, independently of bootstrap registrations.
$this->unregister_all_meta_keys();
register_taxonomy( 'wptests_tax', 'post' );
}

Expand Down
Loading