'fb5ab760', 'post/hp_request_core' => '27d6f4f5', 'post/hp_vendor_core' => '5366e5c8', 'user/hp_user' => '61ac526f', ); /** * Lookups that do not pass a post type (or run with scoped routing off, * until a site's rows are reconciled) keep the legacy owner of keys shared * by several post groups: the LAST group registered. Pinned so the * registration order cannot change silently. */ private const SHARED_KEY_OWNERS = array( 'hp_status' => 'hp_request_core', 'hp_expired_time' => 'hp_request_core', 'hp_view_count' => 'hp_request_core', 'hp_verified' => 'hp_vendor_core', 'hp_rating' => 'hp_vendor_core', 'hp_rating_count' => 'hp_vendor_core', 'hp_hourly_rate' => 'hp_vendor_core', 'hp_user' => 'hp_vendor_core', ); private function post_adapter(): TMDO_Entity_Adapter_Interface { $adapter = $this->createMock( TMDO_Entity_Adapter_Interface::class ); $adapter->method( 'get_entity_id_column' )->willReturn( 'post_id' ); return $adapter; } protected function setUp(): void { TMDO_Entity_Registry::init(); TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_HivePress_Entity_Groups::register_entity_fields(); } protected function tearDown(): void { unset( $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] ); unset( $GLOBALS['_wp_filter_callbacks']['tmdo_rest_default_post_type'] ); } public function test_definitions_match_persisted_schema_hashes(): void { foreach ( self::PERSISTED_HASHES as $id => $hash ) { list( $type, $group ) = explode( '/', $id ); $fields = TMDO_Entity_Registry::get_group_fields( $type, $group ); $this->assertNotEmpty( $fields, "{$id} not registered" ); $this->assertSame( $hash, substr( TMDO_Schema_Manager::calculate_schema_hash( $fields ), 0, 8 ), "{$id} drifted from the persisted definition" ); } } /** * HivePress filters and sorts listings by these; they are declared as * group indexes (added in place by core) rather than `searchable`, which * would change the pinned hash and rebuild the table. */ public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void { $this->assertSame( array( 'hourly_rate' => array( 'hp_hourly_rate' ), 'rating_count' => array( 'hp_rating_count' ), // Sorts: the value then the ID tie-break, read in order (2.4 PR-119). 'price_id' => array( 'hp_price', 'post_id' ), 'rating_id' => array( 'hp_rating', 'post_id' ), 'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ), ), TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' ) ); } public function test_site_listing_attributes_are_opt_in(): void { $this->assertSame( array(), TMDO_Entity_Registry::get_group_fields( 'post', 'hp_listing_attrs' ) ); } /** * A site flattens its own HivePress listing attributes (e.g. a "Warranty" * number attribute → hp_warranty) in a group of their own: a new table, * indexed at creation; hp_listing_core and its pinned hash stay as they are. */ public function test_site_listing_attributes_get_their_own_indexed_group(): void { $fields = static fn() => array( 'hp_warranty' => 'integer', 'hp_price' => 'decimal', // Already in hp_listing_core. 'warranty' => 'integer', // Not a HivePress key. 'hp_color' => 'colour', // Unknown type. 'hp_features' => 'json', // No plain index on LONGTEXT. ); add_filter( 'tmdo_hp_listing_attribute_fields', $fields ); $GLOBALS['_wp_did_action']['after_setup_theme'] = 1; try { TMDO_Entity_Registry::init(); TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_HivePress_Entity_Groups::register_entity_fields(); } finally { $GLOBALS['_wp_filter_callbacks']['tmdo_hp_listing_attribute_fields'] = array(); unset( $GLOBALS['_wp_did_action'] ); } $this->assertSame( array( 'hp_warranty' ), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ) ); $this->assertTrue( (bool) TMDO_Entity_Registry::get_group_fields( 'post', 'hp_listing_attrs' )[0]['searchable'] ); $this->assertSame( array( 'hp_listing' ), TMDO_Entity_Registry::get_group_post_types( 'post', 'hp_listing_attrs' ) ); $this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_price' )['group'] ); $this->test_definitions_match_persisted_schema_hashes(); } /** * Registration runs twice (plugins_loaded, then init:1): the group is * registered on the run after themes load, so a theme's filter counts and * a filter that changed between runs cannot collide (review F8). */ public function test_site_listing_attributes_are_registered_once_themes_are_loaded(): void { add_filter( 'tmdo_hp_listing_attribute_fields', static fn() => array( 'hp_warranty' => 'integer' ) ); try { TMDO_Entity_Registry::init(); TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_HivePress_Entity_Groups::register_entity_fields(); $this->assertSame( array(), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ), 'plugins_loaded run: not yet' ); $GLOBALS['_wp_did_action']['after_setup_theme'] = 1; TMDO_HivePress_Entity_Groups::register_entity_fields(); } finally { $GLOBALS['_wp_filter_callbacks']['tmdo_hp_listing_attribute_fields'] = array(); unset( $GLOBALS['_wp_did_action'] ); } $this->assertSame( array( 'hp_warranty' ), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ) ); $this->assertSame( array(), TMDO_Entity_Registry::get_collisions() ); } public function test_shared_keys_keep_their_current_owner(): void { foreach ( self::SHARED_KEY_OWNERS as $key => $group ) { $this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key )['group'], $key ); } } public function test_shared_keys_follow_the_post_type_when_scoped_routing_is_on(): void { $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '1'; foreach ( array( 'hp_listing' => 'hp_listing_core', 'hp_request' => 'hp_request_core', 'hp_vendor' => 'hp_vendor_core' ) as $post_type => $group ) { foreach ( TMDO_Entity_Registry::get_group_keys( 'post', $group ) as $key ) { $this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key, $post_type )['group'], "{$post_type}:{$key}" ); } } $this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'hp_rating', 'post' ), 'HivePress keys stay native on other post types' ); } /** * Fields only the Hot zone used to cover (ADR-003), as their own groups. */ private const HOT_ONLY_GROUPS = array( 'hp_draft_flags' => array( array( 'hp_listing', 'hp_request' ), array( 'hp_drafted' ) ), 'hp_booking_core' => array( array( 'hp_booking' ), array( 'hp_start_time', 'hp_end_time', 'hp_quantity' ) ), 'hp_membership_plan_core' => array( array( 'hp_membership_plan' ), array( 'hp_expire_period', 'hp_primary' ) ), 'hp_membership_core' => array( array( 'hp_membership' ), array( 'hp_expired_time' ) ), ); public function test_hot_only_fields_have_entity_groups_scoped_to_their_post_types(): void { foreach ( self::HOT_ONLY_GROUPS as $group => list( $post_types, $keys ) ) { $this->assertSame( $post_types, TMDO_Entity_Registry::get_group_post_types( 'post', $group ), $group ); $this->assertSame( $keys, TMDO_Entity_Registry::get_group_keys( 'post', $group ), $group ); } } public function test_hot_only_fields_route_to_their_group_when_scoped_routing_is_on(): void { $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '1'; foreach ( self::HOT_ONLY_GROUPS as $group => list( $post_types, $keys ) ) { foreach ( $post_types as $post_type ) { foreach ( $keys as $key ) { $this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key, $post_type )['group'], "{$post_type}:{$key}" ); } } } $this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_expired_time', 'hp_listing' )['group'] ); } public function test_hot_only_groups_add_no_collisions(): void { $this->assertSame( array(), TMDO_Entity_Registry::get_collisions() ); } public function test_reconciler_plans_the_misrouted_hivepress_keys(): void { $moves = array(); foreach ( TMDO_Post_Group_Reconciler::plan() as $m ) { $moves[] = "{$m['source']}>{$m['target']}:{$m['key']}"; } sort( $moves ); $this->assertSame( array( 'hp_request_core>hp_listing_core:hp_expired_time', 'hp_request_core>hp_listing_core:hp_status', 'hp_request_core>hp_listing_core:hp_view_count', 'hp_request_core>hp_membership_core:hp_expired_time', 'hp_vendor_core>hp_listing_core:hp_hourly_rate', 'hp_vendor_core>hp_listing_core:hp_rating', 'hp_vendor_core>hp_listing_core:hp_rating_count', 'hp_vendor_core>hp_listing_core:hp_verified', 'hp_vendor_core>hp_request_core:hp_user', ), $moves ); } public function test_rest_listings_default_to_hivepress_listings(): void { TMDO_HivePress_Entity_Groups::register(); $this->assertSame( 'hp_listing', TMDO_REST_API::default_post_type() ); } public function test_core_adapter_does_not_register_a_conflicting_group(): void { ( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields(); $this->assertSame( array(), TMDO_Entity_Registry::get_collisions() ); } }