From 544de4160c8072dd385ffcea7566c1e06c2b0314 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:10:02 +0100 Subject: [PATCH 01/14] Refactor to extract Abilities base class --- includes/abilities/abilities-integration.php | 1 + ...class-scf-internal-post-type-abilities.php | 793 ++++++++++++++++++ .../class-scf-post-type-abilities.php | 617 +------------- .../class-scf-taxonomy-abilities.php | 614 +------------- includes/class-scf-json-schema-validator.php | 4 +- .../test-scf-post-type-abilities.php | 64 +- .../abilities/test-scf-taxonomy-abilities.php | 88 +- 7 files changed, 884 insertions(+), 1297 deletions(-) create mode 100644 includes/abilities/class-scf-internal-post-type-abilities.php diff --git a/includes/abilities/abilities-integration.php b/includes/abilities/abilities-integration.php index 3a8e3d17..b25e6b1f 100644 --- a/includes/abilities/abilities-integration.php +++ b/includes/abilities/abilities-integration.php @@ -39,6 +39,7 @@ public function init() { return; } + acf_include( 'includes/abilities/class-scf-internal-post-type-abilities.php' ); acf_include( 'includes/abilities/class-scf-post-type-abilities.php' ); acf_include( 'includes/abilities/class-scf-taxonomy-abilities.php' ); } diff --git a/includes/abilities/class-scf-internal-post-type-abilities.php b/includes/abilities/class-scf-internal-post-type-abilities.php new file mode 100644 index 00000000..47eec8cf --- /dev/null +++ b/includes/abilities/class-scf-internal-post-type-abilities.php @@ -0,0 +1,793 @@ +internal_post_type ) ) { + _doing_it_wrong( __METHOD__, 'Child classes must set $internal_post_type property.', '6.7.0' ); + return; + } + + $validator = acf_get_instance( 'SCF_JSON_Schema_Validator' ); + if ( ! $validator->validate_required_schemas() ) { + return; + } + + add_action( 'wp_abilities_api_categories_init', array( $this, 'register_categories' ) ); + add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); + } + + /** + * Gets the internal post type instance. + * + * @return ACF_Internal_Post_Type + */ + private function instance() { + if ( null === $this->instance ) { + $this->instance = acf_get_internal_post_type_instance( $this->internal_post_type ); + } + return $this->instance; + } + + /** + * Gets entity name, e.g., 'post type', 'taxonomy'. + * + * @return string + */ + private function entity_name() { + return str_replace( '_', ' ', $this->instance()->hook_name ); + } + + /** + * Gets entity name plural, e.g., 'post types', 'taxonomies'. + * + * @return string + */ + private function entity_name_plural() { + return str_replace( '_', ' ', $this->instance()->hook_name_plural ); + } + + /** + * Gets schema name, e.g., 'post-type', 'taxonomy'. + * + * @return string + */ + private function schema_name() { + return str_replace( '_', '-', $this->instance()->hook_name ); + } + + /** + * Gets ability category, e.g., 'scf-post-types', 'scf-taxonomies'. + * + * @return string + */ + private function ability_category() { + return 'scf-' . str_replace( '_', '-', $this->instance()->hook_name_plural ); + } + + /** + * Gets ability name for an action. + * + * @param string $action E.g., 'list', 'get', 'create'. + * @return string E.g., 'scf/list-post-types', 'scf/get-post-type'. + */ + private function ability_name( $action ) { + $slug = str_replace( '_', '-', 'list' === $action ? $this->instance()->hook_name_plural : $this->instance()->hook_name ); + return 'scf/' . $action . '-' . $slug; + } + + // Schema methods. + + /** + * Gets the entity schema from JSON schema file. + * + * @return array + */ + private function get_entity_schema() { + if ( null === $this->entity_schema ) { + $validator = new SCF_JSON_Schema_Validator(); + $schema = $validator->load_schema( $this->schema_name() ); + + // Convert hook_name to camelCase for schema definition key (post_type → postType). + $def_key = lcfirst( str_replace( ' ', '', ucwords( $this->entity_name() ) ) ); + $this->entity_schema = json_decode( wp_json_encode( $schema->definitions->$def_key ), true ); + } + return $this->entity_schema; + } + + /** + * Gets the SCF identifier schema. + * + * @return array + */ + private function get_scf_identifier_schema() { + if ( null === $this->scf_identifier_schema ) { + $validator = new SCF_JSON_Schema_Validator(); + $this->scf_identifier_schema = json_decode( wp_json_encode( $validator->load_schema( 'scf-identifier' ) ), true ); + } + return $this->scf_identifier_schema; + } + + /** + * Gets the internal fields schema. + * + * @return array + */ + private function get_internal_fields_schema() { + $validator = new SCF_JSON_Schema_Validator(); + $schema = $validator->load_schema( 'internal-fields' ); + return json_decode( wp_json_encode( $schema->definitions->internalFields ), true ); + } + + /** + * Gets the entity schema merged with internal fields. + * + * @return array + */ + private function get_entity_with_internal_fields_schema() { + $schema = $this->get_entity_schema(); + $internal = $this->get_internal_fields_schema(); + $schema['properties'] = array_merge( $schema['properties'], $internal['properties'] ); + return $schema; + } + + // Registration. + + /** + * Registers the ability category. + * + * @return void + */ + public function register_categories() { + wp_register_ability_category( + $this->ability_category(), + array( + 'label' => sprintf( + /* translators: %s: Entity type plural, e.g., 'Post Types' */ + __( 'SCF %s', 'secure-custom-fields' ), + ucwords( $this->entity_name_plural() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type plural, e.g., 'post types' */ + __( 'Abilities for managing Secure Custom Fields %s.', 'secure-custom-fields' ), + $this->entity_name_plural() + ), + ) + ); + } + + /** + * Registers all abilities for this entity type. + * + * @return void + */ + public function register_abilities() { + $this->register_list_ability(); + $this->register_get_ability(); + $this->register_create_ability(); + $this->register_update_ability(); + $this->register_delete_ability(); + $this->register_duplicate_ability(); + $this->register_export_ability(); + $this->register_import_ability(); + } + + /** + * Registers the list ability. + * + * @return void + */ + private function register_list_ability() { + wp_register_ability( + $this->ability_name( 'list' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type plural */ + __( 'List %s', 'secure-custom-fields' ), + ucwords( $this->entity_name_plural() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type plural */ + __( 'Retrieves a list of all SCF %s with optional filtering.', 'secure-custom-fields' ), + $this->entity_name_plural() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'list_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'filter' => array( + 'type' => 'object', + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Optional filters to apply to the %s list.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'properties' => array( + 'active' => array( + 'type' => 'boolean', + 'description' => __( 'Filter by active status.', 'secure-custom-fields' ), + ), + ), + ), + ), + ), + 'output_schema' => array( + 'type' => 'array', + 'items' => $this->get_entity_with_internal_fields_schema(), + ), + ) + ); + } + + /** + * Registers the get ability. + * + * @return void + */ + private function register_get_ability() { + wp_register_ability( + $this->ability_name( 'get' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Get %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Retrieves a specific SCF %s configuration by ID or key.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'get_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'identifier' => $this->get_scf_identifier_schema(), + ), + 'required' => array( 'identifier' ), + ), + 'output_schema' => $this->get_entity_with_internal_fields_schema(), + ) + ); + } + + /** + * Registers the create ability. + * + * @return void + */ + private function register_create_ability() { + wp_register_ability( + $this->ability_name( 'create' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Create %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Creates a new custom %s in SCF with the provided configuration.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'create_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => false, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => $this->get_entity_schema(), + 'output_schema' => $this->get_entity_with_internal_fields_schema(), + ) + ); + } + + /** + * Registers the update ability. + * + * @return void + */ + private function register_update_ability() { + $input_schema = $this->get_entity_with_internal_fields_schema(); + $input_schema['required'] = array( 'ID' ); + + wp_register_ability( + $this->ability_name( 'update' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Update %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Updates an existing SCF %s with new configuration.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'update_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => $input_schema, + 'output_schema' => $this->get_entity_with_internal_fields_schema(), + ) + ); + } + + /** + * Registers the delete ability. + * + * @return void + */ + private function register_delete_ability() { + wp_register_ability( + $this->ability_name( 'delete' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Delete %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Permanently deletes an SCF %s. This action cannot be undone.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'delete_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => true, + 'idempotent' => true, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'identifier' => $this->get_scf_identifier_schema(), + ), + 'required' => array( 'identifier' ), + ), + 'output_schema' => array( + 'type' => 'boolean', + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'True if %s was successfully deleted.', 'secure-custom-fields' ), + $this->entity_name() + ), + ), + ) + ); + } + + /** + * Registers the duplicate ability. + * + * @return void + */ + private function register_duplicate_ability() { + wp_register_ability( + $this->ability_name( 'duplicate' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Duplicate %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Creates a copy of an existing SCF %s. The duplicate receives a new unique key.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'duplicate_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => false, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'identifier' => $this->get_scf_identifier_schema(), + 'new_post_id' => array( + 'type' => 'integer', + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Optional new post ID for the duplicated %s.', 'secure-custom-fields' ), + $this->entity_name() + ), + ), + ), + 'required' => array( 'identifier' ), + ), + 'output_schema' => $this->get_entity_with_internal_fields_schema(), + ) + ); + } + + /** + * Registers the export ability. + * + * @return void + */ + private function register_export_ability() { + wp_register_ability( + $this->ability_name( 'export' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Export %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Exports an SCF %s configuration as JSON for backup or transfer.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'export_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'identifier' => $this->get_scf_identifier_schema(), + ), + 'required' => array( 'identifier' ), + ), + 'output_schema' => $this->get_entity_schema(), + ) + ); + } + + /** + * Registers the import ability. + * + * @return void + */ + private function register_import_ability() { + wp_register_ability( + $this->ability_name( 'import' ), + array( + 'label' => sprintf( + /* translators: %s: Entity type */ + __( 'Import %s', 'secure-custom-fields' ), + ucwords( $this->entity_name() ) + ), + 'description' => sprintf( + /* translators: %s: Entity type */ + __( 'Imports an SCF %s from JSON configuration data.', 'secure-custom-fields' ), + $this->entity_name() + ), + 'category' => $this->ability_category(), + 'execute_callback' => array( $this, 'import_callback' ), + 'meta' => array( + 'show_in_rest' => true, + 'mcp' => array( 'public' => true ), + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => false, + ), + ), + 'permission_callback' => 'scf_current_user_has_capability', + 'input_schema' => $this->get_entity_with_internal_fields_schema(), + 'output_schema' => $this->get_entity_with_internal_fields_schema(), + ) + ); + } + + // Callbacks. + + /** + * Handles the list ability callback. + * + * @param array $input The input parameters. + * @return array List of entities. + */ + public function list_callback( $input ) { + $filter = isset( $input['filter'] ) ? $input['filter'] : array(); + return $this->instance()->filter_posts( $this->instance()->get_posts(), $filter ); + } + + /** + * Handles the get ability callback. + * + * @param array $input The input parameters. + * @return array|WP_Error Entity data or error if not found. + */ + public function get_callback( $input ) { + $entity = $this->instance()->get_post( $input['identifier'] ); + if ( ! $entity ) { + return $this->not_found_error(); + } + return $entity; + } + + /** + * Handles the create ability callback. + * + * @param array $input The entity data to create. + * @return array|WP_Error Created entity or error on failure. + */ + public function create_callback( $input ) { + if ( $this->instance()->get_post( $input['key'] ) ) { + return new WP_Error( + $this->instance()->hook_name . '_exists', + sprintf( + /* translators: %s: Entity type */ + __( 'A %s with this key already exists.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + + $entity = $this->instance()->update_post( $input ); + if ( ! $entity ) { + return new WP_Error( + 'create_' . $this->instance()->hook_name . '_failed', + sprintf( + /* translators: %s: Entity type */ + __( 'Failed to create %s.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + return $entity; + } + + /** + * Handles the update ability callback. + * + * @param array $input The entity data to update. + * @return array|WP_Error Updated entity or error on failure. + */ + public function update_callback( $input ) { + $existing = $this->instance()->get_post( $input['ID'] ); + if ( ! $existing ) { + return $this->not_found_error(); + } + + $entity = $this->instance()->update_post( array_merge( $existing, $input ) ); + if ( ! $entity ) { + return new WP_Error( + 'update_' . $this->instance()->hook_name . '_failed', + sprintf( + /* translators: %s: Entity type */ + __( 'Failed to update %s.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + return $entity; + } + + /** + * Handles the delete ability callback. + * + * @param array $input The input parameters. + * @return bool|WP_Error True on success or error on failure. + */ + public function delete_callback( $input ) { + if ( ! $this->instance()->get_post( $input['identifier'] ) ) { + return $this->not_found_error(); + } + + if ( ! $this->instance()->delete_post( $input['identifier'] ) ) { + return new WP_Error( + 'delete_' . $this->instance()->hook_name . '_failed', + sprintf( + /* translators: %s: Entity type */ + __( 'Failed to delete %s.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + return true; + } + + /** + * Handles the duplicate ability callback. + * + * @param array $input The input parameters. + * @return array|WP_Error Duplicated entity or error on failure. + */ + public function duplicate_callback( $input ) { + if ( ! $this->instance()->get_post( $input['identifier'] ) ) { + return $this->not_found_error(); + } + + $new_post_id = isset( $input['new_post_id'] ) ? $input['new_post_id'] : 0; + $duplicated = $this->instance()->duplicate_post( $input['identifier'], $new_post_id ); + + if ( ! $duplicated ) { + return new WP_Error( + 'duplicate_' . $this->instance()->hook_name . '_failed', + sprintf( + /* translators: %s: Entity type */ + __( 'Failed to duplicate %s.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + return $duplicated; + } + + /** + * Handles the export ability callback. + * + * @param array $input The input parameters. + * @return array|WP_Error Exported entity data or error on failure. + */ + public function export_callback( $input ) { + $entity = $this->instance()->get_post( $input['identifier'] ); + if ( ! $entity ) { + return $this->not_found_error(); + } + + $export = $this->instance()->prepare_post_for_export( $entity ); + if ( ! $export ) { + return new WP_Error( + 'export_' . $this->instance()->hook_name . '_failed', + sprintf( + /* translators: %s: Entity type */ + __( 'Failed to prepare %s for export.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + return $export; + } + + /** + * Handles the import ability callback. + * + * @param array|object $input The entity data to import. + * @return array|WP_Error Imported entity or error on failure. + */ + public function import_callback( $input ) { + $imported = $this->instance()->import_post( $input ); + if ( ! $imported ) { + return new WP_Error( + 'import_' . $this->instance()->hook_name . '_failed', + sprintf( + /* translators: %s: Entity type */ + __( 'Failed to import %s.', 'secure-custom-fields' ), + $this->entity_name() + ) + ); + } + return $imported; + } + + /** + * Creates a not found error. + * + * @return WP_Error + */ + private function not_found_error() { + return new WP_Error( + $this->instance()->hook_name . '_not_found', + sprintf( + /* translators: %s: Entity type */ + __( '%s not found.', 'secure-custom-fields' ), + ucfirst( $this->entity_name() ) + ), + array( 'status' => 404 ) + ); + } + } + +endif; diff --git a/includes/abilities/class-scf-post-type-abilities.php b/includes/abilities/class-scf-post-type-abilities.php index cc180f02..0e40fd07 100644 --- a/includes/abilities/class-scf-post-type-abilities.php +++ b/includes/abilities/class-scf-post-type-abilities.php @@ -24,626 +24,17 @@ * * @since 6.6.0 */ - class SCF_Post_Type_Abilities { + class SCF_Post_Type_Abilities extends SCF_Internal_Post_Type_Abilities { /** - * Post type schema to reuse across ability registrations. + * The internal post type identifier. * - * @var object|null + * @var string */ - private $post_type_schema = null; - - /** - * SCF identifier schema to reuse across ability registrations. - * - * @var object|null - */ - private $scf_identifier_schema = null; - - /** - * Constructor. - * - * @since 6.6.0 - */ - public function __construct() { - $validator = acf_get_instance( 'SCF_JSON_Schema_Validator' ); - - // Only register abilities if schemas are available - if ( ! $validator->validate_required_schemas() ) { - return; - } - - add_action( 'wp_abilities_api_categories_init', array( $this, 'register_categories' ) ); - add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); - } - - /** - * Get the SCF post type schema, loading it once and caching for reuse. - * - * @since 6.6.0 - * @return array The post type schema definition. - */ - private function get_post_type_schema() { - if ( null === $this->post_type_schema ) { - $validator = new SCF_JSON_Schema_Validator(); - $schema = $validator->load_schema( 'post-type' ); - - $this->post_type_schema = json_decode( wp_json_encode( $schema->definitions->postType ), true ); - } - - return $this->post_type_schema; - } - - /** - * Get the SCF identifier schema, loading it once and caching for reuse. - * - * @since 6.6.0 - * - * @return array The SCF identifier schema definition. - */ - private function get_scf_identifier_schema() { - if ( null === $this->scf_identifier_schema ) { - $validator = new SCF_JSON_Schema_Validator(); - - $this->scf_identifier_schema = json_decode( wp_json_encode( $validator->load_schema( 'scf-identifier' ) ), true ); - } - - return $this->scf_identifier_schema; - } - - /** - * Get the internal fields schema (ID, _valid, local). - * - * @since 6.6.0 - * @return array The internal fields schema. - */ - private function get_internal_fields_schema() { - $validator = new SCF_JSON_Schema_Validator(); - $schema = $validator->load_schema( 'internal-fields' ); - - return json_decode( wp_json_encode( $schema->definitions->internalFields ), true ); - } - - /** - * Get the post type schema extended with internal fields for GET/LIST/CREATE/UPDATE/IMPORT/DUPLICATE operations. - * - * @since 6.6.0 - * - * @return array The extended post type schema with internal fields. - */ - private function get_post_type_with_internal_fields_schema() { - $schema = $this->get_post_type_schema(); - $internal_fields = $this->get_internal_fields_schema(); - $schema['properties'] = array_merge( $schema['properties'], $internal_fields['properties'] ); - - return $schema; - } - - /** - * Register SCF ability categories. - * - * @since 6.6.0 - */ - public function register_categories() { - wp_register_ability_category( - 'scf-post-types', - array( - 'label' => __( 'SCF Post Types', 'secure-custom-fields' ), - 'description' => __( 'Abilities for managing Secure Custom Fields post types.', 'secure-custom-fields' ), - ) - ); - } - - /** - * Register all post type abilities. - * - * @since 6.6.0 - */ - public function register_abilities() { - $this->register_list_post_types_ability(); - $this->register_get_post_type_ability(); - $this->register_create_post_type_ability(); - $this->register_update_post_type_ability(); - $this->register_delete_post_type_ability(); - $this->register_duplicate_post_type_ability(); - $this->register_export_post_type_ability(); - $this->register_import_post_type_ability(); - } - - /** - * Register the list post types ability. - * - * @since 6.6.0 - */ - private function register_list_post_types_ability() { - wp_register_ability( - 'scf/list-post-types', - array( - 'label' => __( 'List Post Types', 'secure-custom-fields' ), - 'description' => __( 'Retrieves a list of all SCF post types with optional filtering.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'list_post_types_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'filter' => array( - 'type' => 'object', - 'description' => __( 'Optional filters to apply to the post type list.', 'secure-custom-fields' ), - 'properties' => array( - 'active' => array( - 'type' => 'boolean', - 'description' => __( 'Filter by active status.', 'secure-custom-fields' ), - ), - 'search' => array( - 'type' => 'string', - 'description' => __( 'Search term to filter post types.', 'secure-custom-fields' ), - ), - ), - ), - ), - ), - 'output_schema' => array( - 'type' => 'array', - 'items' => $this->get_post_type_with_internal_fields_schema(), - ), - ) - ); - } - - /** - * Register the get post type ability. - * - * @since 6.6.0 - */ - private function register_get_post_type_ability() { - wp_register_ability( - 'scf/get-post-type', - array( - 'label' => __( 'Get Post Type', 'secure-custom-fields' ), - 'description' => __( 'Retrieves a specific SCF post type configuration by ID or key.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'get_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => $this->get_post_type_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the create post type ability. - * - * @since 6.6.0 - */ - private function register_create_post_type_ability() { - $input_schema = $this->get_post_type_schema(); - - wp_register_ability( - 'scf/create-post-type', - array( - 'label' => __( 'Create Post Type', 'secure-custom-fields' ), - 'description' => __( 'Creates a new custom post type in SCF with the provided configuration.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'create_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => $input_schema, - 'output_schema' => $this->get_post_type_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the update post type ability. - * - * @since 6.6.0 - */ - private function register_update_post_type_ability() { - - // For updates, only ID is required, everything else is optional - $input_schema = $this->get_post_type_with_internal_fields_schema(); - $input_schema['required'] = array( 'ID' ); - - wp_register_ability( - 'scf/update-post-type', - array( - 'label' => __( 'Update Post Type', 'secure-custom-fields' ), - 'description' => __( 'Updates an existing SCF post type with new configuration.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'update_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => $input_schema, - 'output_schema' => $this->get_post_type_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the delete post type ability. - * - * @since 6.6.0 - */ - private function register_delete_post_type_ability() { - wp_register_ability( - 'scf/delete-post-type', - array( - 'label' => __( 'Delete Post Type', 'secure-custom-fields' ), - 'description' => __( 'Permanently deletes an SCF post type. This action cannot be undone.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'delete_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => true, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => array( - 'type' => 'boolean', - 'description' => __( 'True if post type was successfully deleted.', 'secure-custom-fields' ), - ), - ) - ); - } - - /** - * Register the duplicate post type ability. - * - * @since 6.6.0 - */ - private function register_duplicate_post_type_ability() { - wp_register_ability( - 'scf/duplicate-post-type', - array( - 'label' => __( 'Duplicate Post Type', 'secure-custom-fields' ), - 'description' => __( 'Creates a copy of an existing SCF post type. The duplicate receives a new unique key but retains the same post_type slug, so it will not register until the slug is changed.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'duplicate_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - 'new_post_id' => array( - 'type' => 'integer', - 'description' => __( 'Optional new post ID for the duplicated post type.', 'secure-custom-fields' ), - ), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => $this->get_post_type_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the export post type ability. - * - * @since 6.6.0 - */ - private function register_export_post_type_ability() { - wp_register_ability( - 'scf/export-post-type', - array( - 'label' => __( 'Export Post Type', 'secure-custom-fields' ), - 'description' => __( 'Exports an SCF post type configuration as JSON for backup or transfer.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'export_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => $this->get_post_type_schema(), - ) - ); - } - - /** - * Register the import post type ability. - * - * @since 6.6.0 - */ - private function register_import_post_type_ability() { - wp_register_ability( - 'scf/import-post-type', - array( - 'label' => __( 'Import Post Type', 'secure-custom-fields' ), - 'description' => __( 'Imports an SCF post type from JSON configuration data.', 'secure-custom-fields' ), - 'category' => 'scf-post-types', - 'execute_callback' => array( $this, 'import_post_type_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => $this->get_post_type_with_internal_fields_schema(), - 'output_schema' => $this->get_post_type_with_internal_fields_schema(), - ) - ); - } - - /** - * Callback for the list post types ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return array The response data. - */ - public function list_post_types_callback( $input ) { - $filter = isset( $input['filter'] ) ? $input['filter'] : array(); - - $post_types = acf_get_acf_post_types( $filter ); - return is_array( $post_types ) ? $post_types : array(); - } - - /** - * Callback for the get post type ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return array The response data. - */ - public function get_post_type_callback( $input ) { - $post_type = acf_get_post_type( $input['identifier'] ); - - if ( ! $post_type ) { - return $this->post_type_not_found_error(); - } - - return $post_type; - } - - /** - * Callback for the create post type ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return array The response data. - */ - public function create_post_type_callback( $input ) { - // Check if post type already exists. - if ( acf_get_post_type( $input['key'] ) ) { - return new WP_Error( 'post_type_exists', __( 'A post type with this key already exists.', 'secure-custom-fields' ) ); - } - - $post_type = acf_update_post_type( $input ); - - if ( ! $post_type ) { - return new WP_Error( 'create_post_type_failed', __( 'Failed to create post type.', 'secure-custom-fields' ) ); - } - - return $post_type; - } - - /** - * Callback for the update post type ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The post type data on success, WP_Error on failure. - */ - public function update_post_type_callback( $input ) { - $existing_post_type = acf_get_post_type( $input['ID'] ); - if ( ! $existing_post_type ) { - return $this->post_type_not_found_error(); - } - - // Merge input with existing post type data to preserve unmodified fields. - $input = array_merge( $existing_post_type, $input ); - - $post_type = acf_update_post_type( $input ); - - if ( ! $post_type ) { - return new WP_Error( 'update_post_type_failed', __( 'Failed to update post type.', 'secure-custom-fields' ) ); - } - - return $post_type; - } - - /** - * Callback for the delete post type ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return bool|WP_Error True on success, WP_Error on failure. - */ - public function delete_post_type_callback( $input ) { - $post_type = acf_get_post_type( $input['identifier'] ); - if ( ! $post_type ) { - return $this->post_type_not_found_error(); - } - - $result = acf_delete_post_type( $input['identifier'] ); - - if ( ! $result ) { - return new WP_Error( 'delete_post_type_failed', __( 'Failed to delete post type.', 'secure-custom-fields' ) ); - } - - return true; - } - - /** - * Callback for the duplicate post type ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The duplicated post type data on success, WP_Error on failure. - */ - public function duplicate_post_type_callback( $input ) { - $post_type = acf_get_post_type( $input['identifier'] ); - if ( ! $post_type ) { - return $this->post_type_not_found_error(); - } - - $new_post_id = isset( $input['new_post_id'] ) ? $input['new_post_id'] : 0; - $duplicated_post_type = acf_duplicate_post_type( $input['identifier'], $new_post_id ); - - if ( ! $duplicated_post_type ) { - return new WP_Error( 'duplicate_post_type_failed', __( 'Failed to duplicate post type.', 'secure-custom-fields' ) ); - } - - return $duplicated_post_type; - } - - /** - * Callback for the export post type ability. - * - * @since 6.6.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The export data on success, WP_Error on failure. - */ - public function export_post_type_callback( $input ) { - $post_type = acf_get_post_type( $input['identifier'] ); - if ( ! $post_type ) { - return $this->post_type_not_found_error(); - } - - $export_data = acf_prepare_internal_post_type_for_export( $post_type, 'acf-post-type' ); - - if ( ! $export_data ) { - return new WP_Error( 'export_post_type_failed', __( 'Failed to prepare post type for export.', 'secure-custom-fields' ) ); - } - - return $export_data; - } - - /** - * Callback for the import post type ability. - * - * @since 6.6.0 - * - * @param array|object $input The input parameters. - * @return array|WP_Error The imported post type data on success, WP_Error on failure. - */ - public function import_post_type_callback( $input ) { - // Import the post type (handles both create and update based on presence of ID). - $imported_post_type = acf_import_internal_post_type( $input, 'acf-post-type' ); - - if ( ! $imported_post_type ) { - return new WP_Error( 'import_post_type_failed', __( 'Failed to import post type.', 'secure-custom-fields' ) ); - } - - return $imported_post_type; - } - - /** - * Returns a WP_Error for post type not found. - * - * @return WP_Error The error object with 404 status. - */ - private function post_type_not_found_error() { - return new WP_Error( - 'post_type_not_found', - __( 'Post type not found.', 'secure-custom-fields' ), - array( 'status' => 404 ) - ); - } + protected $internal_post_type = 'acf-post-type'; } // Initialize abilities instance. acf_new_instance( 'SCF_Post_Type_Abilities' ); - endif; // class_exists check diff --git a/includes/abilities/class-scf-taxonomy-abilities.php b/includes/abilities/class-scf-taxonomy-abilities.php index a622167d..764a52e3 100644 --- a/includes/abilities/class-scf-taxonomy-abilities.php +++ b/includes/abilities/class-scf-taxonomy-abilities.php @@ -24,623 +24,17 @@ * * @since 6.7.0 */ - class SCF_Taxonomy_Abilities { + class SCF_Taxonomy_Abilities extends SCF_Internal_Post_Type_Abilities { /** - * Taxonomy schema to reuse across ability registrations. + * The internal post type identifier. * - * @var array|null + * @var string */ - private $taxonomy_schema = null; - - /** - * SCF identifier schema to reuse across ability registrations. - * - * @var array|null - */ - private $scf_identifier_schema = null; - - /** - * Constructor. - * - * @since 6.7.0 - */ - public function __construct() { - $validator = acf_get_instance( 'SCF_JSON_Schema_Validator' ); - - // Only register abilities if schemas are available. - if ( ! $validator->validate_required_schemas() ) { - return; - } - - add_action( 'wp_abilities_api_categories_init', array( $this, 'register_categories' ) ); - add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); - } - - /** - * Get the SCF taxonomy schema, loading it once and caching for reuse. - * - * @since 6.7.0 - * @return array The taxonomy schema definition. - */ - private function get_taxonomy_schema() { - if ( null === $this->taxonomy_schema ) { - $validator = new SCF_JSON_Schema_Validator(); - $schema = $validator->load_schema( 'taxonomy' ); - - $this->taxonomy_schema = json_decode( wp_json_encode( $schema->definitions->taxonomy ), true ); - } - - return $this->taxonomy_schema; - } - - /** - * Get the SCF identifier schema, loading it once and caching for reuse. - * - * @since 6.7.0 - * - * @return array The SCF identifier schema definition. - */ - private function get_scf_identifier_schema() { - if ( null === $this->scf_identifier_schema ) { - $validator = new SCF_JSON_Schema_Validator(); - - $this->scf_identifier_schema = json_decode( wp_json_encode( $validator->load_schema( 'scf-identifier' ) ), true ); - } - - return $this->scf_identifier_schema; - } - - /** - * Get the internal fields schema (ID, _valid, local). - * - * @since 6.7.0 - * @return array The internal fields schema. - */ - private function get_internal_fields_schema() { - $validator = new SCF_JSON_Schema_Validator(); - $schema = $validator->load_schema( 'internal-fields' ); - - return json_decode( wp_json_encode( $schema->definitions->internalFields ), true ); - } - - /** - * Get the taxonomy schema extended with internal fields for GET/LIST/CREATE/UPDATE/IMPORT/DUPLICATE operations. - * - * @since 6.7.0 - * - * @return array The extended taxonomy schema with internal fields. - */ - private function get_taxonomy_with_internal_fields_schema() { - $schema = $this->get_taxonomy_schema(); - $internal_fields = $this->get_internal_fields_schema(); - $schema['properties'] = array_merge( $schema['properties'], $internal_fields['properties'] ); - - return $schema; - } - - /** - * Register SCF ability categories. - * - * @since 6.7.0 - */ - public function register_categories() { - wp_register_ability_category( - 'scf-taxonomies', - array( - 'label' => __( 'SCF Taxonomies', 'secure-custom-fields' ), - 'description' => __( 'Abilities for managing Secure Custom Fields taxonomies.', 'secure-custom-fields' ), - ) - ); - } - - /** - * Register all taxonomy abilities. - * - * @since 6.7.0 - */ - public function register_abilities() { - $this->register_list_taxonomies_ability(); - $this->register_get_taxonomy_ability(); - $this->register_create_taxonomy_ability(); - $this->register_update_taxonomy_ability(); - $this->register_delete_taxonomy_ability(); - $this->register_duplicate_taxonomy_ability(); - $this->register_export_taxonomy_ability(); - $this->register_import_taxonomy_ability(); - } - - /** - * Register the list taxonomies ability. - * - * @since 6.7.0 - */ - private function register_list_taxonomies_ability() { - wp_register_ability( - 'scf/list-taxonomies', - array( - 'label' => __( 'List Taxonomies', 'secure-custom-fields' ), - 'description' => __( 'Retrieves a list of all SCF taxonomies with optional filtering.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'list_taxonomies_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'filter' => array( - 'type' => 'object', - 'description' => __( 'Optional filters to apply to the taxonomy list.', 'secure-custom-fields' ), - 'properties' => array( - 'active' => array( - 'type' => 'boolean', - 'description' => __( 'Filter by active status.', 'secure-custom-fields' ), - ), - ), - ), - ), - ), - 'output_schema' => array( - 'type' => 'array', - 'items' => $this->get_taxonomy_with_internal_fields_schema(), - ), - ) - ); - } - - /** - * Register the get taxonomy ability. - * - * @since 6.7.0 - */ - private function register_get_taxonomy_ability() { - wp_register_ability( - 'scf/get-taxonomy', - array( - 'label' => __( 'Get Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Retrieves a specific SCF taxonomy configuration by ID or key.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'get_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => $this->get_taxonomy_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the create taxonomy ability. - * - * @since 6.7.0 - */ - private function register_create_taxonomy_ability() { - $input_schema = $this->get_taxonomy_schema(); - - wp_register_ability( - 'scf/create-taxonomy', - array( - 'label' => __( 'Create Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Creates a new custom taxonomy in SCF with the provided configuration.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'create_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => $input_schema, - 'output_schema' => $this->get_taxonomy_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the update taxonomy ability. - * - * @since 6.7.0 - */ - private function register_update_taxonomy_ability() { - - // For updates, only ID is required, everything else is optional. - $input_schema = $this->get_taxonomy_with_internal_fields_schema(); - $input_schema['required'] = array( 'ID' ); - - wp_register_ability( - 'scf/update-taxonomy', - array( - 'label' => __( 'Update Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Updates an existing SCF taxonomy with new configuration.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'update_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => $input_schema, - 'output_schema' => $this->get_taxonomy_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the delete taxonomy ability. - * - * @since 6.7.0 - */ - private function register_delete_taxonomy_ability() { - wp_register_ability( - 'scf/delete-taxonomy', - array( - 'label' => __( 'Delete Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Permanently deletes an SCF taxonomy. This action cannot be undone.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'delete_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => true, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => array( - 'type' => 'boolean', - 'description' => __( 'True if taxonomy was successfully deleted.', 'secure-custom-fields' ), - ), - ) - ); - } - - /** - * Register the duplicate taxonomy ability. - * - * @since 6.7.0 - */ - private function register_duplicate_taxonomy_ability() { - wp_register_ability( - 'scf/duplicate-taxonomy', - array( - 'label' => __( 'Duplicate Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Creates a copy of an existing SCF taxonomy. The duplicate receives a new unique key but retains the same taxonomy slug, so it will not register until the slug is changed.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'duplicate_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - 'new_post_id' => array( - 'type' => 'integer', - 'description' => __( 'Optional new post ID for the duplicated taxonomy.', 'secure-custom-fields' ), - ), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => $this->get_taxonomy_with_internal_fields_schema(), - ) - ); - } - - /** - * Register the export taxonomy ability. - * - * @since 6.7.0 - */ - private function register_export_taxonomy_ability() { - wp_register_ability( - 'scf/export-taxonomy', - array( - 'label' => __( 'Export Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Exports an SCF taxonomy configuration as JSON for backup or transfer.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'export_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => array( - 'type' => 'object', - 'properties' => array( - 'identifier' => $this->get_scf_identifier_schema(), - ), - 'required' => array( 'identifier' ), - ), - 'output_schema' => $this->get_taxonomy_schema(), - ) - ); - } - - /** - * Register the import taxonomy ability. - * - * @since 6.7.0 - */ - private function register_import_taxonomy_ability() { - wp_register_ability( - 'scf/import-taxonomy', - array( - 'label' => __( 'Import Taxonomy', 'secure-custom-fields' ), - 'description' => __( 'Imports an SCF taxonomy from JSON configuration data.', 'secure-custom-fields' ), - 'category' => 'scf-taxonomies', - 'execute_callback' => array( $this, 'import_taxonomy_callback' ), - 'meta' => array( - 'show_in_rest' => true, - 'mcp' => array( - 'public' => true, - ), - 'annotations' => array( - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ), - ), - 'permission_callback' => 'scf_current_user_has_capability', - 'input_schema' => $this->get_taxonomy_with_internal_fields_schema(), - 'output_schema' => $this->get_taxonomy_with_internal_fields_schema(), - ) - ); - } - - /** - * Callback for the list taxonomies ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return array The response data. - */ - public function list_taxonomies_callback( $input ) { - $filter = isset( $input['filter'] ) ? $input['filter'] : array(); - - $taxonomies = acf_get_acf_taxonomies( $filter ); - return is_array( $taxonomies ) ? $taxonomies : array(); - } - - /** - * Callback for the get taxonomy ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The taxonomy data on success, WP_Error on failure. - */ - public function get_taxonomy_callback( $input ) { - $taxonomy = acf_get_taxonomy( $input['identifier'] ); - - if ( ! $taxonomy ) { - return $this->taxonomy_not_found_error(); - } - - return $taxonomy; - } - - /** - * Callback for the create taxonomy ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The taxonomy data on success, WP_Error on failure. - */ - public function create_taxonomy_callback( $input ) { - // Check if taxonomy already exists. - if ( acf_get_taxonomy( $input['key'] ) ) { - return new WP_Error( 'taxonomy_exists', __( 'A taxonomy with this key already exists.', 'secure-custom-fields' ) ); - } - - $taxonomy = acf_update_taxonomy( $input ); - - if ( ! $taxonomy ) { - return new WP_Error( 'create_taxonomy_failed', __( 'Failed to create taxonomy.', 'secure-custom-fields' ) ); - } - - return $taxonomy; - } - - /** - * Callback for the update taxonomy ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The taxonomy data on success, WP_Error on failure. - */ - public function update_taxonomy_callback( $input ) { - $existing_taxonomy = acf_get_taxonomy( $input['ID'] ); - if ( ! $existing_taxonomy ) { - return $this->taxonomy_not_found_error(); - } - - // Merge input with existing taxonomy data to preserve unmodified fields. - $input = array_merge( $existing_taxonomy, $input ); - - $taxonomy = acf_update_taxonomy( $input ); - - if ( ! $taxonomy ) { - return new WP_Error( 'update_taxonomy_failed', __( 'Failed to update taxonomy.', 'secure-custom-fields' ) ); - } - - return $taxonomy; - } - - /** - * Callback for the delete taxonomy ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return bool|WP_Error True on success, WP_Error on failure. - */ - public function delete_taxonomy_callback( $input ) { - $taxonomy = acf_get_taxonomy( $input['identifier'] ); - if ( ! $taxonomy ) { - return $this->taxonomy_not_found_error(); - } - - $result = acf_delete_taxonomy( $input['identifier'] ); - - if ( ! $result ) { - return new WP_Error( 'delete_taxonomy_failed', __( 'Failed to delete taxonomy.', 'secure-custom-fields' ) ); - } - - return true; - } - - /** - * Callback for the duplicate taxonomy ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The duplicated taxonomy data on success, WP_Error on failure. - */ - public function duplicate_taxonomy_callback( $input ) { - $taxonomy = acf_get_taxonomy( $input['identifier'] ); - if ( ! $taxonomy ) { - return $this->taxonomy_not_found_error(); - } - - $new_post_id = isset( $input['new_post_id'] ) ? $input['new_post_id'] : 0; - $duplicated_taxonomy = acf_duplicate_taxonomy( $input['identifier'], $new_post_id ); - - if ( ! $duplicated_taxonomy ) { - return new WP_Error( 'duplicate_taxonomy_failed', __( 'Failed to duplicate taxonomy.', 'secure-custom-fields' ) ); - } - - return $duplicated_taxonomy; - } - - /** - * Callback for the export taxonomy ability. - * - * @since 6.7.0 - * - * @param array $input The input parameters. - * @return array|WP_Error The export data on success, WP_Error on failure. - */ - public function export_taxonomy_callback( $input ) { - $taxonomy = acf_get_taxonomy( $input['identifier'] ); - if ( ! $taxonomy ) { - return $this->taxonomy_not_found_error(); - } - - $export_data = acf_prepare_internal_post_type_for_export( $taxonomy, 'acf-taxonomy' ); - - if ( ! $export_data ) { - return new WP_Error( 'export_taxonomy_failed', __( 'Failed to prepare taxonomy for export.', 'secure-custom-fields' ) ); - } - - return $export_data; - } - - /** - * Callback for the import taxonomy ability. - * - * @since 6.7.0 - * - * @param array|object $input The input parameters. - * @return array|WP_Error The imported taxonomy data on success, WP_Error on failure. - */ - public function import_taxonomy_callback( $input ) { - // Import the taxonomy (handles both create and update based on presence of ID). - $imported_taxonomy = acf_import_internal_post_type( $input, 'acf-taxonomy' ); - - if ( ! $imported_taxonomy ) { - return new WP_Error( 'import_taxonomy_failed', __( 'Failed to import taxonomy.', 'secure-custom-fields' ) ); - } - - return $imported_taxonomy; - } - - /** - * Returns a WP_Error for taxonomy not found. - * - * @since 6.7.0 - * @return WP_Error The error object with 404 status. - */ - private function taxonomy_not_found_error() { - return new WP_Error( - 'taxonomy_not_found', - __( 'Taxonomy not found.', 'secure-custom-fields' ), - array( 'status' => 404 ) - ); - } + protected $internal_post_type = 'acf-taxonomy'; } // Initialize abilities instance. acf_new_instance( 'SCF_Taxonomy_Abilities' ); - endif; // class_exists check. diff --git a/includes/class-scf-json-schema-validator.php b/includes/class-scf-json-schema-validator.php index d6ea7b93..4a66ee28 100644 --- a/includes/class-scf-json-schema-validator.php +++ b/includes/class-scf-json-schema-validator.php @@ -22,11 +22,11 @@ class SCF_JSON_Schema_Validator { /** - * Required schema files for post type abilities + * Required schema files for SCF abilities. * * @var array */ - public const REQUIRED_SCHEMAS = array( 'post-type', 'internal-fields', 'scf-identifier' ); + public const REQUIRED_SCHEMAS = array( 'post-type', 'taxonomy', 'internal-fields', 'scf-identifier' ); /** * The last validation errors. diff --git a/tests/php/includes/abilities/test-scf-post-type-abilities.php b/tests/php/includes/abilities/test-scf-post-type-abilities.php index db22ce79..3226a6a1 100644 --- a/tests/php/includes/abilities/test-scf-post-type-abilities.php +++ b/tests/php/includes/abilities/test-scf-post-type-abilities.php @@ -11,8 +11,12 @@ use WorDBless\BaseTestCase; -// Load the abilities class directly since wp_register_ability doesn't exist in WorDBless. -require_once dirname( __DIR__, 3 ) . '/../includes/abilities/class-scf-post-type-abilities.php'; +// Load ACF internal post type class to register the post-type instance. +require_once dirname( __DIR__, 4 ) . '/includes/post-types/class-acf-post-type.php'; + +// Load the abilities classes after ACF classes are loaded. +require_once dirname( __DIR__, 4 ) . '/includes/abilities/class-scf-internal-post-type-abilities.php'; +require_once dirname( __DIR__, 4 ) . '/includes/abilities/class-scf-post-type-abilities.php'; /** * Test SCF Post Type Abilities callbacks @@ -46,19 +50,19 @@ public function setUp(): void { } /** - * Test list_post_types_callback returns array + * Test list_callback returns array */ public function test_list_post_types_returns_array() { - $result = $this->abilities->list_post_types_callback( array() ); + $result = $this->abilities->list_callback( array() ); $this->assertIsArray( $result ); } /** - * Test list_post_types_callback with filter parameter + * Test list_callback with filter parameter */ public function test_list_post_types_with_filter() { - $result = $this->abilities->list_post_types_callback( + $result = $this->abilities->list_callback( array( 'filter' => array( 'active' => true ) ) ); @@ -66,10 +70,10 @@ public function test_list_post_types_with_filter() { } /** - * Test create_post_type_callback returns array with key + * Test create_callback returns array with key */ public function test_create_post_type_returns_array_with_key() { - $result = $this->abilities->create_post_type_callback( $this->test_post_type ); + $result = $this->abilities->create_callback( $this->test_post_type ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'key', $result ); @@ -77,10 +81,10 @@ public function test_create_post_type_returns_array_with_key() { } /** - * Test create_post_type_callback returns array with title + * Test create_callback returns array with title */ public function test_create_post_type_returns_array_with_title() { - $result = $this->abilities->create_post_type_callback( $this->test_post_type ); + $result = $this->abilities->create_callback( $this->test_post_type ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'title', $result ); @@ -88,10 +92,10 @@ public function test_create_post_type_returns_array_with_title() { } /** - * Test get_post_type_callback returns WP_Error for non-existent ID + * Test get_callback returns WP_Error for non-existent ID */ public function test_get_post_type_not_found_returns_error() { - $result = $this->abilities->get_post_type_callback( + $result = $this->abilities->get_callback( array( 'identifier' => 999999 ) ); @@ -100,10 +104,10 @@ public function test_get_post_type_not_found_returns_error() { } /** - * Test get_post_type_callback returns 404 status for non-existent key + * Test get_callback returns 404 status for non-existent key */ public function test_get_post_type_not_found_returns_404_status() { - $result = $this->abilities->get_post_type_callback( + $result = $this->abilities->get_callback( array( 'identifier' => 'nonexistent_post_type' ) ); @@ -113,10 +117,10 @@ public function test_get_post_type_not_found_returns_404_status() { } /** - * Test update_post_type_callback returns WP_Error for non-existent ID + * Test update_callback returns WP_Error for non-existent ID */ public function test_update_post_type_not_found_returns_error() { - $result = $this->abilities->update_post_type_callback( + $result = $this->abilities->update_callback( array( 'ID' => 999999, 'title' => 'Should Fail', @@ -128,10 +132,10 @@ public function test_update_post_type_not_found_returns_error() { } /** - * Test delete_post_type_callback returns WP_Error for non-existent ID + * Test delete_callback returns WP_Error for non-existent ID */ public function test_delete_post_type_not_found_returns_error() { - $result = $this->abilities->delete_post_type_callback( + $result = $this->abilities->delete_callback( array( 'identifier' => 999999 ) ); @@ -140,10 +144,10 @@ public function test_delete_post_type_not_found_returns_error() { } /** - * Test delete_post_type_callback returns 404 status for non-existent key + * Test delete_callback returns 404 status for non-existent key */ public function test_delete_post_type_not_found_returns_404_status() { - $result = $this->abilities->delete_post_type_callback( + $result = $this->abilities->delete_callback( array( 'identifier' => 'nonexistent_delete_target' ) ); @@ -153,10 +157,10 @@ public function test_delete_post_type_not_found_returns_404_status() { } /** - * Test duplicate_post_type_callback returns WP_Error for non-existent ID + * Test duplicate_callback returns WP_Error for non-existent ID */ public function test_duplicate_post_type_not_found_returns_error() { - $result = $this->abilities->duplicate_post_type_callback( + $result = $this->abilities->duplicate_callback( array( 'identifier' => 999999 ) ); @@ -165,10 +169,10 @@ public function test_duplicate_post_type_not_found_returns_error() { } /** - * Test export_post_type_callback returns WP_Error for non-existent ID + * Test export_callback returns WP_Error for non-existent ID */ public function test_export_post_type_not_found_returns_error() { - $result = $this->abilities->export_post_type_callback( + $result = $this->abilities->export_callback( array( 'identifier' => 999999 ) ); @@ -177,29 +181,29 @@ public function test_export_post_type_not_found_returns_error() { } /** - * Test import_post_type_callback returns array + * Test import_callback returns array */ public function test_import_post_type_returns_array() { - $result = $this->abilities->import_post_type_callback( $this->test_post_type ); + $result = $this->abilities->import_callback( $this->test_post_type ); $this->assertIsArray( $result ); } /** - * Test import_post_type_callback returns correct post_type + * Test import_callback returns correct post_type */ public function test_import_post_type_returns_correct_post_type() { - $result = $this->abilities->import_post_type_callback( $this->test_post_type ); + $result = $this->abilities->import_callback( $this->test_post_type ); $this->assertIsArray( $result ); $this->assertEquals( $this->test_post_type['post_type'], $result['post_type'] ); } /** - * Test import_post_type_callback returns correct title + * Test import_callback returns correct title */ public function test_import_post_type_returns_correct_title() { - $result = $this->abilities->import_post_type_callback( $this->test_post_type ); + $result = $this->abilities->import_callback( $this->test_post_type ); $this->assertIsArray( $result ); $this->assertEquals( $this->test_post_type['title'], $result['title'] ); diff --git a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php b/tests/php/includes/abilities/test-scf-taxonomy-abilities.php index efefc293..22b54cc7 100644 --- a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php +++ b/tests/php/includes/abilities/test-scf-taxonomy-abilities.php @@ -14,8 +14,12 @@ // Load mock Abilities API functions before loading the class. require_once __DIR__ . '/abilities-api-mocks.php'; -// Load the abilities class after mocks are defined. -require_once dirname( __DIR__, 3 ) . '/../includes/abilities/class-scf-taxonomy-abilities.php'; +// Load ACF internal post type class to register the taxonomy instance. +require_once dirname( __DIR__, 4 ) . '/includes/post-types/class-acf-taxonomy.php'; + +// Load the abilities classes after ACF classes are loaded. +require_once dirname( __DIR__, 4 ) . '/includes/abilities/class-scf-internal-post-type-abilities.php'; +require_once dirname( __DIR__, 4 ) . '/includes/abilities/class-scf-taxonomy-abilities.php'; /** * Test SCF Taxonomy Abilities callbacks @@ -224,22 +228,22 @@ public function test_export_ability_is_readonly() { // Callback tests. /** - * Test list_taxonomies_callback returns array + * Test list_callback returns array * * Note: Full CRUD flow tests require WordPress post persistence * which WorDBless doesn't fully support. These are tested in E2E. */ public function test_list_taxonomies_callback_returns_array() { - $result = $this->abilities->list_taxonomies_callback( array() ); + $result = $this->abilities->list_callback( array() ); $this->assertIsArray( $result ); } /** - * Test list_taxonomies_callback with filter parameter + * Test list_callback with filter parameter */ public function test_list_taxonomies_callback_with_filter() { - $result = $this->abilities->list_taxonomies_callback( + $result = $this->abilities->list_callback( array( 'filter' => array( 'active' => true ) ) ); @@ -247,10 +251,10 @@ public function test_list_taxonomies_callback_with_filter() { } /** - * Test get_taxonomy_callback returns WP_Error for non-existent ID + * Test get_callback returns WP_Error for non-existent ID */ public function test_get_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->get_taxonomy_callback( + $result = $this->abilities->get_callback( array( 'identifier' => 999999 ) ); @@ -259,10 +263,10 @@ public function test_get_taxonomy_callback_not_found_returns_error() { } /** - * Test get_taxonomy_callback returns 404 status for non-existent key + * Test get_callback returns 404 status for non-existent key */ public function test_get_taxonomy_callback_not_found_returns_404_status() { - $result = $this->abilities->get_taxonomy_callback( + $result = $this->abilities->get_callback( array( 'identifier' => 'nonexistent_taxonomy' ) ); @@ -272,10 +276,10 @@ public function test_get_taxonomy_callback_not_found_returns_404_status() { } /** - * Test create_taxonomy_callback returns array with key + * Test create_callback returns array with key */ public function test_create_taxonomy_callback_returns_array_with_key() { - $result = $this->abilities->create_taxonomy_callback( $this->test_taxonomy ); + $result = $this->abilities->create_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'key', $result ); @@ -283,10 +287,10 @@ public function test_create_taxonomy_callback_returns_array_with_key() { } /** - * Test create_taxonomy_callback returns array with title + * Test create_callback returns array with title */ public function test_create_taxonomy_callback_returns_array_with_title() { - $result = $this->abilities->create_taxonomy_callback( $this->test_taxonomy ); + $result = $this->abilities->create_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'title', $result ); @@ -294,10 +298,10 @@ public function test_create_taxonomy_callback_returns_array_with_title() { } /** - * Test update_taxonomy_callback returns WP_Error for non-existent ID + * Test update_callback returns WP_Error for non-existent ID */ public function test_update_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->update_taxonomy_callback( + $result = $this->abilities->update_callback( array( 'ID' => 999999, 'title' => 'Should Fail', @@ -309,10 +313,10 @@ public function test_update_taxonomy_callback_not_found_returns_error() { } /** - * Test delete_taxonomy_callback returns WP_Error for non-existent ID + * Test delete_callback returns WP_Error for non-existent ID */ public function test_delete_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->delete_taxonomy_callback( + $result = $this->abilities->delete_callback( array( 'identifier' => 999999 ) ); @@ -321,10 +325,10 @@ public function test_delete_taxonomy_callback_not_found_returns_error() { } /** - * Test delete_taxonomy_callback returns 404 status for non-existent key + * Test delete_callback returns 404 status for non-existent key */ public function test_delete_taxonomy_callback_not_found_returns_404_status() { - $result = $this->abilities->delete_taxonomy_callback( + $result = $this->abilities->delete_callback( array( 'identifier' => 'nonexistent_delete_target' ) ); @@ -334,10 +338,10 @@ public function test_delete_taxonomy_callback_not_found_returns_404_status() { } /** - * Test duplicate_taxonomy_callback returns WP_Error for non-existent ID + * Test duplicate_callback returns WP_Error for non-existent ID */ public function test_duplicate_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->duplicate_taxonomy_callback( + $result = $this->abilities->duplicate_callback( array( 'identifier' => 999999 ) ); @@ -346,10 +350,10 @@ public function test_duplicate_taxonomy_callback_not_found_returns_error() { } /** - * Test export_taxonomy_callback returns WP_Error for non-existent ID + * Test export_callback returns WP_Error for non-existent ID */ public function test_export_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->export_taxonomy_callback( + $result = $this->abilities->export_callback( array( 'identifier' => 999999 ) ); @@ -358,29 +362,29 @@ public function test_export_taxonomy_callback_not_found_returns_error() { } /** - * Test import_taxonomy_callback returns array + * Test import_callback returns array */ public function test_import_taxonomy_callback_returns_array() { - $result = $this->abilities->import_taxonomy_callback( $this->test_taxonomy ); + $result = $this->abilities->import_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); } /** - * Test import_taxonomy_callback returns correct taxonomy + * Test import_callback returns correct taxonomy */ public function test_import_taxonomy_callback_returns_correct_taxonomy() { - $result = $this->abilities->import_taxonomy_callback( $this->test_taxonomy ); + $result = $this->abilities->import_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); $this->assertEquals( $this->test_taxonomy['taxonomy'], $result['taxonomy'] ); } /** - * Test import_taxonomy_callback returns correct title + * Test import_callback returns correct title */ public function test_import_taxonomy_callback_returns_correct_title() { - $result = $this->abilities->import_taxonomy_callback( $this->test_taxonomy ); + $result = $this->abilities->import_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); $this->assertEquals( $this->test_taxonomy['title'], $result['title'] ); @@ -389,11 +393,11 @@ public function test_import_taxonomy_callback_returns_correct_title() { // Schema tests. /** - * Test get_taxonomy_schema returns valid schema + * Test get_entity_schema returns valid schema */ - public function test_get_taxonomy_schema_returns_array() { + public function test_get_entity_schema_returns_array() { $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'get_taxonomy_schema' ); + $method = $reflection->getMethod( 'get_entity_schema' ); $method->setAccessible( true ); $schema = $method->invoke( $this->abilities ); @@ -404,11 +408,11 @@ public function test_get_taxonomy_schema_returns_array() { } /** - * Test get_taxonomy_schema has required fields + * Test get_entity_schema has required fields */ - public function test_get_taxonomy_schema_has_required_fields() { + public function test_get_entity_schema_has_required_fields() { $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'get_taxonomy_schema' ); + $method = $reflection->getMethod( 'get_entity_schema' ); $method->setAccessible( true ); $schema = $method->invoke( $this->abilities ); @@ -437,11 +441,11 @@ public function test_get_scf_identifier_schema_returns_array() { // Helper method tests. /** - * Test taxonomy_not_found_error returns correct error code + * Test not_found_error returns correct error code */ - public function test_taxonomy_not_found_error_returns_correct_code() { + public function test_not_found_error_returns_correct_code() { $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'taxonomy_not_found_error' ); + $method = $reflection->getMethod( 'not_found_error' ); $method->setAccessible( true ); $error = $method->invoke( $this->abilities ); @@ -451,11 +455,11 @@ public function test_taxonomy_not_found_error_returns_correct_code() { } /** - * Test taxonomy_not_found_error returns 404 status + * Test not_found_error returns 404 status */ - public function test_taxonomy_not_found_error_returns_404() { + public function test_not_found_error_returns_404() { $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'taxonomy_not_found_error' ); + $method = $reflection->getMethod( 'not_found_error' ); $method->setAccessible( true ); $error = $method->invoke( $this->abilities ); From 160c15736dfa0a705bb73eb3cd07484ac11f2373 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:17:23 +0100 Subject: [PATCH 02/14] Unify error codes, make them post type agnostic --- .../class-scf-internal-post-type-abilities.php | 16 ++++++++-------- tests/e2e/abilities-post-types.spec.ts | 2 +- tests/e2e/abilities-taxonomies.spec.ts | 2 +- .../abilities/test-scf-post-type-abilities.php | 10 +++++----- .../abilities/test-scf-taxonomy-abilities.php | 12 ++++++------ 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/includes/abilities/class-scf-internal-post-type-abilities.php b/includes/abilities/class-scf-internal-post-type-abilities.php index 47eec8cf..b9bd8144 100644 --- a/includes/abilities/class-scf-internal-post-type-abilities.php +++ b/includes/abilities/class-scf-internal-post-type-abilities.php @@ -625,7 +625,7 @@ public function get_callback( $input ) { public function create_callback( $input ) { if ( $this->instance()->get_post( $input['key'] ) ) { return new WP_Error( - $this->instance()->hook_name . '_exists', + 'already_exists', sprintf( /* translators: %s: Entity type */ __( 'A %s with this key already exists.', 'secure-custom-fields' ), @@ -637,7 +637,7 @@ public function create_callback( $input ) { $entity = $this->instance()->update_post( $input ); if ( ! $entity ) { return new WP_Error( - 'create_' . $this->instance()->hook_name . '_failed', + 'create_failed', sprintf( /* translators: %s: Entity type */ __( 'Failed to create %s.', 'secure-custom-fields' ), @@ -663,7 +663,7 @@ public function update_callback( $input ) { $entity = $this->instance()->update_post( array_merge( $existing, $input ) ); if ( ! $entity ) { return new WP_Error( - 'update_' . $this->instance()->hook_name . '_failed', + 'update_failed', sprintf( /* translators: %s: Entity type */ __( 'Failed to update %s.', 'secure-custom-fields' ), @@ -687,7 +687,7 @@ public function delete_callback( $input ) { if ( ! $this->instance()->delete_post( $input['identifier'] ) ) { return new WP_Error( - 'delete_' . $this->instance()->hook_name . '_failed', + 'delete_failed', sprintf( /* translators: %s: Entity type */ __( 'Failed to delete %s.', 'secure-custom-fields' ), @@ -714,7 +714,7 @@ public function duplicate_callback( $input ) { if ( ! $duplicated ) { return new WP_Error( - 'duplicate_' . $this->instance()->hook_name . '_failed', + 'duplicate_failed', sprintf( /* translators: %s: Entity type */ __( 'Failed to duplicate %s.', 'secure-custom-fields' ), @@ -740,7 +740,7 @@ public function export_callback( $input ) { $export = $this->instance()->prepare_post_for_export( $entity ); if ( ! $export ) { return new WP_Error( - 'export_' . $this->instance()->hook_name . '_failed', + 'export_failed', sprintf( /* translators: %s: Entity type */ __( 'Failed to prepare %s for export.', 'secure-custom-fields' ), @@ -761,7 +761,7 @@ public function import_callback( $input ) { $imported = $this->instance()->import_post( $input ); if ( ! $imported ) { return new WP_Error( - 'import_' . $this->instance()->hook_name . '_failed', + 'import_failed', sprintf( /* translators: %s: Entity type */ __( 'Failed to import %s.', 'secure-custom-fields' ), @@ -779,7 +779,7 @@ public function import_callback( $input ) { */ private function not_found_error() { return new WP_Error( - $this->instance()->hook_name . '_not_found', + 'not_found', sprintf( /* translators: %s: Entity type */ __( '%s not found.', 'secure-custom-fields' ), diff --git a/tests/e2e/abilities-post-types.spec.ts b/tests/e2e/abilities-post-types.spec.ts index 13b13b37..9bf00375 100644 --- a/tests/e2e/abilities-post-types.spec.ts +++ b/tests/e2e/abilities-post-types.spec.ts @@ -46,7 +46,7 @@ test.describe( 'Post Type Abilities', () => { await requestPromise; throw new Error( 'Expected not found error but request succeeded' ); } catch ( error ) { - expect( error.code ).toBe( 'post_type_not_found' ); + expect( error.code ).toBe( 'not_found' ); expect( error.data?.status ).toBe( 404 ); } } diff --git a/tests/e2e/abilities-taxonomies.spec.ts b/tests/e2e/abilities-taxonomies.spec.ts index 8452d2c3..c97adb6f 100644 --- a/tests/e2e/abilities-taxonomies.spec.ts +++ b/tests/e2e/abilities-taxonomies.spec.ts @@ -50,7 +50,7 @@ test.describe( 'Taxonomy Abilities', () => { await requestPromise; throw new Error( 'Expected not found error but request succeeded' ); } catch ( error ) { - expect( error.code ).toBe( 'taxonomy_not_found' ); + expect( error.code ).toBe( 'not_found' ); expect( error.data?.status ).toBe( 404 ); } } diff --git a/tests/php/includes/abilities/test-scf-post-type-abilities.php b/tests/php/includes/abilities/test-scf-post-type-abilities.php index 3226a6a1..05e245be 100644 --- a/tests/php/includes/abilities/test-scf-post-type-abilities.php +++ b/tests/php/includes/abilities/test-scf-post-type-abilities.php @@ -100,7 +100,7 @@ public function test_get_post_type_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'post_type_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -128,7 +128,7 @@ public function test_update_post_type_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'post_type_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -140,7 +140,7 @@ public function test_delete_post_type_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'post_type_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -165,7 +165,7 @@ public function test_duplicate_post_type_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'post_type_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -177,7 +177,7 @@ public function test_export_post_type_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'post_type_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** diff --git a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php b/tests/php/includes/abilities/test-scf-taxonomy-abilities.php index 22b54cc7..d5ee7319 100644 --- a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php +++ b/tests/php/includes/abilities/test-scf-taxonomy-abilities.php @@ -259,7 +259,7 @@ public function test_get_taxonomy_callback_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -309,7 +309,7 @@ public function test_update_taxonomy_callback_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -321,7 +321,7 @@ public function test_delete_taxonomy_callback_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -346,7 +346,7 @@ public function test_duplicate_taxonomy_callback_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -358,7 +358,7 @@ public function test_export_taxonomy_callback_not_found_returns_error() { ); $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() ); + $this->assertEquals( 'not_found', $result->get_error_code() ); } /** @@ -451,7 +451,7 @@ public function test_not_found_error_returns_correct_code() { $error = $method->invoke( $this->abilities ); $this->assertInstanceOf( WP_Error::class, $error ); - $this->assertEquals( 'taxonomy_not_found', $error->get_error_code() ); + $this->assertEquals( 'not_found', $error->get_error_code() ); } /** From e59ce80b90276592edf8cf1040ff577bdda31006 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:28:03 +0100 Subject: [PATCH 03/14] Remove redundant PHPUnit tests now that we have a base class --- ...p => SCFInternalPostTypeAbilitiesTest.php} | 14 +- .../test-scf-post-type-abilities.php | 211 ------------------ 2 files changed, 8 insertions(+), 217 deletions(-) rename tests/php/includes/abilities/{test-scf-taxonomy-abilities.php => SCFInternalPostTypeAbilitiesTest.php} (96%) delete mode 100644 tests/php/includes/abilities/test-scf-post-type-abilities.php diff --git a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php similarity index 96% rename from tests/php/includes/abilities/test-scf-taxonomy-abilities.php rename to tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php index d5ee7319..b96ad6b8 100644 --- a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php +++ b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php @@ -1,10 +1,9 @@ 'post_type_phpunit_test', - 'title' => 'PHPUnit Test Type', - 'post_type' => 'phpunit_test', - ); - - /** - * Setup test fixtures - */ - public function setUp(): void { - parent::setUp(); - $this->abilities = acf_get_instance( 'SCF_Post_Type_Abilities' ); - } - - /** - * Test list_callback returns array - */ - public function test_list_post_types_returns_array() { - $result = $this->abilities->list_callback( array() ); - - $this->assertIsArray( $result ); - } - - /** - * Test list_callback with filter parameter - */ - public function test_list_post_types_with_filter() { - $result = $this->abilities->list_callback( - array( 'filter' => array( 'active' => true ) ) - ); - - $this->assertIsArray( $result ); - } - - /** - * Test create_callback returns array with key - */ - public function test_create_post_type_returns_array_with_key() { - $result = $this->abilities->create_callback( $this->test_post_type ); - - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'key', $result ); - $this->assertEquals( $this->test_post_type['key'], $result['key'] ); - } - - /** - * Test create_callback returns array with title - */ - public function test_create_post_type_returns_array_with_title() { - $result = $this->abilities->create_callback( $this->test_post_type ); - - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'title', $result ); - $this->assertEquals( $this->test_post_type['title'], $result['title'] ); - } - - /** - * Test get_callback returns WP_Error for non-existent ID - */ - public function test_get_post_type_not_found_returns_error() { - $result = $this->abilities->get_callback( - array( 'identifier' => 999999 ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test get_callback returns 404 status for non-existent key - */ - public function test_get_post_type_not_found_returns_404_status() { - $result = $this->abilities->get_callback( - array( 'identifier' => 'nonexistent_post_type' ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $error_data = $result->get_error_data(); - $this->assertEquals( 404, $error_data['status'] ); - } - - /** - * Test update_callback returns WP_Error for non-existent ID - */ - public function test_update_post_type_not_found_returns_error() { - $result = $this->abilities->update_callback( - array( - 'ID' => 999999, - 'title' => 'Should Fail', - ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test delete_callback returns WP_Error for non-existent ID - */ - public function test_delete_post_type_not_found_returns_error() { - $result = $this->abilities->delete_callback( - array( 'identifier' => 999999 ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test delete_callback returns 404 status for non-existent key - */ - public function test_delete_post_type_not_found_returns_404_status() { - $result = $this->abilities->delete_callback( - array( 'identifier' => 'nonexistent_delete_target' ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $error_data = $result->get_error_data(); - $this->assertEquals( 404, $error_data['status'] ); - } - - /** - * Test duplicate_callback returns WP_Error for non-existent ID - */ - public function test_duplicate_post_type_not_found_returns_error() { - $result = $this->abilities->duplicate_callback( - array( 'identifier' => 999999 ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test export_callback returns WP_Error for non-existent ID - */ - public function test_export_post_type_not_found_returns_error() { - $result = $this->abilities->export_callback( - array( 'identifier' => 999999 ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test import_callback returns array - */ - public function test_import_post_type_returns_array() { - $result = $this->abilities->import_callback( $this->test_post_type ); - - $this->assertIsArray( $result ); - } - - /** - * Test import_callback returns correct post_type - */ - public function test_import_post_type_returns_correct_post_type() { - $result = $this->abilities->import_callback( $this->test_post_type ); - - $this->assertIsArray( $result ); - $this->assertEquals( $this->test_post_type['post_type'], $result['post_type'] ); - } - - /** - * Test import_callback returns correct title - */ - public function test_import_post_type_returns_correct_title() { - $result = $this->abilities->import_callback( $this->test_post_type ); - - $this->assertIsArray( $result ); - $this->assertEquals( $this->test_post_type['title'], $result['title'] ); - } -} From f4d7f1b80cf6d78b07b0847040a41689b9540959 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:31:44 +0100 Subject: [PATCH 04/14] Update PHPUnit settings to also load tests with the correct `Test` suffix --- phpunit.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 569560d5..c6c32f3a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ ./tests/php/ + ./tests/php/ From f71d43d3565a16a79604309d7147871de0abd5aa Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:37:48 +0100 Subject: [PATCH 05/14] Simplify e2e tests with a single, parametrized suite --- .../e2e/abilities-internal-post-types.spec.ts | 502 ++++++++++++++++++ tests/e2e/abilities-post-types.spec.ts | 385 -------------- tests/e2e/abilities-taxonomies.spec.ts | 482 ----------------- 3 files changed, 502 insertions(+), 867 deletions(-) create mode 100644 tests/e2e/abilities-internal-post-types.spec.ts delete mode 100644 tests/e2e/abilities-post-types.spec.ts delete mode 100644 tests/e2e/abilities-taxonomies.spec.ts diff --git a/tests/e2e/abilities-internal-post-types.spec.ts b/tests/e2e/abilities-internal-post-types.spec.ts new file mode 100644 index 00000000..3e94cd26 --- /dev/null +++ b/tests/e2e/abilities-internal-post-types.spec.ts @@ -0,0 +1,502 @@ +/** + * E2E tests for SCF Internal Post Type Abilities (Post Types and Taxonomies) + * + * Tests the WordPress Abilities API endpoints for SCF post type and taxonomy management. + * Both entity types share the same base class, so tests are parameterized. + * + * HTTP Method Reference (per PR #152 in WordPress/abilities-api): + * - Read-only abilities (readonly: true) → GET with query params + * - Regular abilities (readonly: false, destructive: false) → POST with body + * - Destructive abilities (destructive: true) → DELETE with query params + */ +const { test, expect } = require( './fixtures' ); + +const PLUGIN_SLUG = 'secure-custom-fields'; +const ABILITIES_BASE = '/wp-abilities/v1/abilities'; + +/** + * Entity type configurations for parameterized tests. + */ +const ENTITY_TYPES = [ + { + name: 'Post Type', + slug: 'post-type', + slugPlural: 'post-types', + identifierKey: 'post_type', + testEntity: { + key: 'post_type_e2e_test', + title: 'E2E Test Type', + post_type: 'e2e_test', + }, + }, + { + name: 'Taxonomy', + slug: 'taxonomy', + slugPlural: 'taxonomies', + identifierKey: 'taxonomy', + testEntity: { + key: 'taxonomy_e2e_test', + title: 'E2E Test Taxonomy', + taxonomy: 'e2e_test_tax', + }, + }, +]; + +// Shared helper functions + +/** + * Check if Abilities API exists (for older WordPress versions). + * + * @param {Object} requestUtils - Playwright request utilities. + * @return {Promise} Whether the Abilities API is available. + */ +async function abilitiesApiExists( requestUtils ) { + try { + await requestUtils.rest( { + method: 'GET', + path: '/wp-abilities/v1', + } ); + return true; + } catch { + return false; + } +} + +/** + * Assert that a REST request throws a "not found" error with 404 status. + * + * @param {Promise} requestPromise - The REST request promise to check. + */ +async function expectNotFound( requestPromise ) { + try { + await requestPromise; + throw new Error( 'Expected not found error but request succeeded' ); + } catch ( error ) { + expect( error.code ).toBe( 'not_found' ); + expect( error.data?.status ).toBe( 404 ); + } +} + +/** + * Assert that a REST request throws an "invalid input" error with 400 status. + * + * @param {Promise} requestPromise - The REST request promise to check. + */ +async function expectInvalidInput( requestPromise ) { + try { + await requestPromise; + throw new Error( 'Expected invalid input error but request succeeded' ); + } catch ( error ) { + expect( error.code ).toBe( 'ability_invalid_input' ); + expect( error.data?.status ).toBe( 400 ); + } +} + +/** + * Creates API helper functions for a given entity type. + * + * @param {Object} entityType - The entity type configuration. + * @return {Object} Object containing API helper functions. + */ +function createApiHelpers( entityType ) { + const { slug, slugPlural, testEntity } = entityType; + + return { + list: ( requestUtils, filter = {} ) => + requestUtils.rest( { + method: 'POST', + path: `${ ABILITIES_BASE }/scf/list-${ slugPlural }/run`, + data: { input: { filter } }, + } ), + + get: ( requestUtils, identifier ) => + requestUtils.rest( { + method: 'POST', + path: `${ ABILITIES_BASE }/scf/get-${ slug }/run`, + data: { input: { identifier } }, + } ), + + create: ( requestUtils, data = testEntity ) => + requestUtils.rest( { + method: 'POST', + path: `${ ABILITIES_BASE }/scf/create-${ slug }/run`, + data: { input: data }, + } ), + + update: ( requestUtils, input ) => + requestUtils.rest( { + method: 'POST', + path: `${ ABILITIES_BASE }/scf/update-${ slug }/run`, + data: { input }, + } ), + + delete: ( requestUtils, identifier ) => + requestUtils.rest( { + method: 'DELETE', + path: `${ ABILITIES_BASE }/scf/delete-${ slug }/run`, + params: { 'input[identifier]': identifier }, + } ), + + duplicate: ( requestUtils, identifier ) => + requestUtils.rest( { + method: 'POST', + path: `${ ABILITIES_BASE }/scf/duplicate-${ slug }/run`, + data: { input: { identifier } }, + } ), + + export: ( requestUtils, identifier ) => + requestUtils.rest( { + method: 'GET', + path: `${ ABILITIES_BASE }/scf/export-${ slug }/run`, + params: { 'input[identifier]': identifier }, + } ), + + import: ( requestUtils, data ) => + requestUtils.rest( { + method: 'POST', + path: `${ ABILITIES_BASE }/scf/import-${ slug }/run`, + data: { input: data }, + } ), + + cleanup: async ( requestUtils, identifier ) => { + try { + await requestUtils.rest( { + method: 'DELETE', + path: `${ ABILITIES_BASE }/scf/delete-${ slug }/run`, + params: { 'input[identifier]': identifier }, + } ); + } catch { + // Ignore errors - entity may not exist + } + }, + }; +} + +// Run tests for each entity type +for ( const entityType of ENTITY_TYPES ) { + const { name, slug, slugPlural, identifierKey, testEntity } = entityType; + const api = createApiHelpers( entityType ); + + test.describe( `${ name } Abilities`, () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( PLUGIN_SLUG ); + + // Skip all tests if Abilities API is not available + const hasAbilitiesApi = await abilitiesApiExists( requestUtils ); + test.skip( + ! hasAbilitiesApi, + 'Abilities API not available in this WordPress version' + ); + } ); + + // List entities - POST with body + + test.describe( `scf/list-${ slugPlural }`, () => { + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + await api.create( requestUtils ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should list all SCF ${ slugPlural }`, async ( { + requestUtils, + } ) => { + const result = await api.list( requestUtils ); + + expect( Array.isArray( result ) ).toBe( true ); + expect( + result.some( ( item ) => item.key === testEntity.key ) + ).toBe( true ); + } ); + + test( 'should support filter parameter', async ( { + requestUtils, + } ) => { + const result = await api.list( requestUtils, { active: true } ); + + expect( Array.isArray( result ) ).toBe( true ); + } ); + } ); + + // Get entity - POST with body + + test.describe( `scf/get-${ slug }`, () => { + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + await api.create( requestUtils ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should get a ${ slug } by identifier`, async ( { + requestUtils, + } ) => { + const result = await api.get( requestUtils, testEntity.key ); + + expect( result ).toHaveProperty( + identifierKey, + testEntity[ identifierKey ] + ); + expect( result ).toHaveProperty( 'title', testEntity.title ); + } ); + + test( `should return error for non-existent ${ slug }`, async ( { + requestUtils, + } ) => { + await expectNotFound( + api.get( requestUtils, 'nonexistent_entity_abc' ) + ); + } ); + } ); + + // Export entity - GET with query params (readonly) + + test.describe( `scf/export-${ slug }`, () => { + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + await api.create( requestUtils ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should export a ${ slug } as JSON`, async ( { + requestUtils, + } ) => { + const result = await api.export( requestUtils, testEntity.key ); + + expect( result ).toHaveProperty( 'key', testEntity.key ); + expect( result ).toHaveProperty( + identifierKey, + testEntity[ identifierKey ] + ); + expect( result ).toHaveProperty( 'title', testEntity.title ); + } ); + + test( `should return error for non-existent ${ slug }`, async ( { + requestUtils, + } ) => { + await expectNotFound( + api.export( requestUtils, 'nonexistent_export' ) + ); + } ); + } ); + + // Create entity - POST with body + + test.describe( `scf/create-${ slug }`, () => { + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should create a new ${ slug }`, async ( { + requestUtils, + } ) => { + const result = await api.create( requestUtils ); + + expect( result ).toHaveProperty( + identifierKey, + testEntity[ identifierKey ] + ); + expect( result ).toHaveProperty( 'title', testEntity.title ); + expect( result ).toHaveProperty( 'key', testEntity.key ); + } ); + + test( 'should return error when required fields are missing', async ( { + requestUtils, + } ) => { + await expectInvalidInput( + api.create( requestUtils, { + title: 'Missing Required Fields', + } ) + ); + } ); + } ); + + // Update entity - POST with body + + test.describe( `scf/update-${ slug }`, () => { + let testEntityId; + + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + const result = await api.create( requestUtils ); + testEntityId = result.ID; + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should update an existing ${ slug }`, async ( { + requestUtils, + } ) => { + const result = await api.update( requestUtils, { + ID: testEntityId, + title: 'Updated Title', + } ); + + expect( result ).toHaveProperty( 'title', 'Updated Title' ); + } ); + + test( `should return error for non-existent ${ slug } ID`, async ( { + requestUtils, + } ) => { + await expectNotFound( + api.update( requestUtils, { + ID: 999999, + title: 'Should Fail', + } ) + ); + } ); + + test( 'should return error when ID is missing', async ( { + requestUtils, + } ) => { + await expectInvalidInput( + api.update( requestUtils, { title: 'Missing ID' } ) + ); + } ); + } ); + + // Delete entity - DELETE with query params (destructive) + + test.describe( `scf/delete-${ slug }`, () => { + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + await api.create( requestUtils ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should delete an existing ${ slug }`, async ( { + requestUtils, + } ) => { + const result = await api.delete( requestUtils, testEntity.key ); + expect( result ).toBe( true ); + + // Verify it's actually deleted + await expectNotFound( api.get( requestUtils, testEntity.key ) ); + } ); + + test( `should return error for non-existent ${ slug }`, async ( { + requestUtils, + } ) => { + await api.cleanup( requestUtils, testEntity.key ); + + await expectNotFound( + api.delete( requestUtils, 'nonexistent_entity_xyz' ) + ); + } ); + + test( 'should return error when identifier is missing', async ( { + requestUtils, + } ) => { + await expectInvalidInput( + requestUtils.rest( { + method: 'DELETE', + path: `${ ABILITIES_BASE }/scf/delete-${ slug }/run`, + params: { input: '' }, + } ) + ); + } ); + } ); + + // Duplicate entity - POST with body + // + // Note: The duplicate receives a new unique key but retains the same + // slug. The duplicate won't register until slug is changed. + + test.describe( `scf/duplicate-${ slug }`, () => { + let duplicatedKey; + + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + await api.create( requestUtils ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + if ( duplicatedKey ) { + await api.cleanup( requestUtils, duplicatedKey ); + duplicatedKey = null; + } + } ); + + test( `should duplicate an existing ${ slug }`, async ( { + requestUtils, + } ) => { + const result = await api.duplicate( + requestUtils, + testEntity.key + ); + duplicatedKey = result.key; + + expect( result ).toHaveProperty( 'key' ); + expect( result ).toHaveProperty( identifierKey ); + expect( result.key ).not.toBe( testEntity.key ); + expect( result[ identifierKey ] ).toBe( + testEntity[ identifierKey ] + ); + expect( result.title ).toContain( '(copy)' ); + } ); + + test( `should return error for non-existent ${ slug }`, async ( { + requestUtils, + } ) => { + await expectNotFound( + api.duplicate( + requestUtils, + 'nonexistent_duplicate_source' + ) + ); + } ); + } ); + + // Import entity - POST with body + + test.describe( `scf/import-${ slug }`, () => { + test.beforeEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await api.cleanup( requestUtils, testEntity.key ); + } ); + + test( `should import a ${ slug } from JSON`, async ( { + requestUtils, + } ) => { + const result = await api.import( requestUtils, testEntity ); + + expect( result ).toHaveProperty( + identifierKey, + testEntity[ identifierKey ] + ); + expect( result ).toHaveProperty( 'title', testEntity.title ); + } ); + + test( 'should return error when required fields are missing', async ( { + requestUtils, + } ) => { + await expectInvalidInput( + api.import( requestUtils, { + title: 'Missing Required Fields', + } ) + ); + } ); + } ); + } ); +} diff --git a/tests/e2e/abilities-post-types.spec.ts b/tests/e2e/abilities-post-types.spec.ts deleted file mode 100644 index 9bf00375..00000000 --- a/tests/e2e/abilities-post-types.spec.ts +++ /dev/null @@ -1,385 +0,0 @@ -/** - * E2E tests for SCF Post Type Abilities - * - * Tests the WordPress Abilities API endpoints for SCF post type management. - * - * HTTP Method Reference (per PR #152 in WordPress/abilities-api): - * - Read-only abilities (readonly: true) → GET with query params - * - Regular abilities (readonly: false, destructive: false) → POST with body - * - Destructive abilities (destructive: true) → DELETE with query params - */ -const { test, expect } = require( './fixtures' ); - -test.describe( 'Post Type Abilities', () => { - const PLUGIN_SLUG = 'secure-custom-fields'; - const ABILITIES_BASE = '/wp-abilities/v1/abilities'; - - // Reusable test post type - each test creates/cleans its own instance - const TEST_POST_TYPE = { - key: 'post_type_e2e_test', - title: 'E2E Test Type', - post_type: 'e2e_test', - }; - - // Helper functions - - /** - * Check if Abilities API exists (for older WordPress versions). - */ - async function abilitiesApiExists( requestUtils ) { - try { - await requestUtils.rest( { - method: 'GET', - path: '/wp-abilities/v1', - } ); - return true; - } catch { - return false; - } - } - - /** - * Assert that a REST request throws a "not found" error with 404 status. - */ - async function expectNotFound( requestPromise ) { - try { - await requestPromise; - throw new Error( 'Expected not found error but request succeeded' ); - } catch ( error ) { - expect( error.code ).toBe( 'not_found' ); - expect( error.data?.status ).toBe( 404 ); - } - } - - /** - * Assert that a REST request throws an "invalid input" error with 400 status. - */ - async function expectInvalidInput( requestPromise ) { - try { - await requestPromise; - throw new Error( 'Expected invalid input error but request succeeded' ); - } catch ( error ) { - expect( error.code ).toBe( 'ability_invalid_input' ); - expect( error.data?.status ).toBe( 400 ); - } - } - - // Post type API helpers - - async function listPostTypes( requestUtils, filter = {} ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/list-post-types/run`, - data: { input: { filter } }, - } ); - } - - async function getPostType( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/get-post-type/run`, - data: { input: { identifier } }, - } ); - } - - async function createPostType( requestUtils, postTypeData = TEST_POST_TYPE ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/create-post-type/run`, - data: { input: postTypeData }, - } ); - } - - async function updatePostType( requestUtils, input ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/update-post-type/run`, - data: { input }, - } ); - } - - async function deletePostType( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'DELETE', - path: `${ ABILITIES_BASE }/scf/delete-post-type/run`, - params: { 'input[identifier]': identifier }, - } ); - } - - async function duplicatePostType( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/duplicate-post-type/run`, - data: { input: { identifier } }, - } ); - } - - async function exportPostType( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'GET', - path: `${ ABILITIES_BASE }/scf/export-post-type/run`, - params: { 'input[identifier]': identifier }, - } ); - } - - async function importPostType( requestUtils, postTypeData ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/import-post-type/run`, - data: { input: postTypeData }, - } ); - } - - /** - * Clean up a post type (ignore errors if it doesn't exist). - */ - async function cleanupPostType( requestUtils, identifier ) { - try { - await deletePostType( requestUtils, identifier ); - } catch { - // Ignore errors - post type may not exist - } - } - - // Test setup - - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activatePlugin( PLUGIN_SLUG ); - - // Skip all tests if Abilities API is not available (older WordPress versions) - const hasAbilitiesApi = await abilitiesApiExists( requestUtils ); - test.skip( - ! hasAbilitiesApi, - 'Abilities API not available in this WordPress version' - ); - } ); - - // List post types - POST with body - - test.describe( 'scf/list-post-types', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - await createPostType( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should list all SCF post types', async ( { requestUtils } ) => { - const result = await listPostTypes( requestUtils ); - - expect( Array.isArray( result ) ).toBe( true ); - expect( result.some( ( pt ) => pt.key === TEST_POST_TYPE.key ) ).toBe( true ); - } ); - - test( 'should support filter parameter', async ( { requestUtils } ) => { - const result = await listPostTypes( requestUtils, { active: true } ); - - expect( Array.isArray( result ) ).toBe( true ); - } ); - } ); - - // Get post type - POST with body - - test.describe( 'scf/get-post-type', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - await createPostType( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should get a post type by identifier', async ( { requestUtils } ) => { - const result = await getPostType( requestUtils, TEST_POST_TYPE.key ); - - expect( result ).toHaveProperty( 'post_type', TEST_POST_TYPE.post_type ); - expect( result ).toHaveProperty( 'title', TEST_POST_TYPE.title ); - } ); - - test( 'should return error for non-existent post type', async ( { requestUtils } ) => { - await expectNotFound( getPostType( requestUtils, 'nonexistent_type_abc' ) ); - } ); - } ); - - // Export post type - GET with query params (readonly) - - test.describe( 'scf/export-post-type', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - await createPostType( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should export a post type as JSON', async ( { requestUtils } ) => { - const result = await exportPostType( requestUtils, TEST_POST_TYPE.key ); - - expect( result ).toHaveProperty( 'key', TEST_POST_TYPE.key ); - expect( result ).toHaveProperty( 'post_type', TEST_POST_TYPE.post_type ); - expect( result ).toHaveProperty( 'title', TEST_POST_TYPE.title ); - } ); - - test( 'should return error for non-existent post type', async ( { requestUtils } ) => { - await expectNotFound( exportPostType( requestUtils, 'nonexistent_export' ) ); - } ); - } ); - - // Create post type - POST with body - - test.describe( 'scf/create-post-type', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should create a new post type', async ( { requestUtils } ) => { - const result = await createPostType( requestUtils ); - - expect( result ).toHaveProperty( 'post_type', TEST_POST_TYPE.post_type ); - expect( result ).toHaveProperty( 'title', TEST_POST_TYPE.title ); - expect( result ).toHaveProperty( 'key', TEST_POST_TYPE.key ); - } ); - - test( 'should return error when required fields are missing', async ( { requestUtils } ) => { - await expectInvalidInput( createPostType( requestUtils, { title: 'Missing Key and Post Type' } ) ); - } ); - } ); - - // Update post type - POST with body - - test.describe( 'scf/update-post-type', () => { - let testPostTypeId; - - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - const result = await createPostType( requestUtils ); - testPostTypeId = result.ID; - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should update an existing post type', async ( { requestUtils } ) => { - const result = await updatePostType( requestUtils, { - ID: testPostTypeId, - title: 'Updated Title', - } ); - - expect( result ).toHaveProperty( 'title', 'Updated Title' ); - } ); - - test( 'should return error for non-existent post type ID', async ( { requestUtils } ) => { - await expectNotFound( updatePostType( requestUtils, { ID: 999999, title: 'Should Fail' } ) ); - } ); - - test( 'should return error when ID is missing', async ( { requestUtils } ) => { - await expectInvalidInput( updatePostType( requestUtils, { title: 'Missing ID' } ) ); - } ); - } ); - - // Delete post type - DELETE with query params (destructive) - - test.describe( 'scf/delete-post-type', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - await createPostType( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should delete an existing post type', async ( { requestUtils } ) => { - const result = await deletePostType( requestUtils, TEST_POST_TYPE.key ); - expect( result ).toBe( true ); - - // Verify it's actually deleted - await expectNotFound( getPostType( requestUtils, TEST_POST_TYPE.key ) ); - } ); - - test( 'should return error for non-existent post type', async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - - await expectNotFound( deletePostType( requestUtils, 'nonexistent_type_xyz' ) ); - } ); - - test( 'should return error when identifier is missing', async ( { requestUtils } ) => { - await expectInvalidInput( - requestUtils.rest( { - method: 'DELETE', - path: `${ ABILITIES_BASE }/scf/delete-post-type/run`, - params: { input: '' }, - } ) - ); - } ); - } ); - - // Duplicate post type - POST with body - // - // Note: The duplicate receives a new unique key but retains the same - // post_type slug. The duplicate won't register until slug is changed. - - test.describe( 'scf/duplicate-post-type', () => { - let duplicatedKey; - - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - await createPostType( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - if ( duplicatedKey ) { - await cleanupPostType( requestUtils, duplicatedKey ); - duplicatedKey = null; - } - } ); - - test( 'should duplicate an existing post type', async ( { requestUtils } ) => { - const result = await duplicatePostType( requestUtils, TEST_POST_TYPE.key ); - duplicatedKey = result.key; - - expect( result ).toHaveProperty( 'key' ); - expect( result ).toHaveProperty( 'post_type' ); - expect( result.key ).not.toBe( TEST_POST_TYPE.key ); - expect( result.post_type ).toBe( TEST_POST_TYPE.post_type ); - expect( result.title ).toContain( '(copy)' ); - } ); - - test( 'should return error for non-existent post type', async ( { requestUtils } ) => { - await expectNotFound( duplicatePostType( requestUtils, 'nonexistent_duplicate_source' ) ); - } ); - } ); - - // Import post type - POST with body - - test.describe( 'scf/import-post-type', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupPostType( requestUtils, TEST_POST_TYPE.key ); - } ); - - test( 'should import a post type from JSON', async ( { requestUtils } ) => { - const result = await importPostType( requestUtils, TEST_POST_TYPE ); - - expect( result ).toHaveProperty( 'post_type', TEST_POST_TYPE.post_type ); - expect( result ).toHaveProperty( 'title', TEST_POST_TYPE.title ); - } ); - - test( 'should return error when required fields are missing', async ( { requestUtils } ) => { - await expectInvalidInput( importPostType( requestUtils, { title: 'Missing Required Fields' } ) ); - } ); - } ); -} ); diff --git a/tests/e2e/abilities-taxonomies.spec.ts b/tests/e2e/abilities-taxonomies.spec.ts deleted file mode 100644 index c97adb6f..00000000 --- a/tests/e2e/abilities-taxonomies.spec.ts +++ /dev/null @@ -1,482 +0,0 @@ -/** - * E2E tests for SCF Taxonomy Abilities - * - * Tests the WordPress Abilities API endpoints for SCF taxonomy management. - * - * HTTP Method Reference (per PR #152 in WordPress/abilities-api): - * - Read-only abilities (readonly: true) → GET with query params - * - Regular abilities (readonly: false, destructive: false) → POST with body - * - Destructive abilities (destructive: true) → DELETE with query params - */ -const { test, expect } = require( './fixtures' ); - -test.describe( 'Taxonomy Abilities', () => { - const PLUGIN_SLUG = 'secure-custom-fields'; - const ABILITIES_BASE = '/wp-abilities/v1/abilities'; - - // Reusable test taxonomy - each test creates/cleans its own instance - const TEST_TAXONOMY = { - key: 'taxonomy_e2e_test', - title: 'E2E Test Taxonomy', - taxonomy: 'e2e_test_tax', - }; - - // Helper functions - - /** - * Check if Abilities API exists (for older WordPress versions). - * - * @param {Object} requestUtils - Playwright request utilities. - */ - async function abilitiesApiExists( requestUtils ) { - try { - await requestUtils.rest( { - method: 'GET', - path: '/wp-abilities/v1', - } ); - return true; - } catch { - return false; - } - } - - /** - * Assert that a REST request throws a "not found" error with 404 status. - * - * @param {Promise} requestPromise - The REST request promise to check. - */ - async function expectNotFound( requestPromise ) { - try { - await requestPromise; - throw new Error( 'Expected not found error but request succeeded' ); - } catch ( error ) { - expect( error.code ).toBe( 'not_found' ); - expect( error.data?.status ).toBe( 404 ); - } - } - - /** - * Assert that a REST request throws an "invalid input" error with 400 status. - * - * @param {Promise} requestPromise - The REST request promise to check. - */ - async function expectInvalidInput( requestPromise ) { - try { - await requestPromise; - throw new Error( - 'Expected invalid input error but request succeeded' - ); - } catch ( error ) { - expect( error.code ).toBe( 'ability_invalid_input' ); - expect( error.data?.status ).toBe( 400 ); - } - } - - // Taxonomy API helpers - - async function listTaxonomies( requestUtils, filter = {} ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/list-taxonomies/run`, - data: { input: { filter } }, - } ); - } - - async function getTaxonomy( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/get-taxonomy/run`, - data: { input: { identifier } }, - } ); - } - - async function createTaxonomy( - requestUtils, - taxonomyData = TEST_TAXONOMY - ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/create-taxonomy/run`, - data: { input: taxonomyData }, - } ); - } - - async function updateTaxonomy( requestUtils, input ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/update-taxonomy/run`, - data: { input }, - } ); - } - - async function deleteTaxonomy( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'DELETE', - path: `${ ABILITIES_BASE }/scf/delete-taxonomy/run`, - params: { 'input[identifier]': identifier }, - } ); - } - - async function duplicateTaxonomy( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/duplicate-taxonomy/run`, - data: { input: { identifier } }, - } ); - } - - async function exportTaxonomy( requestUtils, identifier ) { - return await requestUtils.rest( { - method: 'GET', - path: `${ ABILITIES_BASE }/scf/export-taxonomy/run`, - params: { 'input[identifier]': identifier }, - } ); - } - - async function importTaxonomy( requestUtils, taxonomyData ) { - return await requestUtils.rest( { - method: 'POST', - path: `${ ABILITIES_BASE }/scf/import-taxonomy/run`, - data: { input: taxonomyData }, - } ); - } - - /** - * Clean up a taxonomy (ignore errors if it doesn't exist). - * - * @param {Object} requestUtils - Playwright request utilities. - * @param {string} identifier - The taxonomy identifier (key or ID). - */ - async function cleanupTaxonomy( requestUtils, identifier ) { - try { - await deleteTaxonomy( requestUtils, identifier ); - } catch { - // Ignore errors - taxonomy may not exist - } - } - - // Test setup - - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activatePlugin( PLUGIN_SLUG ); - - // Skip all tests if Abilities API is not available (older WordPress versions) - const hasAbilitiesApi = await abilitiesApiExists( requestUtils ); - test.skip( - ! hasAbilitiesApi, - 'Abilities API not available in this WordPress version' - ); - } ); - - // List taxonomies - POST with body - - test.describe( 'scf/list-taxonomies', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - await createTaxonomy( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should list all SCF taxonomies', async ( { requestUtils } ) => { - const result = await listTaxonomies( requestUtils ); - - expect( Array.isArray( result ) ).toBe( true ); - expect( - result.some( ( tax ) => tax.key === TEST_TAXONOMY.key ) - ).toBe( true ); - } ); - - test( 'should support filter parameter', async ( { requestUtils } ) => { - const result = await listTaxonomies( requestUtils, { - active: true, - } ); - - expect( Array.isArray( result ) ).toBe( true ); - } ); - } ); - - // Get taxonomy - POST with body - - test.describe( 'scf/get-taxonomy', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - await createTaxonomy( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should get a taxonomy by identifier', async ( { - requestUtils, - } ) => { - const result = await getTaxonomy( requestUtils, TEST_TAXONOMY.key ); - - expect( result ).toHaveProperty( - 'taxonomy', - TEST_TAXONOMY.taxonomy - ); - expect( result ).toHaveProperty( 'title', TEST_TAXONOMY.title ); - } ); - - test( 'should return error for non-existent taxonomy', async ( { - requestUtils, - } ) => { - await expectNotFound( - getTaxonomy( requestUtils, 'nonexistent_taxonomy_abc' ) - ); - } ); - } ); - - // Export taxonomy - GET with query params (readonly) - - test.describe( 'scf/export-taxonomy', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - await createTaxonomy( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should export a taxonomy as JSON', async ( { - requestUtils, - } ) => { - const result = await exportTaxonomy( - requestUtils, - TEST_TAXONOMY.key - ); - - expect( result ).toHaveProperty( 'key', TEST_TAXONOMY.key ); - expect( result ).toHaveProperty( - 'taxonomy', - TEST_TAXONOMY.taxonomy - ); - expect( result ).toHaveProperty( 'title', TEST_TAXONOMY.title ); - } ); - - test( 'should return error for non-existent taxonomy', async ( { - requestUtils, - } ) => { - await expectNotFound( - exportTaxonomy( requestUtils, 'nonexistent_export' ) - ); - } ); - } ); - - // Create taxonomy - POST with body - - test.describe( 'scf/create-taxonomy', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should create a new taxonomy', async ( { requestUtils } ) => { - const result = await createTaxonomy( requestUtils ); - - expect( result ).toHaveProperty( - 'taxonomy', - TEST_TAXONOMY.taxonomy - ); - expect( result ).toHaveProperty( 'title', TEST_TAXONOMY.title ); - expect( result ).toHaveProperty( 'key', TEST_TAXONOMY.key ); - } ); - - test( 'should return error when required fields are missing', async ( { - requestUtils, - } ) => { - await expectInvalidInput( - createTaxonomy( requestUtils, { - title: 'Missing Key and Taxonomy', - } ) - ); - } ); - } ); - - // Update taxonomy - POST with body - - test.describe( 'scf/update-taxonomy', () => { - let testTaxonomyId; - - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - const result = await createTaxonomy( requestUtils ); - testTaxonomyId = result.ID; - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should update an existing taxonomy', async ( { - requestUtils, - } ) => { - const result = await updateTaxonomy( requestUtils, { - ID: testTaxonomyId, - title: 'Updated Title', - } ); - - expect( result ).toHaveProperty( 'title', 'Updated Title' ); - } ); - - test( 'should return error for non-existent taxonomy ID', async ( { - requestUtils, - } ) => { - await expectNotFound( - updateTaxonomy( requestUtils, { - ID: 999999, - title: 'Should Fail', - } ) - ); - } ); - - test( 'should return error when ID is missing', async ( { - requestUtils, - } ) => { - await expectInvalidInput( - updateTaxonomy( requestUtils, { title: 'Missing ID' } ) - ); - } ); - } ); - - // Delete taxonomy - DELETE with query params (destructive) - - test.describe( 'scf/delete-taxonomy', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - await createTaxonomy( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should delete an existing taxonomy', async ( { - requestUtils, - } ) => { - const result = await deleteTaxonomy( - requestUtils, - TEST_TAXONOMY.key - ); - expect( result ).toBe( true ); - - // Verify it's actually deleted - await expectNotFound( - getTaxonomy( requestUtils, TEST_TAXONOMY.key ) - ); - } ); - - test( 'should return error for non-existent taxonomy', async ( { - requestUtils, - } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - - await expectNotFound( - deleteTaxonomy( requestUtils, 'nonexistent_taxonomy_xyz' ) - ); - } ); - - test( 'should return error when identifier is missing', async ( { - requestUtils, - } ) => { - await expectInvalidInput( - requestUtils.rest( { - method: 'DELETE', - path: `${ ABILITIES_BASE }/scf/delete-taxonomy/run`, - params: { input: '' }, - } ) - ); - } ); - } ); - - // Duplicate taxonomy - POST with body - // - // Note: The duplicate receives a new unique key but retains the same - // taxonomy slug. The duplicate won't register until slug is changed. - - test.describe( 'scf/duplicate-taxonomy', () => { - let duplicatedKey; - - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - await createTaxonomy( requestUtils ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - if ( duplicatedKey ) { - await cleanupTaxonomy( requestUtils, duplicatedKey ); - duplicatedKey = null; - } - } ); - - test( 'should duplicate an existing taxonomy', async ( { - requestUtils, - } ) => { - const result = await duplicateTaxonomy( - requestUtils, - TEST_TAXONOMY.key - ); - duplicatedKey = result.key; - - expect( result ).toHaveProperty( 'key' ); - expect( result ).toHaveProperty( 'taxonomy' ); - expect( result.key ).not.toBe( TEST_TAXONOMY.key ); - expect( result.taxonomy ).toBe( TEST_TAXONOMY.taxonomy ); - expect( result.title ).toContain( '(copy)' ); - } ); - - test( 'should return error for non-existent taxonomy', async ( { - requestUtils, - } ) => { - await expectNotFound( - duplicateTaxonomy( - requestUtils, - 'nonexistent_duplicate_source' - ) - ); - } ); - } ); - - // Import taxonomy - POST with body - - test.describe( 'scf/import-taxonomy', () => { - test.beforeEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await cleanupTaxonomy( requestUtils, TEST_TAXONOMY.key ); - } ); - - test( 'should import a taxonomy from JSON', async ( { - requestUtils, - } ) => { - const result = await importTaxonomy( requestUtils, TEST_TAXONOMY ); - - expect( result ).toHaveProperty( - 'taxonomy', - TEST_TAXONOMY.taxonomy - ); - expect( result ).toHaveProperty( 'title', TEST_TAXONOMY.title ); - } ); - - test( 'should return error when required fields are missing', async ( { - requestUtils, - } ) => { - await expectInvalidInput( - importTaxonomy( requestUtils, { - title: 'Missing Required Fields', - } ) - ); - } ); - } ); -} ); From a6c06a299ea051f0d93cb2697e5676e64c806c01 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:43:35 +0100 Subject: [PATCH 06/14] Fix annotations for readonly abilities --- includes/abilities/class-scf-internal-post-type-abilities.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/abilities/class-scf-internal-post-type-abilities.php b/includes/abilities/class-scf-internal-post-type-abilities.php index b9bd8144..d81151ea 100644 --- a/includes/abilities/class-scf-internal-post-type-abilities.php +++ b/includes/abilities/class-scf-internal-post-type-abilities.php @@ -255,7 +255,7 @@ private function register_list_ability() { 'show_in_rest' => true, 'mcp' => array( 'public' => true ), 'annotations' => array( - 'readonly' => false, + 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), @@ -313,7 +313,7 @@ private function register_get_ability() { 'show_in_rest' => true, 'mcp' => array( 'public' => true ), 'annotations' => array( - 'readonly' => false, + 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), From f46da7a0c8c1c0c331e245af47c6752c21316b30 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:56:23 +0100 Subject: [PATCH 07/14] Add detailed testing to test return paths by mocking internal SCF methds --- .../SCFInternalPostTypeAbilitiesTest.php | 360 +++++++++++++++++- 1 file changed, 358 insertions(+), 2 deletions(-) diff --git a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php index b96ad6b8..00d995f4 100644 --- a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php +++ b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php @@ -54,6 +54,28 @@ public function setUp(): void { $this->abilities = acf_get_instance( 'SCF_Taxonomy_Abilities' ); } + /** + * Helper to inject a mock instance into the abilities object. + * + * @param array $method_returns Map of method names to return values. + * @return \PHPUnit\Framework\MockObject\MockObject The mock instance. + */ + private function inject_mock_instance( array $method_returns ) { + $mock_instance = $this->createMock( ACF_Taxonomy::class ); + $mock_instance->hook_name = 'acf_taxonomy'; + + foreach ( $method_returns as $method => $return_value ) { + $mock_instance->method( $method )->willReturn( $return_value ); + } + + $reflection = new ReflectionClass( SCF_Internal_Post_Type_Abilities::class ); + $property = $reflection->getProperty( 'instance' ); + $property->setAccessible( true ); + $property->setValue( $this->abilities, $mock_instance ); + + return $mock_instance; + } + // Constructor tests. /** @@ -227,6 +249,83 @@ public function test_export_ability_is_readonly() { $this->assertTrue( $annotations['readonly'] ?? false, 'Export ability should be marked readonly' ); } + /** + * Test list ability is marked as readonly + */ + public function test_list_ability_is_readonly() { + global $mock_registered_abilities; + $mock_registered_abilities = array(); + + $this->abilities->register_abilities(); + + $this->assertArrayHasKey( 'scf/list-taxonomies', $mock_registered_abilities ); + $ability = $mock_registered_abilities['scf/list-taxonomies']; + $meta = $ability['meta'] ?? array(); + $annotations = $meta['annotations'] ?? array(); + $this->assertTrue( $annotations['readonly'] ?? false, 'List ability should be marked readonly' ); + } + + /** + * Test get ability is marked as readonly + */ + public function test_get_ability_is_readonly() { + global $mock_registered_abilities; + $mock_registered_abilities = array(); + + $this->abilities->register_abilities(); + + $this->assertArrayHasKey( 'scf/get-taxonomy', $mock_registered_abilities ); + $ability = $mock_registered_abilities['scf/get-taxonomy']; + $meta = $ability['meta'] ?? array(); + $annotations = $meta['annotations'] ?? array(); + $this->assertTrue( $annotations['readonly'] ?? false, 'Get ability should be marked readonly' ); + } + + /** + * Test abilities that return data have output_schema + */ + public function test_data_returning_abilities_have_output_schema() { + global $mock_registered_abilities; + $mock_registered_abilities = array(); + + $this->abilities->register_abilities(); + + $abilities_with_output = array( + 'scf/list-taxonomies', + 'scf/get-taxonomy', + 'scf/create-taxonomy', + 'scf/update-taxonomy', + 'scf/duplicate-taxonomy', + 'scf/export-taxonomy', + 'scf/import-taxonomy', + ); + + foreach ( $abilities_with_output as $ability_name ) { + $this->assertArrayHasKey( $ability_name, $mock_registered_abilities ); + $this->assertArrayHasKey( + 'output_schema', + $mock_registered_abilities[ $ability_name ], + "Ability $ability_name should have output_schema" + ); + } + } + + /** + * Test delete ability returns boolean success schema + */ + public function test_delete_ability_has_boolean_output_schema() { + global $mock_registered_abilities; + $mock_registered_abilities = array(); + + $this->abilities->register_abilities(); + + $this->assertArrayHasKey( 'scf/delete-taxonomy', $mock_registered_abilities ); + $ability = $mock_registered_abilities['scf/delete-taxonomy']; + $this->assertArrayHasKey( 'output_schema', $ability ); + // Delete returns boolean true on success. + $this->assertEquals( 'boolean', $ability['output_schema']['type'] ); + } + // Callback tests. /** @@ -392,7 +491,7 @@ public function test_import_taxonomy_callback_returns_correct_title() { $this->assertEquals( $this->test_taxonomy['title'], $result['title'] ); } - // Schema tests. + // Schema method tests. /** * Test get_entity_schema returns valid schema @@ -440,7 +539,64 @@ public function test_get_scf_identifier_schema_returns_array() { $this->assertArrayHasKey( 'description', $schema ); } - // Helper method tests. + /** + * Test get_internal_fields_schema returns valid schema + */ + public function test_get_internal_fields_schema_returns_array() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'get_internal_fields_schema' ); + $method->setAccessible( true ); + + $schema = $method->invoke( $this->abilities ); + + $this->assertIsArray( $schema ); + $this->assertArrayHasKey( 'properties', $schema ); + } + + /** + * Test get_internal_fields_schema contains ID property + */ + public function test_get_internal_fields_schema_has_id_property() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'get_internal_fields_schema' ); + $method->setAccessible( true ); + + $schema = $method->invoke( $this->abilities ); + + $this->assertArrayHasKey( 'ID', $schema['properties'] ); + } + + /** + * Test get_entity_with_internal_fields_schema returns merged schema + */ + public function test_get_entity_with_internal_fields_schema_returns_array() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'get_entity_with_internal_fields_schema' ); + $method->setAccessible( true ); + + $schema = $method->invoke( $this->abilities ); + + $this->assertIsArray( $schema ); + $this->assertArrayHasKey( 'properties', $schema ); + } + + /** + * Test get_entity_with_internal_fields_schema contains both entity and internal fields + */ + public function test_get_entity_with_internal_fields_schema_has_merged_properties() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'get_entity_with_internal_fields_schema' ); + $method->setAccessible( true ); + + $schema = $method->invoke( $this->abilities ); + + // Should have entity-specific field (taxonomy has 'taxonomy' field). + $this->assertArrayHasKey( 'taxonomy', $schema['properties'], 'Should have entity-specific taxonomy field' ); + // Should have internal field (ID). + $this->assertArrayHasKey( 'ID', $schema['properties'], 'Should have internal ID field' ); + } + + // Private method tests. /** * Test not_found_error returns correct error code @@ -469,4 +625,204 @@ public function test_not_found_error_returns_404() { $this->assertEquals( 404, $error_data['status'] ); } + + /** + * Test entity_name returns correct value for taxonomy + */ + public function test_entity_name_returns_taxonomy() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'entity_name' ); + $method->setAccessible( true ); + + $this->assertEquals( 'taxonomy', $method->invoke( $this->abilities ) ); + } + + /** + * Test entity_name_plural returns correct value for taxonomy + */ + public function test_entity_name_plural_returns_taxonomies() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'entity_name_plural' ); + $method->setAccessible( true ); + + $this->assertEquals( 'taxonomies', $method->invoke( $this->abilities ) ); + } + + /** + * Test schema_name returns correct schema file name + */ + public function test_schema_name_returns_taxonomy() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'schema_name' ); + $method->setAccessible( true ); + + $this->assertEquals( 'taxonomy', $method->invoke( $this->abilities ) ); + } + + /** + * Test ability_category returns correct category + */ + public function test_ability_category_returns_scf_taxonomies() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'ability_category' ); + $method->setAccessible( true ); + + $this->assertEquals( 'scf-taxonomies', $method->invoke( $this->abilities ) ); + } + + /** + * Test ability_name uses plural for list action + */ + public function test_ability_name_uses_plural_for_list() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'ability_name' ); + $method->setAccessible( true ); + + $this->assertEquals( 'scf/list-taxonomies', $method->invoke( $this->abilities, 'list' ) ); + } + + /** + * Test ability_name uses singular for non-list actions + */ + public function test_ability_name_uses_singular_for_get() { + $reflection = new ReflectionClass( $this->abilities ); + $method = $reflection->getMethod( 'ability_name' ); + $method->setAccessible( true ); + + $this->assertEquals( 'scf/get-taxonomy', $method->invoke( $this->abilities, 'get' ) ); + } + + /** + * Test instance() method caches result + */ + public function test_instance_caches_result() { + $reflection = new ReflectionClass( SCF_Internal_Post_Type_Abilities::class ); + + // Access the private $instance property from base class. + $property = $reflection->getProperty( 'instance' ); + $property->setAccessible( true ); + + // Reset instance to null for this test. + $property->setValue( $this->abilities, null ); + + // Initially should be null after reset. + $this->assertNull( $property->getValue( $this->abilities ), 'Instance should be null after reset' ); + + // Call instance() method. + $method = $reflection->getMethod( 'instance' ); + $method->setAccessible( true ); + $first_call = $method->invoke( $this->abilities ); + + // Should now be cached. + $cached = $property->getValue( $this->abilities ); + $this->assertNotNull( $cached, 'Instance should be cached after first call' ); + $this->assertSame( $first_call, $cached, 'Cached value should match first call result' ); + + // Second call should return same cached instance. + $second_call = $method->invoke( $this->abilities ); + $this->assertSame( $first_call, $second_call, 'Second call should return cached instance' ); + } + + // Mocked callback tests. + + /** + * Test create_callback returns error for duplicate key + */ + public function test_create_callback_returns_error_for_duplicate_key() { + $this->inject_mock_instance( + array( + 'get_post' => array( + 'ID' => 123, + 'key' => 'existing_key', + ), + ) + ); + + $result = $this->abilities->create_callback( array( 'key' => 'existing_key' ) ); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertEquals( 'already_exists', $result->get_error_code() ); + } + + /** + * Test create_callback succeeds when no duplicate + */ + public function test_create_callback_success() { + $created_entity = array( + 'ID' => 456, + 'key' => 'new_key', + 'title' => 'New Taxonomy', + ); + + $this->inject_mock_instance( + array( + 'get_post' => null, + 'update_post' => $created_entity, + ) + ); + + $result = $this->abilities->create_callback( + array( + 'key' => 'new_key', + 'title' => 'New Taxonomy', + ) + ); + + $this->assertIsArray( $result ); + $this->assertEquals( 456, $result['ID'] ); + $this->assertEquals( 'new_key', $result['key'] ); + } + + /** + * Test update_callback succeeds with valid entity + */ + public function test_update_callback_success() { + $existing_entity = array( + 'ID' => 123, + 'key' => 'test_key', + 'title' => 'Old Title', + ); + $updated_entity = array( + 'ID' => 123, + 'key' => 'test_key', + 'title' => 'New Title', + ); + + $this->inject_mock_instance( + array( + 'get_post' => $existing_entity, + 'update_post' => $updated_entity, + ) + ); + + $result = $this->abilities->update_callback( + array( + 'ID' => 123, + 'title' => 'New Title', + ) + ); + + $this->assertIsArray( $result ); + $this->assertEquals( 123, $result['ID'] ); + $this->assertEquals( 'New Title', $result['title'] ); + } + + /** + * Test delete_callback succeeds when entity exists + */ + public function test_delete_callback_success() { + $this->inject_mock_instance( + array( + 'get_post' => array( + 'ID' => 123, + 'key' => 'test_key', + ), + 'delete_post' => true, + ) + ); + + $result = $this->abilities->delete_callback( array( 'identifier' => 123 ) ); + + $this->assertTrue( $result ); + } } From 9dd8b30e9deb2547c56bb08bff09f161d7bb410b Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:08:48 +0100 Subject: [PATCH 08/14] Test errors in callbacks --- .../SCFInternalPostTypeAbilitiesTest.php | 233 +++++++++++++++++- 1 file changed, 226 insertions(+), 7 deletions(-) diff --git a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php index 00d995f4..1b5ec726 100644 --- a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php +++ b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php @@ -76,6 +76,21 @@ private function inject_mock_instance( array $method_returns ) { return $mock_instance; } + /** + * Helper to assert a callback returns a specific WP_Error code. + * + * @param array $mock_returns Mock method return values. + * @param callable $callback The callback to invoke. + * @param array $input Input for the callback. + * @param string $expected_code Expected error code. + */ + private function assert_callback_error( array $mock_returns, callable $callback, array $input, string $expected_code ) { + $this->inject_mock_instance( $mock_returns ); + $result = $callback( $input ); + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertEquals( $expected_code, $result->get_error_code() ); + } + // Constructor tests. /** @@ -729,19 +744,17 @@ public function test_instance_caches_result() { * Test create_callback returns error for duplicate key */ public function test_create_callback_returns_error_for_duplicate_key() { - $this->inject_mock_instance( + $this->assert_callback_error( array( 'get_post' => array( 'ID' => 123, 'key' => 'existing_key', ), - ) + ), + array( $this->abilities, 'create_callback' ), + array( 'key' => 'existing_key' ), + 'already_exists' ); - - $result = $this->abilities->create_callback( array( 'key' => 'existing_key' ) ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'already_exists', $result->get_error_code() ); } /** @@ -825,4 +838,210 @@ public function test_delete_callback_success() { $this->assertTrue( $result ); } + + /** + * Test duplicate_callback succeeds when entity exists + */ + public function test_duplicate_callback_success() { + $existing_entity = array( + 'ID' => 123, + 'key' => 'test_key', + ); + $duplicated_entity = array( + 'ID' => 456, + 'key' => 'test_key_copy', + ); + + $this->inject_mock_instance( + array( + 'get_post' => $existing_entity, + 'duplicate_post' => $duplicated_entity, + ) + ); + + $result = $this->abilities->duplicate_callback( array( 'identifier' => 123 ) ); + + $this->assertIsArray( $result ); + $this->assertEquals( 456, $result['ID'] ); + } + + /** + * Test duplicate_callback returns error when duplication fails + */ + public function test_duplicate_callback_returns_error_on_failure() { + $this->assert_callback_error( + array( + 'get_post' => array( + 'ID' => 123, + 'key' => 'test_key', + ), + 'duplicate_post' => false, + ), + array( $this->abilities, 'duplicate_callback' ), + array( 'identifier' => 123 ), + 'duplicate_failed' + ); + } + + /** + * Test export_callback succeeds when entity exists + */ + public function test_export_callback_success() { + $existing_entity = array( + 'ID' => 123, + 'key' => 'test_key', + 'title' => 'Test Taxonomy', + ); + $exported_data = array( + 'key' => 'test_key', + 'title' => 'Test Taxonomy', + ); + + $this->inject_mock_instance( + array( + 'get_post' => $existing_entity, + 'prepare_post_for_export' => $exported_data, + ) + ); + + $result = $this->abilities->export_callback( array( 'identifier' => 123 ) ); + + $this->assertIsArray( $result ); + $this->assertEquals( 'test_key', $result['key'] ); + } + + /** + * Test export_callback returns error when export fails + */ + public function test_export_callback_returns_error_on_failure() { + $this->assert_callback_error( + array( + 'get_post' => array( + 'ID' => 123, + 'key' => 'test_key', + ), + 'prepare_post_for_export' => false, + ), + array( $this->abilities, 'export_callback' ), + array( 'identifier' => 123 ), + 'export_failed' + ); + } + + /** + * Test create_callback returns error when creation fails + */ + public function test_create_callback_returns_error_on_failure() { + $this->assert_callback_error( + array( + 'get_post' => null, + 'update_post' => false, + ), + array( $this->abilities, 'create_callback' ), + array( 'key' => 'new_key' ), + 'create_failed' + ); + } + + /** + * Test update_callback returns error when update fails + */ + public function test_update_callback_returns_error_on_failure() { + $this->assert_callback_error( + array( + 'get_post' => array( + 'ID' => 123, + 'key' => 'test_key', + ), + 'update_post' => false, + ), + array( $this->abilities, 'update_callback' ), + array( + 'ID' => 123, + 'title' => 'New Title', + ), + 'update_failed' + ); + } + + /** + * Test delete_callback returns error when deletion fails + */ + public function test_delete_callback_returns_error_on_failure() { + $this->assert_callback_error( + array( + 'get_post' => array( + 'ID' => 123, + 'key' => 'test_key', + ), + 'delete_post' => false, + ), + array( $this->abilities, 'delete_callback' ), + array( 'identifier' => 123 ), + 'delete_failed' + ); + } + + /** + * Test import_callback returns error when import fails + */ + public function test_import_callback_returns_error_on_failure() { + $this->assert_callback_error( + array( 'import_post' => false ), + array( $this->abilities, 'import_callback' ), + array( 'key' => 'test_key' ), + 'import_failed' + ); + } + + /** + * Test get_callback returns entity when found + */ + public function test_get_callback_success() { + $entity = array( + 'ID' => 123, + 'key' => 'test_key', + 'title' => 'Test Taxonomy', + ); + + $this->inject_mock_instance( + array( + 'get_post' => $entity, + ) + ); + + $result = $this->abilities->get_callback( array( 'identifier' => 123 ) ); + + $this->assertIsArray( $result ); + $this->assertEquals( 123, $result['ID'] ); + $this->assertEquals( 'test_key', $result['key'] ); + } + + /** + * Test list_callback returns filtered entities + */ + public function test_list_callback_success() { + $entities = array( + array( + 'ID' => 1, + 'key' => 'tax_1', + ), + array( + 'ID' => 2, + 'key' => 'tax_2', + ), + ); + + $this->inject_mock_instance( + array( + 'get_posts' => $entities, + 'filter_posts' => $entities, + ) + ); + + $result = $this->abilities->list_callback( array() ); + + $this->assertIsArray( $result ); + $this->assertCount( 2, $result ); + } } From f65a39c515699f239d96a048064c9d86340007cb Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:22:38 +0100 Subject: [PATCH 09/14] Fix tests to properly use GET methods for readonly abilities --- .../e2e/abilities-internal-post-types.spec.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/e2e/abilities-internal-post-types.spec.ts b/tests/e2e/abilities-internal-post-types.spec.ts index 3e94cd26..97231464 100644 --- a/tests/e2e/abilities-internal-post-types.spec.ts +++ b/tests/e2e/abilities-internal-post-types.spec.ts @@ -104,16 +104,23 @@ function createApiHelpers( entityType ) { return { list: ( requestUtils, filter = {} ) => requestUtils.rest( { - method: 'POST', + method: 'GET', path: `${ ABILITIES_BASE }/scf/list-${ slugPlural }/run`, - data: { input: { filter } }, + params: Object.keys( filter ).length + ? Object.fromEntries( + Object.entries( filter ).map( ( [ key, value ] ) => [ + `input[filter][${ key }]`, + value, + ] ) + ) + : {}, } ), get: ( requestUtils, identifier ) => requestUtils.rest( { - method: 'POST', + method: 'GET', path: `${ ABILITIES_BASE }/scf/get-${ slug }/run`, - data: { input: { identifier } }, + params: { 'input[identifier]': identifier }, } ), create: ( requestUtils, data = testEntity ) => @@ -189,7 +196,7 @@ for ( const entityType of ENTITY_TYPES ) { ); } ); - // List entities - POST with body + // List entities - GET with query params (readonly) test.describe( `scf/list-${ slugPlural }`, () => { test.beforeEach( async ( { requestUtils } ) => { @@ -221,7 +228,7 @@ for ( const entityType of ENTITY_TYPES ) { } ); } ); - // Get entity - POST with body + // Get entity - GET with query params (readonly) test.describe( `scf/get-${ slug }`, () => { test.beforeEach( async ( { requestUtils } ) => { From d64b9d2e2892f8e1f670a97ec4fb1484189d0509 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:23:19 +0100 Subject: [PATCH 10/14] Improve strings for easier translations --- ...class-scf-internal-post-type-abilities.php | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/includes/abilities/class-scf-internal-post-type-abilities.php b/includes/abilities/class-scf-internal-post-type-abilities.php index d81151ea..53e9cd7b 100644 --- a/includes/abilities/class-scf-internal-post-type-abilities.php +++ b/includes/abilities/class-scf-internal-post-type-abilities.php @@ -246,7 +246,7 @@ private function register_list_ability() { ), 'description' => sprintf( /* translators: %s: Entity type plural */ - __( 'Retrieves a list of all SCF %s with optional filtering.', 'secure-custom-fields' ), + __( 'Retrieves a list of SCF %s with optional filtering.', 'secure-custom-fields' ), $this->entity_name_plural() ), 'category' => $this->ability_category(), @@ -266,11 +266,7 @@ private function register_list_ability() { 'properties' => array( 'filter' => array( 'type' => 'object', - 'description' => sprintf( - /* translators: %s: Entity type */ - __( 'Optional filters to apply to the %s list.', 'secure-custom-fields' ), - $this->entity_name() - ), + 'description' => __( 'Optional filters to apply to results.', 'secure-custom-fields' ), 'properties' => array( 'active' => array( 'type' => 'boolean', @@ -304,7 +300,7 @@ private function register_get_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Retrieves a specific SCF %s configuration by ID or key.', 'secure-custom-fields' ), + __( 'Retrieves SCF %s configuration by ID or key.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -347,7 +343,7 @@ private function register_create_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Creates a new custom %s in SCF with the provided configuration.', 'secure-custom-fields' ), + __( 'Creates a new instance of SCF %s with provided configuration.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -387,7 +383,7 @@ private function register_update_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Updates an existing SCF %s with new configuration.', 'secure-custom-fields' ), + __( 'Updates an existing instance of SCF %s with new configuration.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -424,7 +420,7 @@ private function register_delete_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Permanently deletes an SCF %s. This action cannot be undone.', 'secure-custom-fields' ), + __( 'Permanently deletes an instance of SCF %s. This action cannot be undone.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -450,7 +446,7 @@ private function register_delete_ability() { 'type' => 'boolean', 'description' => sprintf( /* translators: %s: Entity type */ - __( 'True if %s was successfully deleted.', 'secure-custom-fields' ), + __( 'True if %s was deleted successfully.', 'secure-custom-fields' ), $this->entity_name() ), ), @@ -474,7 +470,7 @@ private function register_duplicate_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Creates a copy of an existing SCF %s. The duplicate receives a new unique key.', 'secure-custom-fields' ), + __( 'Creates a copy of SCF %s. Duplicate receives a new unique key.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -495,11 +491,7 @@ private function register_duplicate_ability() { 'identifier' => $this->get_scf_identifier_schema(), 'new_post_id' => array( 'type' => 'integer', - 'description' => sprintf( - /* translators: %s: Entity type */ - __( 'Optional new post ID for the duplicated %s.', 'secure-custom-fields' ), - $this->entity_name() - ), + 'description' => __( 'Optional post ID for duplicate.', 'secure-custom-fields' ), ), ), 'required' => array( 'identifier' ), @@ -525,7 +517,7 @@ private function register_export_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Exports an SCF %s configuration as JSON for backup or transfer.', 'secure-custom-fields' ), + __( 'Exports an instance of SCF %s as JSON for backup or transfer.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -568,7 +560,7 @@ private function register_import_ability() { ), 'description' => sprintf( /* translators: %s: Entity type */ - __( 'Imports an SCF %s from JSON configuration data.', 'secure-custom-fields' ), + __( 'Imports an instance of SCF %s from JSON data.', 'secure-custom-fields' ), $this->entity_name() ), 'category' => $this->ability_category(), @@ -628,8 +620,8 @@ public function create_callback( $input ) { 'already_exists', sprintf( /* translators: %s: Entity type */ - __( 'A %s with this key already exists.', 'secure-custom-fields' ), - $this->entity_name() + __( '%s with this key already exists.', 'secure-custom-fields' ), + ucfirst( $this->entity_name() ) ) ); } @@ -743,7 +735,7 @@ public function export_callback( $input ) { 'export_failed', sprintf( /* translators: %s: Entity type */ - __( 'Failed to prepare %s for export.', 'secure-custom-fields' ), + __( 'Failed to export %s.', 'secure-custom-fields' ), $this->entity_name() ) ); From 328c5e1f361c98976143941e956c8f56a7c7d338 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:33:44 +0100 Subject: [PATCH 11/14] Add tests for the constructor --- .../SCFInternalPostTypeAbilitiesTest.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php index 1b5ec726..2fda61b7 100644 --- a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php +++ b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php @@ -108,6 +108,40 @@ public function test_constructor_registers_action_hooks() { ); } + /** + * Test categories are registered when wp_abilities_api_categories_init fires + */ + public function test_categories_registered_on_action() { + global $mock_registered_ability_categories; + $mock_registered_ability_categories = array(); + + // Trigger the action that constructor hooked into. + do_action( 'wp_abilities_api_categories_init' ); + + $this->assertArrayHasKey( + 'scf-taxonomies', + $mock_registered_ability_categories, + 'Category should be registered when action fires' + ); + } + + /** + * Test abilities are registered when wp_abilities_api_init fires + */ + public function test_abilities_registered_on_action() { + global $mock_registered_abilities; + $mock_registered_abilities = array(); + + // Trigger the action that constructor hooked into. + do_action( 'wp_abilities_api_init' ); + + $this->assertArrayHasKey( + 'scf/list-taxonomies', + $mock_registered_abilities, + 'Abilities should be registered when action fires' + ); + } + /** * Test constructor does not register hooks when schema validation fails */ @@ -145,6 +179,28 @@ public function validate_required_schemas() { } } + /** + * Test constructor triggers _doing_it_wrong when internal_post_type is empty + * + * @expectedIncorrectUsage SCF_Internal_Post_Type_Abilities::__construct + */ + public function test_constructor_doing_it_wrong_when_internal_post_type_empty() { + // Create anonymous class that doesn't set internal_post_type. + $test_instance = new class() extends SCF_Internal_Post_Type_Abilities { + // Intentionally not setting $internal_post_type to trigger _doing_it_wrong. + }; + + // Verify NO hooks were registered since constructor returned early. + $this->assertFalse( + has_action( 'wp_abilities_api_categories_init', array( $test_instance, 'register_categories' ) ), + 'Should NOT register hooks when internal_post_type is empty' + ); + $this->assertFalse( + has_action( 'wp_abilities_api_init', array( $test_instance, 'register_abilities' ) ), + 'Should NOT register abilities hook when internal_post_type is empty' + ); + } + // Registration tests. /** From 9e85229ddca68dffbab9cd66ba138b35ce592475 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:43:14 +0100 Subject: [PATCH 12/14] Fix e2e tets for readonly abilities --- .../e2e/abilities-internal-post-types.spec.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/e2e/abilities-internal-post-types.spec.ts b/tests/e2e/abilities-internal-post-types.spec.ts index 97231464..70599f30 100644 --- a/tests/e2e/abilities-internal-post-types.spec.ts +++ b/tests/e2e/abilities-internal-post-types.spec.ts @@ -4,10 +4,13 @@ * Tests the WordPress Abilities API endpoints for SCF post type and taxonomy management. * Both entity types share the same base class, so tests are parameterized. * - * HTTP Method Reference (per PR #152 in WordPress/abilities-api): - * - Read-only abilities (readonly: true) → GET with query params - * - Regular abilities (readonly: false, destructive: false) → POST with body - * - Destructive abilities (destructive: true) → DELETE with query params + * HTTP Method Reference: + * - Read-only abilities (readonly: true) → GET with bracket notation: { 'input[key]': value } + * - Regular abilities (readonly: false, destructive: false) → POST with JSON body: { input: { key: value } } + * - Destructive abilities (destructive: true) → DELETE with bracket notation: { 'input[key]': value } + * + * Note: GET/DELETE use bracket notation because PHP parses `?input[key]=val` into arrays. + * JSON.stringify does NOT work for query params - PHP receives a string, not an object. */ const { test, expect } = require( './fixtures' ); @@ -102,19 +105,17 @@ function createApiHelpers( entityType ) { const { slug, slugPlural, testEntity } = entityType; return { - list: ( requestUtils, filter = {} ) => - requestUtils.rest( { + list: ( requestUtils, filter = {} ) => { + const params = { 'input[filter]': '' }; + Object.entries( filter ).forEach( ( [ key, value ] ) => { + params[ `input[filter][${ key }]` ] = value; + } ); + return requestUtils.rest( { method: 'GET', path: `${ ABILITIES_BASE }/scf/list-${ slugPlural }/run`, - params: Object.keys( filter ).length - ? Object.fromEntries( - Object.entries( filter ).map( ( [ key, value ] ) => [ - `input[filter][${ key }]`, - value, - ] ) - ) - : {}, - } ), + params, + } ); + }, get: ( requestUtils, identifier ) => requestUtils.rest( { From 3c2394ff6fca64c00f0da1b146c4a2c2efd1ff09 Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:45:53 +0100 Subject: [PATCH 13/14] Consolidate PHPUnit tests --- .../SCFInternalPostTypeAbilitiesTest.php | 154 ++++-------------- 1 file changed, 30 insertions(+), 124 deletions(-) diff --git a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php index 2fda61b7..9eac53b6 100644 --- a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php +++ b/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php @@ -95,15 +95,21 @@ private function assert_callback_error( array $mock_returns, callable $callback, /** * Test constructor registers WordPress action hooks + * + * Creates a fresh instance to ensure the add_action lines + * in the constructor are covered by code coverage tools. */ public function test_constructor_registers_action_hooks() { - // The constructor should have registered these action hooks. + // Create a fresh instance - this executes the constructor including add_action lines. + $fresh_instance = new SCF_Taxonomy_Abilities(); + + // Verify the hooks were registered for this specific instance. $this->assertNotFalse( - has_action( 'wp_abilities_api_categories_init', array( $this->abilities, 'register_categories' ) ), + has_action( 'wp_abilities_api_categories_init', array( $fresh_instance, 'register_categories' ) ), 'Should register wp_abilities_api_categories_init action' ); $this->assertNotFalse( - has_action( 'wp_abilities_api_init', array( $this->abilities, 'register_abilities' ) ), + has_action( 'wp_abilities_api_init', array( $fresh_instance, 'register_abilities' ) ), 'Should register wp_abilities_api_init action' ); } @@ -423,48 +429,27 @@ public function test_list_taxonomies_callback_with_filter() { } /** - * Test get_callback returns WP_Error for non-existent ID + * Test get_callback returns WP_Error with not_found code and 404 status for non-existent entity */ - public function test_get_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->get_callback( - array( 'identifier' => 999999 ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test get_callback returns 404 status for non-existent key - */ - public function test_get_taxonomy_callback_not_found_returns_404_status() { + public function test_get_taxonomy_callback_not_found() { $result = $this->abilities->get_callback( array( 'identifier' => 'nonexistent_taxonomy' ) ); $this->assertInstanceOf( WP_Error::class, $result ); - $error_data = $result->get_error_data(); - $this->assertEquals( 404, $error_data['status'] ); + $this->assertEquals( 'not_found', $result->get_error_code() ); + $this->assertEquals( 404, $result->get_error_data()['status'] ); } /** - * Test create_callback returns array with key + * Test create_callback returns array with expected fields */ - public function test_create_taxonomy_callback_returns_array_with_key() { + public function test_create_taxonomy_callback_returns_expected_fields() { $result = $this->abilities->create_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'key', $result ); $this->assertEquals( $this->test_taxonomy['key'], $result['key'] ); - } - - /** - * Test create_callback returns array with title - */ - public function test_create_taxonomy_callback_returns_array_with_title() { - $result = $this->abilities->create_callback( $this->test_taxonomy ); - - $this->assertIsArray( $result ); $this->assertArrayHasKey( 'title', $result ); $this->assertEquals( $this->test_taxonomy['title'], $result['title'] ); } @@ -485,28 +470,16 @@ public function test_update_taxonomy_callback_not_found_returns_error() { } /** - * Test delete_callback returns WP_Error for non-existent ID - */ - public function test_delete_taxonomy_callback_not_found_returns_error() { - $result = $this->abilities->delete_callback( - array( 'identifier' => 999999 ) - ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'not_found', $result->get_error_code() ); - } - - /** - * Test delete_callback returns 404 status for non-existent key + * Test delete_callback returns WP_Error with not_found code and 404 status for non-existent entity */ - public function test_delete_taxonomy_callback_not_found_returns_404_status() { + public function test_delete_taxonomy_callback_not_found() { $result = $this->abilities->delete_callback( array( 'identifier' => 'nonexistent_delete_target' ) ); $this->assertInstanceOf( WP_Error::class, $result ); - $error_data = $result->get_error_data(); - $this->assertEquals( 404, $error_data['status'] ); + $this->assertEquals( 'not_found', $result->get_error_code() ); + $this->assertEquals( 404, $result->get_error_data()['status'] ); } /** @@ -534,40 +507,22 @@ public function test_export_taxonomy_callback_not_found_returns_error() { } /** - * Test import_callback returns array + * Test import_callback returns array with expected fields */ - public function test_import_taxonomy_callback_returns_array() { - $result = $this->abilities->import_callback( $this->test_taxonomy ); - - $this->assertIsArray( $result ); - } - - /** - * Test import_callback returns correct taxonomy - */ - public function test_import_taxonomy_callback_returns_correct_taxonomy() { + public function test_import_taxonomy_callback_returns_expected_fields() { $result = $this->abilities->import_callback( $this->test_taxonomy ); $this->assertIsArray( $result ); $this->assertEquals( $this->test_taxonomy['taxonomy'], $result['taxonomy'] ); - } - - /** - * Test import_callback returns correct title - */ - public function test_import_taxonomy_callback_returns_correct_title() { - $result = $this->abilities->import_callback( $this->test_taxonomy ); - - $this->assertIsArray( $result ); $this->assertEquals( $this->test_taxonomy['title'], $result['title'] ); } // Schema method tests. /** - * Test get_entity_schema returns valid schema + * Test get_entity_schema returns valid schema with required fields */ - public function test_get_entity_schema_returns_array() { + public function test_get_entity_schema() { $reflection = new ReflectionClass( $this->abilities ); $method = $reflection->getMethod( 'get_entity_schema' ); $method->setAccessible( true ); @@ -577,18 +532,6 @@ public function test_get_entity_schema_returns_array() { $this->assertIsArray( $schema ); $this->assertArrayHasKey( 'type', $schema ); $this->assertEquals( 'object', $schema['type'] ); - } - - /** - * Test get_entity_schema has required fields - */ - public function test_get_entity_schema_has_required_fields() { - $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'get_entity_schema' ); - $method->setAccessible( true ); - - $schema = $method->invoke( $this->abilities ); - $this->assertArrayHasKey( 'required', $schema ); $this->assertContains( 'key', $schema['required'] ); $this->assertContains( 'title', $schema['required'] ); @@ -611,9 +554,9 @@ public function test_get_scf_identifier_schema_returns_array() { } /** - * Test get_internal_fields_schema returns valid schema + * Test get_internal_fields_schema returns valid schema with ID property */ - public function test_get_internal_fields_schema_returns_array() { + public function test_get_internal_fields_schema() { $reflection = new ReflectionClass( $this->abilities ); $method = $reflection->getMethod( 'get_internal_fields_schema' ); $method->setAccessible( true ); @@ -622,25 +565,13 @@ public function test_get_internal_fields_schema_returns_array() { $this->assertIsArray( $schema ); $this->assertArrayHasKey( 'properties', $schema ); - } - - /** - * Test get_internal_fields_schema contains ID property - */ - public function test_get_internal_fields_schema_has_id_property() { - $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'get_internal_fields_schema' ); - $method->setAccessible( true ); - - $schema = $method->invoke( $this->abilities ); - $this->assertArrayHasKey( 'ID', $schema['properties'] ); } /** - * Test get_entity_with_internal_fields_schema returns merged schema + * Test get_entity_with_internal_fields_schema returns merged schema with both entity and internal fields */ - public function test_get_entity_with_internal_fields_schema_returns_array() { + public function test_get_entity_with_internal_fields_schema() { $reflection = new ReflectionClass( $this->abilities ); $method = $reflection->getMethod( 'get_entity_with_internal_fields_schema' ); $method->setAccessible( true ); @@ -649,18 +580,6 @@ public function test_get_entity_with_internal_fields_schema_returns_array() { $this->assertIsArray( $schema ); $this->assertArrayHasKey( 'properties', $schema ); - } - - /** - * Test get_entity_with_internal_fields_schema contains both entity and internal fields - */ - public function test_get_entity_with_internal_fields_schema_has_merged_properties() { - $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'get_entity_with_internal_fields_schema' ); - $method->setAccessible( true ); - - $schema = $method->invoke( $this->abilities ); - // Should have entity-specific field (taxonomy has 'taxonomy' field). $this->assertArrayHasKey( 'taxonomy', $schema['properties'], 'Should have entity-specific taxonomy field' ); // Should have internal field (ID). @@ -670,9 +589,9 @@ public function test_get_entity_with_internal_fields_schema_has_merged_propertie // Private method tests. /** - * Test not_found_error returns correct error code + * Test not_found_error returns WP_Error with correct code and 404 status */ - public function test_not_found_error_returns_correct_code() { + public function test_not_found_error() { $reflection = new ReflectionClass( $this->abilities ); $method = $reflection->getMethod( 'not_found_error' ); $method->setAccessible( true ); @@ -681,20 +600,7 @@ public function test_not_found_error_returns_correct_code() { $this->assertInstanceOf( WP_Error::class, $error ); $this->assertEquals( 'not_found', $error->get_error_code() ); - } - - /** - * Test not_found_error returns 404 status - */ - public function test_not_found_error_returns_404() { - $reflection = new ReflectionClass( $this->abilities ); - $method = $reflection->getMethod( 'not_found_error' ); - $method->setAccessible( true ); - - $error = $method->invoke( $this->abilities ); - $error_data = $error->get_error_data(); - - $this->assertEquals( 404, $error_data['status'] ); + $this->assertEquals( 404, $error->get_error_data()['status'] ); } /** From 21f9fa9c6edebf7332da62539dc13d46fcc725bd Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Sat, 29 Nov 2025 01:38:30 +0100 Subject: [PATCH 14/14] Lint files --- ...ties-integration.php => class-scf-abilities-integration.php} | 0 includes/abilities/class-scf-post-type-abilities.php | 2 +- secure-custom-fields.php | 2 +- ...litiesTest.php => test-scf-internal-post-type-abilities.php} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename includes/abilities/{abilities-integration.php => class-scf-abilities-integration.php} (100%) rename tests/php/includes/abilities/{SCFInternalPostTypeAbilitiesTest.php => test-scf-internal-post-type-abilities.php} (99%) diff --git a/includes/abilities/abilities-integration.php b/includes/abilities/class-scf-abilities-integration.php similarity index 100% rename from includes/abilities/abilities-integration.php rename to includes/abilities/class-scf-abilities-integration.php diff --git a/includes/abilities/class-scf-post-type-abilities.php b/includes/abilities/class-scf-post-type-abilities.php index 0e40fd07..f93d01ed 100644 --- a/includes/abilities/class-scf-post-type-abilities.php +++ b/includes/abilities/class-scf-post-type-abilities.php @@ -37,4 +37,4 @@ class SCF_Post_Type_Abilities extends SCF_Internal_Post_Type_Abilities { // Initialize abilities instance. acf_new_instance( 'SCF_Post_Type_Abilities' ); -endif; // class_exists check +endif; // class_exists check. diff --git a/secure-custom-fields.php b/secure-custom-fields.php index ae8e1630..09b0c659 100644 --- a/secure-custom-fields.php +++ b/secure-custom-fields.php @@ -190,7 +190,7 @@ public function initialize() { acf_include( 'includes/class-acf-options-page.php' ); acf_include( 'includes/class-acf-site-health.php' ); acf_include( 'includes/class-scf-json-schema-validator.php' ); - acf_include( 'includes/abilities/abilities-integration.php' ); + acf_include( 'includes/abilities/class-scf-abilities-integration.php' ); acf_include( 'includes/fields/class-acf-field.php' ); acf_include( 'includes/locations/abstract-acf-legacy-location.php' ); acf_include( 'includes/locations/abstract-acf-location.php' ); diff --git a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php b/tests/php/includes/abilities/test-scf-internal-post-type-abilities.php similarity index 99% rename from tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php rename to tests/php/includes/abilities/test-scf-internal-post-type-abilities.php index 9eac53b6..da7d210e 100644 --- a/tests/php/includes/abilities/SCFInternalPostTypeAbilitiesTest.php +++ b/tests/php/includes/abilities/test-scf-internal-post-type-abilities.php @@ -26,7 +26,7 @@ * Uses SCF_Taxonomy_Abilities as concrete implementation to test * the shared base class behavior. */ -class SCFInternalPostTypeAbilitiesTest extends BaseTestCase { +class Test_SCF_Internal_Post_Type_Abilities extends BaseTestCase { /** * Instance of SCF_Taxonomy_Abilities for testing