diff --git a/includes/abilities/abilities-integration.php b/includes/abilities/class-scf-abilities-integration.php
similarity index 93%
rename from includes/abilities/abilities-integration.php
rename to includes/abilities/class-scf-abilities-integration.php
index 3a8e3d17..b25e6b1f 100644
--- a/includes/abilities/abilities-integration.php
+++ b/includes/abilities/class-scf-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..53e9cd7b
--- /dev/null
+++ b/includes/abilities/class-scf-internal-post-type-abilities.php
@@ -0,0 +1,785 @@
+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 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' => true,
+ '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 results.', '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_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 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' => 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_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 instance of SCF %s with 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 instance of 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 instance of 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 deleted successfully.', '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 SCF %s. 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' => __( 'Optional post ID for duplicate.', 'secure-custom-fields' ),
+ ),
+ ),
+ '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 instance of SCF %s 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 instance of SCF %s from JSON 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(
+ 'already_exists',
+ sprintf(
+ /* translators: %s: Entity type */
+ __( '%s with this key already exists.', 'secure-custom-fields' ),
+ ucfirst( $this->entity_name() )
+ )
+ );
+ }
+
+ $entity = $this->instance()->update_post( $input );
+ if ( ! $entity ) {
+ return new WP_Error(
+ 'create_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_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_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_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_failed',
+ sprintf(
+ /* translators: %s: Entity type */
+ __( 'Failed to export %s.', '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_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(
+ '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..f93d01ed 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
+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/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/
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/e2e/abilities-internal-post-types.spec.ts b/tests/e2e/abilities-internal-post-types.spec.ts
new file mode 100644
index 00000000..70599f30
--- /dev/null
+++ b/tests/e2e/abilities-internal-post-types.spec.ts
@@ -0,0 +1,510 @@
+/**
+ * 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:
+ * - 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' );
+
+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 = {} ) => {
+ 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,
+ } );
+ },
+
+ get: ( requestUtils, identifier ) =>
+ requestUtils.rest( {
+ method: 'GET',
+ path: `${ ABILITIES_BASE }/scf/get-${ slug }/run`,
+ params: { 'input[identifier]': 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 - GET with query params (readonly)
+
+ 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 - GET with query params (readonly)
+
+ 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 13b13b37..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( 'post_type_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 8452d2c3..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( 'taxonomy_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',
- } )
- );
- } );
- } );
-} );
diff --git a/tests/php/includes/abilities/test-scf-internal-post-type-abilities.php b/tests/php/includes/abilities/test-scf-internal-post-type-abilities.php
new file mode 100644
index 00000000..da7d210e
--- /dev/null
+++ b/tests/php/includes/abilities/test-scf-internal-post-type-abilities.php
@@ -0,0 +1,1009 @@
+ 'taxonomy_phpunit_test',
+ 'title' => 'PHPUnit Test Taxonomy',
+ 'taxonomy' => 'phpunit_test',
+ );
+
+ /**
+ * Setup test fixtures
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $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;
+ }
+
+ /**
+ * 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.
+
+ /**
+ * 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() {
+ // 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( $fresh_instance, 'register_categories' ) ),
+ 'Should register wp_abilities_api_categories_init action'
+ );
+ $this->assertNotFalse(
+ has_action( 'wp_abilities_api_init', array( $fresh_instance, 'register_abilities' ) ),
+ 'Should register wp_abilities_api_init action'
+ );
+ }
+
+ /**
+ * 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
+ */
+ public function test_constructor_skips_hooks_when_schemas_invalid() {
+ global $acf_instances;
+
+ // Store original validator.
+ $original_validator = $acf_instances['SCF_JSON_Schema_Validator'] ?? null;
+
+ // Create mock validator that returns false.
+ $mock_validator = new class() {
+ /**
+ * Mock validation that always fails.
+ *
+ * @return bool Always returns false.
+ */
+ public function validate_required_schemas() {
+ return false;
+ }
+ };
+ $acf_instances['SCF_JSON_Schema_Validator'] = $mock_validator;
+
+ // Create a fresh instance (bypassing acf_get_instance cache for this class).
+ $test_instance = new SCF_Taxonomy_Abilities();
+
+ // Verify NO hooks were registered for this instance.
+ $this->assertFalse(
+ has_action( 'wp_abilities_api_categories_init', array( $test_instance, 'register_categories' ) ),
+ 'Should NOT register hooks when schema validation fails'
+ );
+
+ // Restore original validator.
+ if ( $original_validator ) {
+ $acf_instances['SCF_JSON_Schema_Validator'] = $original_validator;
+ }
+ }
+
+ /**
+ * 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.
+
+ /**
+ * Test register_categories registers the scf-taxonomies category
+ */
+ public function test_register_categories_registers_scf_taxonomies() {
+ global $mock_registered_ability_categories;
+ $mock_registered_ability_categories = array();
+
+ $this->abilities->register_categories();
+
+ $this->assertArrayHasKey( 'scf-taxonomies', $mock_registered_ability_categories );
+ $this->assertArrayHasKey( 'label', $mock_registered_ability_categories['scf-taxonomies'] );
+ }
+
+ /**
+ * Test register_abilities registers all expected abilities
+ */
+ public function test_register_abilities_registers_all_abilities() {
+ global $mock_registered_abilities;
+ $mock_registered_abilities = array();
+
+ $this->abilities->register_abilities();
+
+ $expected_abilities = array(
+ 'scf/list-taxonomies',
+ 'scf/get-taxonomy',
+ 'scf/create-taxonomy',
+ 'scf/update-taxonomy',
+ 'scf/delete-taxonomy',
+ 'scf/duplicate-taxonomy',
+ 'scf/export-taxonomy',
+ 'scf/import-taxonomy',
+ );
+
+ foreach ( $expected_abilities as $ability_name ) {
+ $this->assertArrayHasKey( $ability_name, $mock_registered_abilities, "Missing ability: $ability_name" );
+ }
+ }
+
+ /**
+ * Test registered abilities have correct category
+ */
+ public function test_registered_abilities_have_correct_category() {
+ global $mock_registered_abilities;
+ $mock_registered_abilities = array();
+
+ $this->abilities->register_abilities();
+
+ foreach ( $mock_registered_abilities as $name => $args ) {
+ $this->assertEquals(
+ 'scf-taxonomies',
+ $args['category'],
+ "Ability $name has wrong category"
+ );
+ }
+ }
+
+ /**
+ * Test registered abilities have execute callbacks
+ */
+ public function test_registered_abilities_have_execute_callbacks() {
+ global $mock_registered_abilities;
+ $mock_registered_abilities = array();
+
+ $this->abilities->register_abilities();
+
+ foreach ( $mock_registered_abilities as $name => $args ) {
+ $this->assertArrayHasKey( 'execute_callback', $args, "Ability $name missing execute_callback" );
+ $this->assertIsCallable( $args['execute_callback'], "Ability $name execute_callback is not callable" );
+ }
+ }
+
+ /**
+ * Test registered abilities have input schemas
+ */
+ public function test_registered_abilities_have_input_schemas() {
+ global $mock_registered_abilities;
+ $mock_registered_abilities = array();
+
+ $this->abilities->register_abilities();
+
+ foreach ( $mock_registered_abilities as $name => $args ) {
+ $this->assertArrayHasKey( 'input_schema', $args, "Ability $name missing input_schema" );
+ }
+ }
+
+ /**
+ * Test delete ability is marked as destructive
+ */
+ public function test_delete_ability_is_destructive() {
+ 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'];
+ $meta = $ability['meta'] ?? array();
+ $annotations = $meta['annotations'] ?? array();
+ $this->assertTrue( $annotations['destructive'] ?? false, 'Delete ability should be marked destructive' );
+ }
+
+ /**
+ * Test export ability is marked as readonly
+ */
+ public function test_export_ability_is_readonly() {
+ global $mock_registered_abilities;
+ $mock_registered_abilities = array();
+
+ $this->abilities->register_abilities();
+
+ $this->assertArrayHasKey( 'scf/export-taxonomy', $mock_registered_abilities );
+ $ability = $mock_registered_abilities['scf/export-taxonomy'];
+ $meta = $ability['meta'] ?? array();
+ $annotations = $meta['annotations'] ?? array();
+ $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.
+
+ /**
+ * 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_callback( array() );
+
+ $this->assertIsArray( $result );
+ }
+
+ /**
+ * Test list_callback with filter parameter
+ */
+ public function test_list_taxonomies_callback_with_filter() {
+ $result = $this->abilities->list_callback(
+ array( 'filter' => array( 'active' => true ) )
+ );
+
+ $this->assertIsArray( $result );
+ }
+
+ /**
+ * 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() {
+ $result = $this->abilities->get_callback(
+ array( 'identifier' => 'nonexistent_taxonomy' )
+ );
+
+ $this->assertInstanceOf( WP_Error::class, $result );
+ $this->assertEquals( 'not_found', $result->get_error_code() );
+ $this->assertEquals( 404, $result->get_error_data()['status'] );
+ }
+
+ /**
+ * Test create_callback returns array with expected fields
+ */
+ 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'] );
+ $this->assertArrayHasKey( 'title', $result );
+ $this->assertEquals( $this->test_taxonomy['title'], $result['title'] );
+ }
+
+ /**
+ * Test update_callback returns WP_Error for non-existent ID
+ */
+ public function test_update_taxonomy_callback_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 with not_found code and 404 status for non-existent entity
+ */
+ public function test_delete_taxonomy_callback_not_found() {
+ $result = $this->abilities->delete_callback(
+ array( 'identifier' => 'nonexistent_delete_target' )
+ );
+
+ $this->assertInstanceOf( WP_Error::class, $result );
+ $this->assertEquals( 'not_found', $result->get_error_code() );
+ $this->assertEquals( 404, $result->get_error_data()['status'] );
+ }
+
+ /**
+ * Test duplicate_callback returns WP_Error for non-existent ID
+ */
+ public function test_duplicate_taxonomy_callback_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_taxonomy_callback_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 with expected fields
+ */
+ 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'] );
+ $this->assertEquals( $this->test_taxonomy['title'], $result['title'] );
+ }
+
+ // Schema method tests.
+
+ /**
+ * Test get_entity_schema returns valid schema with required fields
+ */
+ public function test_get_entity_schema() {
+ $reflection = new ReflectionClass( $this->abilities );
+ $method = $reflection->getMethod( 'get_entity_schema' );
+ $method->setAccessible( true );
+
+ $schema = $method->invoke( $this->abilities );
+
+ $this->assertIsArray( $schema );
+ $this->assertArrayHasKey( 'type', $schema );
+ $this->assertEquals( 'object', $schema['type'] );
+ $this->assertArrayHasKey( 'required', $schema );
+ $this->assertContains( 'key', $schema['required'] );
+ $this->assertContains( 'title', $schema['required'] );
+ $this->assertContains( 'taxonomy', $schema['required'] );
+ }
+
+ /**
+ * Test get_scf_identifier_schema returns valid schema
+ */
+ public function test_get_scf_identifier_schema_returns_array() {
+ $reflection = new ReflectionClass( $this->abilities );
+ $method = $reflection->getMethod( 'get_scf_identifier_schema' );
+ $method->setAccessible( true );
+
+ $schema = $method->invoke( $this->abilities );
+
+ $this->assertIsArray( $schema );
+ // Should have description and allow integer or string.
+ $this->assertArrayHasKey( 'description', $schema );
+ }
+
+ /**
+ * Test get_internal_fields_schema returns valid schema with ID property
+ */
+ public function test_get_internal_fields_schema() {
+ $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 );
+ $this->assertArrayHasKey( 'ID', $schema['properties'] );
+ }
+
+ /**
+ * 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() {
+ $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 );
+ // 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 WP_Error with correct code and 404 status
+ */
+ public function test_not_found_error() {
+ $reflection = new ReflectionClass( $this->abilities );
+ $method = $reflection->getMethod( 'not_found_error' );
+ $method->setAccessible( true );
+
+ $error = $method->invoke( $this->abilities );
+
+ $this->assertInstanceOf( WP_Error::class, $error );
+ $this->assertEquals( 'not_found', $error->get_error_code() );
+ $this->assertEquals( 404, $error->get_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->assert_callback_error(
+ array(
+ 'get_post' => array(
+ 'ID' => 123,
+ 'key' => 'existing_key',
+ ),
+ ),
+ array( $this->abilities, 'create_callback' ),
+ array( 'key' => 'existing_key' ),
+ 'already_exists'
+ );
+ }
+
+ /**
+ * 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 );
+ }
+
+ /**
+ * 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 );
+ }
+}
diff --git a/tests/php/includes/abilities/test-scf-post-type-abilities.php b/tests/php/includes/abilities/test-scf-post-type-abilities.php
deleted file mode 100644
index db22ce79..00000000
--- a/tests/php/includes/abilities/test-scf-post-type-abilities.php
+++ /dev/null
@@ -1,207 +0,0 @@
- '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_post_types_callback returns array
- */
- public function test_list_post_types_returns_array() {
- $result = $this->abilities->list_post_types_callback( array() );
-
- $this->assertIsArray( $result );
- }
-
- /**
- * Test list_post_types_callback with filter parameter
- */
- public function test_list_post_types_with_filter() {
- $result = $this->abilities->list_post_types_callback(
- array( 'filter' => array( 'active' => true ) )
- );
-
- $this->assertIsArray( $result );
- }
-
- /**
- * Test create_post_type_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 );
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'key', $result );
- $this->assertEquals( $this->test_post_type['key'], $result['key'] );
- }
-
- /**
- * Test create_post_type_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 );
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'title', $result );
- $this->assertEquals( $this->test_post_type['title'], $result['title'] );
- }
-
- /**
- * Test get_post_type_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(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'post_type_not_found', $result->get_error_code() );
- }
-
- /**
- * Test get_post_type_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(
- 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_post_type_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(
- array(
- 'ID' => 999999,
- 'title' => 'Should Fail',
- )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'post_type_not_found', $result->get_error_code() );
- }
-
- /**
- * Test delete_post_type_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(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'post_type_not_found', $result->get_error_code() );
- }
-
- /**
- * Test delete_post_type_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(
- 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_post_type_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(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'post_type_not_found', $result->get_error_code() );
- }
-
- /**
- * Test export_post_type_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(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'post_type_not_found', $result->get_error_code() );
- }
-
- /**
- * Test import_post_type_callback returns array
- */
- public function test_import_post_type_returns_array() {
- $result = $this->abilities->import_post_type_callback( $this->test_post_type );
-
- $this->assertIsArray( $result );
- }
-
- /**
- * Test import_post_type_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 );
-
- $this->assertIsArray( $result );
- $this->assertEquals( $this->test_post_type['post_type'], $result['post_type'] );
- }
-
- /**
- * Test import_post_type_callback returns correct title
- */
- public function test_import_post_type_returns_correct_title() {
- $result = $this->abilities->import_post_type_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
deleted file mode 100644
index efefc293..00000000
--- a/tests/php/includes/abilities/test-scf-taxonomy-abilities.php
+++ /dev/null
@@ -1,466 +0,0 @@
- 'taxonomy_phpunit_test',
- 'title' => 'PHPUnit Test Taxonomy',
- 'taxonomy' => 'phpunit_test',
- );
-
- /**
- * Setup test fixtures
- */
- public function setUp(): void {
- parent::setUp();
- $this->abilities = acf_get_instance( 'SCF_Taxonomy_Abilities' );
- }
-
- // Constructor tests.
-
- /**
- * Test constructor registers WordPress action hooks
- */
- public function test_constructor_registers_action_hooks() {
- // The constructor should have registered these action hooks.
- $this->assertNotFalse(
- has_action( 'wp_abilities_api_categories_init', array( $this->abilities, 'register_categories' ) ),
- 'Should register wp_abilities_api_categories_init action'
- );
- $this->assertNotFalse(
- has_action( 'wp_abilities_api_init', array( $this->abilities, 'register_abilities' ) ),
- 'Should register wp_abilities_api_init action'
- );
- }
-
- /**
- * Test constructor does not register hooks when schema validation fails
- */
- public function test_constructor_skips_hooks_when_schemas_invalid() {
- global $acf_instances;
-
- // Store original validator.
- $original_validator = $acf_instances['SCF_JSON_Schema_Validator'] ?? null;
-
- // Create mock validator that returns false.
- $mock_validator = new class() {
- /**
- * Mock validation that always fails.
- *
- * @return bool Always returns false.
- */
- public function validate_required_schemas() {
- return false;
- }
- };
- $acf_instances['SCF_JSON_Schema_Validator'] = $mock_validator;
-
- // Create a fresh instance (bypassing acf_get_instance cache for this class).
- $test_instance = new SCF_Taxonomy_Abilities();
-
- // Verify NO hooks were registered for this instance.
- $this->assertFalse(
- has_action( 'wp_abilities_api_categories_init', array( $test_instance, 'register_categories' ) ),
- 'Should NOT register hooks when schema validation fails'
- );
-
- // Restore original validator.
- if ( $original_validator ) {
- $acf_instances['SCF_JSON_Schema_Validator'] = $original_validator;
- }
- }
-
- // Registration tests.
-
- /**
- * Test register_categories registers the scf-taxonomies category
- */
- public function test_register_categories_registers_scf_taxonomies() {
- global $mock_registered_ability_categories;
- $mock_registered_ability_categories = array();
-
- $this->abilities->register_categories();
-
- $this->assertArrayHasKey( 'scf-taxonomies', $mock_registered_ability_categories );
- $this->assertArrayHasKey( 'label', $mock_registered_ability_categories['scf-taxonomies'] );
- }
-
- /**
- * Test register_abilities registers all expected abilities
- */
- public function test_register_abilities_registers_all_abilities() {
- global $mock_registered_abilities;
- $mock_registered_abilities = array();
-
- $this->abilities->register_abilities();
-
- $expected_abilities = array(
- 'scf/list-taxonomies',
- 'scf/get-taxonomy',
- 'scf/create-taxonomy',
- 'scf/update-taxonomy',
- 'scf/delete-taxonomy',
- 'scf/duplicate-taxonomy',
- 'scf/export-taxonomy',
- 'scf/import-taxonomy',
- );
-
- foreach ( $expected_abilities as $ability_name ) {
- $this->assertArrayHasKey( $ability_name, $mock_registered_abilities, "Missing ability: $ability_name" );
- }
- }
-
- /**
- * Test registered abilities have correct category
- */
- public function test_registered_abilities_have_correct_category() {
- global $mock_registered_abilities;
- $mock_registered_abilities = array();
-
- $this->abilities->register_abilities();
-
- foreach ( $mock_registered_abilities as $name => $args ) {
- $this->assertEquals(
- 'scf-taxonomies',
- $args['category'],
- "Ability $name has wrong category"
- );
- }
- }
-
- /**
- * Test registered abilities have execute callbacks
- */
- public function test_registered_abilities_have_execute_callbacks() {
- global $mock_registered_abilities;
- $mock_registered_abilities = array();
-
- $this->abilities->register_abilities();
-
- foreach ( $mock_registered_abilities as $name => $args ) {
- $this->assertArrayHasKey( 'execute_callback', $args, "Ability $name missing execute_callback" );
- $this->assertIsCallable( $args['execute_callback'], "Ability $name execute_callback is not callable" );
- }
- }
-
- /**
- * Test registered abilities have input schemas
- */
- public function test_registered_abilities_have_input_schemas() {
- global $mock_registered_abilities;
- $mock_registered_abilities = array();
-
- $this->abilities->register_abilities();
-
- foreach ( $mock_registered_abilities as $name => $args ) {
- $this->assertArrayHasKey( 'input_schema', $args, "Ability $name missing input_schema" );
- }
- }
-
- /**
- * Test delete ability is marked as destructive
- */
- public function test_delete_ability_is_destructive() {
- 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'];
- $meta = $ability['meta'] ?? array();
- $annotations = $meta['annotations'] ?? array();
- $this->assertTrue( $annotations['destructive'] ?? false, 'Delete ability should be marked destructive' );
- }
-
- /**
- * Test export ability is marked as readonly
- */
- public function test_export_ability_is_readonly() {
- global $mock_registered_abilities;
- $mock_registered_abilities = array();
-
- $this->abilities->register_abilities();
-
- $this->assertArrayHasKey( 'scf/export-taxonomy', $mock_registered_abilities );
- $ability = $mock_registered_abilities['scf/export-taxonomy'];
- $meta = $ability['meta'] ?? array();
- $annotations = $meta['annotations'] ?? array();
- $this->assertTrue( $annotations['readonly'] ?? false, 'Export ability should be marked readonly' );
- }
-
- // Callback tests.
-
- /**
- * Test list_taxonomies_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() );
-
- $this->assertIsArray( $result );
- }
-
- /**
- * Test list_taxonomies_callback with filter parameter
- */
- public function test_list_taxonomies_callback_with_filter() {
- $result = $this->abilities->list_taxonomies_callback(
- array( 'filter' => array( 'active' => true ) )
- );
-
- $this->assertIsArray( $result );
- }
-
- /**
- * Test get_taxonomy_callback returns WP_Error for non-existent ID
- */
- public function test_get_taxonomy_callback_not_found_returns_error() {
- $result = $this->abilities->get_taxonomy_callback(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() );
- }
-
- /**
- * Test get_taxonomy_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(
- array( 'identifier' => 'nonexistent_taxonomy' )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $error_data = $result->get_error_data();
- $this->assertEquals( 404, $error_data['status'] );
- }
-
- /**
- * Test create_taxonomy_callback returns array with key
- */
- public function test_create_taxonomy_callback_returns_array_with_key() {
- $result = $this->abilities->create_taxonomy_callback( $this->test_taxonomy );
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'key', $result );
- $this->assertEquals( $this->test_taxonomy['key'], $result['key'] );
- }
-
- /**
- * Test create_taxonomy_callback returns array with title
- */
- public function test_create_taxonomy_callback_returns_array_with_title() {
- $result = $this->abilities->create_taxonomy_callback( $this->test_taxonomy );
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'title', $result );
- $this->assertEquals( $this->test_taxonomy['title'], $result['title'] );
- }
-
- /**
- * Test update_taxonomy_callback returns WP_Error for non-existent ID
- */
- public function test_update_taxonomy_callback_not_found_returns_error() {
- $result = $this->abilities->update_taxonomy_callback(
- array(
- 'ID' => 999999,
- 'title' => 'Should Fail',
- )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() );
- }
-
- /**
- * Test delete_taxonomy_callback returns WP_Error for non-existent ID
- */
- public function test_delete_taxonomy_callback_not_found_returns_error() {
- $result = $this->abilities->delete_taxonomy_callback(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() );
- }
-
- /**
- * Test delete_taxonomy_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(
- 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_taxonomy_callback returns WP_Error for non-existent ID
- */
- public function test_duplicate_taxonomy_callback_not_found_returns_error() {
- $result = $this->abilities->duplicate_taxonomy_callback(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() );
- }
-
- /**
- * Test export_taxonomy_callback returns WP_Error for non-existent ID
- */
- public function test_export_taxonomy_callback_not_found_returns_error() {
- $result = $this->abilities->export_taxonomy_callback(
- array( 'identifier' => 999999 )
- );
-
- $this->assertInstanceOf( WP_Error::class, $result );
- $this->assertEquals( 'taxonomy_not_found', $result->get_error_code() );
- }
-
- /**
- * Test import_taxonomy_callback returns array
- */
- public function test_import_taxonomy_callback_returns_array() {
- $result = $this->abilities->import_taxonomy_callback( $this->test_taxonomy );
-
- $this->assertIsArray( $result );
- }
-
- /**
- * Test import_taxonomy_callback returns correct taxonomy
- */
- public function test_import_taxonomy_callback_returns_correct_taxonomy() {
- $result = $this->abilities->import_taxonomy_callback( $this->test_taxonomy );
-
- $this->assertIsArray( $result );
- $this->assertEquals( $this->test_taxonomy['taxonomy'], $result['taxonomy'] );
- }
-
- /**
- * Test import_taxonomy_callback returns correct title
- */
- public function test_import_taxonomy_callback_returns_correct_title() {
- $result = $this->abilities->import_taxonomy_callback( $this->test_taxonomy );
-
- $this->assertIsArray( $result );
- $this->assertEquals( $this->test_taxonomy['title'], $result['title'] );
- }
-
- // Schema tests.
-
- /**
- * Test get_taxonomy_schema returns valid schema
- */
- public function test_get_taxonomy_schema_returns_array() {
- $reflection = new ReflectionClass( $this->abilities );
- $method = $reflection->getMethod( 'get_taxonomy_schema' );
- $method->setAccessible( true );
-
- $schema = $method->invoke( $this->abilities );
-
- $this->assertIsArray( $schema );
- $this->assertArrayHasKey( 'type', $schema );
- $this->assertEquals( 'object', $schema['type'] );
- }
-
- /**
- * Test get_taxonomy_schema has required fields
- */
- public function test_get_taxonomy_schema_has_required_fields() {
- $reflection = new ReflectionClass( $this->abilities );
- $method = $reflection->getMethod( 'get_taxonomy_schema' );
- $method->setAccessible( true );
-
- $schema = $method->invoke( $this->abilities );
-
- $this->assertArrayHasKey( 'required', $schema );
- $this->assertContains( 'key', $schema['required'] );
- $this->assertContains( 'title', $schema['required'] );
- $this->assertContains( 'taxonomy', $schema['required'] );
- }
-
- /**
- * Test get_scf_identifier_schema returns valid schema
- */
- public function test_get_scf_identifier_schema_returns_array() {
- $reflection = new ReflectionClass( $this->abilities );
- $method = $reflection->getMethod( 'get_scf_identifier_schema' );
- $method->setAccessible( true );
-
- $schema = $method->invoke( $this->abilities );
-
- $this->assertIsArray( $schema );
- // Should have description and allow integer or string.
- $this->assertArrayHasKey( 'description', $schema );
- }
-
- // Helper method tests.
-
- /**
- * Test taxonomy_not_found_error returns correct error code
- */
- public function test_taxonomy_not_found_error_returns_correct_code() {
- $reflection = new ReflectionClass( $this->abilities );
- $method = $reflection->getMethod( 'taxonomy_not_found_error' );
- $method->setAccessible( true );
-
- $error = $method->invoke( $this->abilities );
-
- $this->assertInstanceOf( WP_Error::class, $error );
- $this->assertEquals( 'taxonomy_not_found', $error->get_error_code() );
- }
-
- /**
- * Test taxonomy_not_found_error returns 404 status
- */
- public function test_taxonomy_not_found_error_returns_404() {
- $reflection = new ReflectionClass( $this->abilities );
- $method = $reflection->getMethod( 'taxonomy_not_found_error' );
- $method->setAccessible( true );
-
- $error = $method->invoke( $this->abilities );
- $error_data = $error->get_error_data();
-
- $this->assertEquals( 404, $error_data['status'] );
- }
-}