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