mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
Lands the ACF domain from #76 (by @DeibyRo), reworked to a consolidated 2-tool surface and a new admin home. Surface: instead of 15 individual MCP abilities, the domain registers TWO dispatcher tools — emcp-tools/acf-read and emcp-tools/acf-write — that expose 15 operations (8 read, 7 write). Each takes { operation, arguments }; calling with no operation returns a discovery catalog listing that side's operations. dispatch() gates each call by ACF-6.1+ availability (CPT/tax ops) then the operation's own capability check — no privilege escalation. Gating: read-vs-write is at the TOOL level — acf-read and acf-write are the two toggles (acf-read on, acf-write off by default), enabled/disabled by the standard filter_disabled_tools mechanism like any other tool. No per-operation toggles. Seeded by the v14 defaults step (DEFAULTS_VERSION 13 -> 14), which adds emcp-tools/acf-write to the disabled set and strips any pre-release per-operation ACF slugs. Admin: new "Plugins" sub-tab on the Tools screen; the ACF section shows the two dispatcher toggles, each listing the operations it covers. Everything else from #76 is preserved: CPT/taxonomy authoring as data via acf_import_* (no executable PHP), full Pro field support (repeater/flexible/gallery/group/clone), name->key write resolution, immutable slugs, no delete operations. Tests: tests/ harness reworked to the dispatcher model (49 tests — routing, catalog, permission delegation + the 15 executors). pro/tests ToolCatalogPlatformTest updated for the Plugins tab. Full suite 928 green; live-verified end-to-end against ACF 6.8.5, including over the real MCP stdio transport (acf-read exposed, acf-write hidden while off). Versioned 3.2.1. Co-Authored-By: DeibyRo <[email protected]> Co-Authored-By: Claude Opus 4.8 <[email protected]>
656 lines
29 KiB
PHP
656 lines
29 KiB
PHP
<?php
|
|
/**
|
|
* Unit tests for EMCP_Tools_ACF_Abilities.
|
|
*
|
|
* Runs against the standalone stub harness in tests/bootstrap.php:
|
|
*
|
|
* vendor/bin/phpunit -c tests/phpunit.xml
|
|
*
|
|
* @package EMCP_Tools
|
|
*/
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class AcfAbilitiesTest extends TestCase {
|
|
|
|
/** @var EMCP_Tools_ACF_Abilities */
|
|
private $abilities;
|
|
|
|
protected function setUp(): void {
|
|
emcp_test_reset();
|
|
$this->abilities = new EMCP_Tools_ACF_Abilities();
|
|
}
|
|
|
|
/**
|
|
* Fixture: one post (ID 10) with a field group containing a text field, a
|
|
* repeater (text + image sub-fields), and a flexible content field with a
|
|
* "hero" layout.
|
|
*/
|
|
private function seed_post_with_group(): void {
|
|
$GLOBALS['emcp_test']['posts'][10] = new WP_Post( array( 'ID' => 10, 'post_title' => 'Sample' ) );
|
|
|
|
$group = array( 'key' => 'group_demo', 'ID' => 55, 'title' => 'Demo Group', 'active' => true );
|
|
$GLOBALS['emcp_test']['field_groups'][] = $group;
|
|
$GLOBALS['emcp_test']['groups_for_post'][10] = array( $group );
|
|
$GLOBALS['emcp_test']['group_fields']['group_demo'] = array(
|
|
array( 'key' => 'field_headline', 'name' => 'headline', 'label' => 'Headline', 'type' => 'text' ),
|
|
array(
|
|
'key' => 'field_items',
|
|
'name' => 'items',
|
|
'label' => 'Items',
|
|
'type' => 'repeater',
|
|
'sub_fields' => array(
|
|
array( 'key' => 'field_item_text', 'name' => 'item_text', 'label' => 'Item Text', 'type' => 'text' ),
|
|
array( 'key' => 'field_item_img', 'name' => 'item_img', 'label' => 'Item Image', 'type' => 'image' ),
|
|
),
|
|
),
|
|
array(
|
|
'key' => 'field_sections',
|
|
'name' => 'sections',
|
|
'label' => 'Sections',
|
|
'type' => 'flexible_content',
|
|
'layouts' => array(
|
|
array( 'key' => 'layout_hero', 'name' => 'hero', 'label' => 'Hero', 'sub_fields' => array(
|
|
array( 'key' => 'field_hero_title', 'name' => 'hero_title', 'label' => 'Hero Title', 'type' => 'text' ),
|
|
) ),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Registration
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_registers_two_dispatcher_tools(): void {
|
|
$this->abilities->register();
|
|
|
|
$expected = array( 'emcp-tools/acf-read', 'emcp-tools/acf-write' );
|
|
$this->assertSame( $expected, $this->abilities->get_ability_names() );
|
|
foreach ( $expected as $name ) {
|
|
$this->assertArrayHasKey( $name, $GLOBALS['emcp_test']['abilities'] );
|
|
}
|
|
}
|
|
|
|
public function test_read_catalog_lists_all_read_operations(): void {
|
|
$out = $this->abilities->run_acf_read( array() );
|
|
$this->assertSame( 'read', $out['mode'] );
|
|
$names = array_column( $out['operations'], 'operation' );
|
|
$this->assertSame(
|
|
array( 'list-field-groups', 'get-field-group', 'list-options-pages', 'get-fields', 'list-post-types', 'get-post-type', 'list-taxonomies', 'get-taxonomy' ),
|
|
$names
|
|
);
|
|
}
|
|
|
|
public function test_write_catalog_lists_all_write_operations(): void {
|
|
$out = $this->abilities->run_acf_write( array() );
|
|
$this->assertSame( 'write', $out['mode'] );
|
|
$names = array_column( $out['operations'], 'operation' );
|
|
$this->assertSame(
|
|
array( 'update-fields', 'create-field-group', 'update-field-group', 'create-post-type', 'update-post-type', 'create-taxonomy', 'update-taxonomy' ),
|
|
$names
|
|
);
|
|
}
|
|
|
|
public function test_read_dispatch_routes_to_executor(): void {
|
|
$this->seed_post_with_group();
|
|
$out = $this->abilities->run_acf_read( array( 'operation' => 'get-fields', 'arguments' => array( 'post_id' => 10 ) ) );
|
|
$this->assertFalse( $out instanceof WP_Error, 'get-fields should route to its executor' );
|
|
$this->assertArrayHasKey( 'fields', $out );
|
|
}
|
|
|
|
public function test_unknown_operation_is_rejected(): void {
|
|
$out = $this->abilities->run_acf_read( array( 'operation' => 'delete-everything' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $out );
|
|
$this->assertSame( 'unknown_operation', $out->get_error_code() );
|
|
}
|
|
|
|
public function test_write_operation_via_read_dispatcher_is_rejected(): void {
|
|
$out = $this->abilities->run_acf_read( array( 'operation' => 'create-field-group' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $out );
|
|
$this->assertSame( 'unknown_operation', $out->get_error_code() );
|
|
}
|
|
|
|
public function test_structural_write_requires_manage_options(): void {
|
|
$GLOBALS['emcp_test']['caps'] = array( 'edit_posts' ); // no manage_options
|
|
$out = $this->abilities->run_acf_write( array( 'operation' => 'create-taxonomy', 'arguments' => array( 'taxonomy' => 'x', 'title' => 'X' ) ) );
|
|
$this->assertInstanceOf( WP_Error::class, $out );
|
|
$this->assertSame( 'forbidden', $out->get_error_code() );
|
|
}
|
|
|
|
public function test_cpt_tax_support_is_detected(): void {
|
|
// The harness declares the ACF 6.1+ functions, so the gate is on.
|
|
$this->assertTrue( EMCP_Tools_ACF_Abilities::cpt_tax_supported() );
|
|
}
|
|
|
|
public function test_dispatchers_are_annotated_read_and_write(): void {
|
|
$this->abilities->register();
|
|
$abilities = $GLOBALS['emcp_test']['abilities'];
|
|
|
|
$this->assertTrue( $abilities['emcp-tools/acf-read']['meta']['annotations']['readonly'] );
|
|
$this->assertFalse( $abilities['emcp-tools/acf-write']['meta']['annotations']['readonly'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Permissions
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_options_target_requires_manage_options(): void {
|
|
$GLOBALS['emcp_test']['caps'] = array( 'edit_posts' ); // no manage_options
|
|
$this->assertFalse( $this->abilities->check_fields_permission( array( 'options_page' => 'options' ) ) );
|
|
|
|
$GLOBALS['emcp_test']['caps'] = array( 'manage_options' );
|
|
$this->assertTrue( $this->abilities->check_fields_permission( array( 'options_page' => 'options' ) ) );
|
|
}
|
|
|
|
public function test_post_target_requires_edit_post_on_that_post(): void {
|
|
$GLOBALS['emcp_test']['caps'] = array( 'edit_posts' );
|
|
$GLOBALS['emcp_test']['post_caps'][10] = false;
|
|
$this->assertFalse( $this->abilities->check_fields_permission( array( 'post_id' => 10 ) ) );
|
|
|
|
$GLOBALS['emcp_test']['post_caps'][10] = true;
|
|
$this->assertTrue( $this->abilities->check_fields_permission( array( 'post_id' => 10 ) ) );
|
|
}
|
|
|
|
public function test_field_group_authoring_requires_manage_options(): void {
|
|
$GLOBALS['emcp_test']['caps'] = array( 'edit_posts' );
|
|
$this->assertFalse( $this->abilities->check_manage_permission() );
|
|
|
|
$GLOBALS['emcp_test']['caps'] = array( 'manage_options' );
|
|
$this->assertTrue( $this->abilities->check_manage_permission() );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Target validation
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_update_fields_rejects_both_targets(): void {
|
|
$this->seed_post_with_group();
|
|
$result = $this->abilities->execute_update_fields( array( 'post_id' => 10, 'options_page' => 'options', 'fields' => array( 'headline' => 'x' ) ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'invalid_target', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_update_fields_rejects_missing_target(): void {
|
|
$result = $this->abilities->execute_update_fields( array( 'fields' => array( 'headline' => 'x' ) ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'invalid_target', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_options_target_requires_pro(): void {
|
|
$GLOBALS['emcp_test']['acf_pro'] = false;
|
|
$result = $this->abilities->execute_update_fields( array( 'options_page' => 'options', 'fields' => array( 'x' => 'y' ) ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'acf_pro_required', $result->get_error_code() );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// update-acf-fields
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_update_writes_by_field_key_not_name(): void {
|
|
$this->seed_post_with_group();
|
|
$result = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'headline' => 'Hello World' ),
|
|
) );
|
|
|
|
$this->assertIsArray( $result );
|
|
$this->assertSame( array( 'headline' ), $result['updated'] );
|
|
$this->assertCount( 1, $GLOBALS['emcp_test']['update_field_calls'] );
|
|
$this->assertSame( array( 'field_headline', 'Hello World', 10 ), $GLOBALS['emcp_test']['update_field_calls'][0] );
|
|
}
|
|
|
|
public function test_update_accepts_field_keys_directly(): void {
|
|
$this->seed_post_with_group();
|
|
$GLOBALS['emcp_test']['fields_by_key']['field_headline'] = $GLOBALS['emcp_test']['group_fields']['group_demo'][0];
|
|
|
|
$result = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'field_headline' => 'Via key' ),
|
|
) );
|
|
|
|
$this->assertSame( array( 'headline' ), $result['updated'] );
|
|
$this->assertSame( 'field_headline', $GLOBALS['emcp_test']['update_field_calls'][0][0] );
|
|
}
|
|
|
|
public function test_update_skips_unknown_fields_with_reason(): void {
|
|
$this->seed_post_with_group();
|
|
$result = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'nope' => 'x' ),
|
|
) );
|
|
|
|
$this->assertSame( array(), $result['updated'] );
|
|
$this->assertSame( array( array( 'field' => 'nope', 'reason' => 'field_not_found' ) ), $result['skipped'] );
|
|
$this->assertCount( 0, $GLOBALS['emcp_test']['update_field_calls'] );
|
|
}
|
|
|
|
public function test_update_repeater_rows_pass_through_to_acf(): void {
|
|
$this->seed_post_with_group();
|
|
$rows = array(
|
|
array( 'item_text' => 'Row 1', 'item_img' => 42 ),
|
|
array( 'item_text' => 'Row 2', 'item_img' => 43 ),
|
|
);
|
|
$result = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'items' => $rows ),
|
|
) );
|
|
|
|
$this->assertSame( array( 'items' ), $result['updated'] );
|
|
$this->assertSame( array( 'field_items', $rows, 10 ), $GLOBALS['emcp_test']['update_field_calls'][0] );
|
|
}
|
|
|
|
public function test_update_pro_field_rejected_on_free_acf(): void {
|
|
$this->seed_post_with_group();
|
|
$GLOBALS['emcp_test']['acf_pro'] = false;
|
|
|
|
$result = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'items' => array() ),
|
|
) );
|
|
|
|
$this->assertSame( array( array( 'field' => 'items', 'reason' => 'acf_pro_required' ) ), $result['skipped'] );
|
|
$this->assertCount( 0, $GLOBALS['emcp_test']['update_field_calls'] );
|
|
}
|
|
|
|
public function test_flexible_rows_require_known_layout(): void {
|
|
$this->seed_post_with_group();
|
|
|
|
$missing = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'sections' => array( array( 'hero_title' => 'No layout key' ) ) ),
|
|
) );
|
|
$this->assertSame( 'missing_acf_fc_layout', $missing['skipped'][0]['reason'] );
|
|
|
|
$unknown = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'sections' => array( array( 'acf_fc_layout' => 'bogus' ) ) ),
|
|
) );
|
|
$this->assertSame( 'unknown_layout', $unknown['skipped'][0]['reason'] );
|
|
|
|
$valid = $this->abilities->execute_update_fields( array(
|
|
'post_id' => 10,
|
|
'fields' => array( 'sections' => array( array( 'acf_fc_layout' => 'hero', 'hero_title' => 'Hi' ) ) ),
|
|
) );
|
|
$this->assertSame( array( 'sections' ), $valid['updated'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// get-acf-fields
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_get_fields_returns_null_for_unsaved_fields(): void {
|
|
$this->seed_post_with_group();
|
|
$GLOBALS['emcp_test']['values']['10'] = array( 'field_headline' => 'Stored' );
|
|
|
|
$result = $this->abilities->execute_get_fields( array( 'post_id' => 10 ) );
|
|
|
|
$this->assertSame( 'Stored', $result['fields']['headline'] );
|
|
$this->assertNull( $result['fields']['items'] );
|
|
$this->assertNull( $result['fields']['sections'] );
|
|
}
|
|
|
|
public function test_get_fields_normalizes_wp_post_and_image_arrays(): void {
|
|
$this->seed_post_with_group();
|
|
$GLOBALS['emcp_test']['values']['10'] = array(
|
|
'field_headline' => new WP_Post( array( 'ID' => 7, 'post_title' => 'Linked', 'post_type' => 'page' ) ),
|
|
'field_items' => array( array( 'item_img' => array( 'ID' => 42, 'url' => 'http://example.test/i.jpg', 'mime_type' => 'image/jpeg', 'alt' => 'Alt' ) ) ),
|
|
);
|
|
|
|
$result = $this->abilities->execute_get_fields( array( 'post_id' => 10 ) );
|
|
|
|
$this->assertSame( array( 'id' => 7, 'title' => 'Linked', 'post_type' => 'page', 'url' => 'http://example.test/?p=7' ), $result['fields']['headline'] );
|
|
$this->assertSame( array( 'id' => 42, 'url' => 'http://example.test/i.jpg', 'alt' => 'Alt', 'mime' => 'image/jpeg' ), $result['fields']['items'][0]['item_img'] );
|
|
}
|
|
|
|
public function test_get_fields_filter_limits_output(): void {
|
|
$this->seed_post_with_group();
|
|
$result = $this->abilities->execute_get_fields( array( 'post_id' => 10, 'fields' => array( 'headline' ) ) );
|
|
$this->assertSame( array( 'headline' ), array_keys( $result['fields'] ) );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Field group discovery
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_get_field_group_formats_recursive_tree(): void {
|
|
$this->seed_post_with_group();
|
|
$result = $this->abilities->execute_get_field_group( array( 'key' => 'group_demo' ) );
|
|
|
|
$this->assertSame( 'group_demo', $result['key'] );
|
|
$this->assertCount( 3, $result['fields'] );
|
|
$this->assertSame( 'item_text', $result['fields'][1]['sub_fields'][0]['name'] );
|
|
$this->assertSame( 'hero', $result['fields'][2]['layouts'][0]['name'] );
|
|
$this->assertSame( 'hero_title', $result['fields'][2]['layouts'][0]['sub_fields'][0]['name'] );
|
|
}
|
|
|
|
public function test_list_field_groups_filters_inactive_by_default(): void {
|
|
$GLOBALS['emcp_test']['field_groups'] = array(
|
|
array( 'key' => 'group_a', 'ID' => 1, 'title' => 'Active', 'active' => true ),
|
|
array( 'key' => 'group_b', 'ID' => 2, 'title' => 'Inactive', 'active' => false ),
|
|
);
|
|
|
|
$result = $this->abilities->execute_list_field_groups( array() );
|
|
$this->assertSame( 1, $result['total'] );
|
|
$this->assertSame( 'group_a', $result['groups'][0]['key'] );
|
|
|
|
$all = $this->abilities->execute_list_field_groups( array( 'active_only' => false ) );
|
|
$this->assertSame( 2, $all['total'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// create-acf-field-group
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_create_group_assigns_keys_and_imports(): void {
|
|
$result = $this->abilities->execute_create_field_group( array(
|
|
'title' => 'New Group',
|
|
'fields' => array(
|
|
array( 'label' => 'Headline', 'name' => 'headline', 'type' => 'text' ),
|
|
array(
|
|
'label' => 'Items',
|
|
'name' => 'items',
|
|
'type' => 'repeater',
|
|
'sub_fields' => array(
|
|
array( 'label' => 'Text', 'name' => 'item_text', 'type' => 'text' ),
|
|
),
|
|
),
|
|
),
|
|
) );
|
|
|
|
$this->assertIsArray( $result );
|
|
$this->assertCount( 1, $GLOBALS['emcp_test']['imported_groups'] );
|
|
$imported = $GLOBALS['emcp_test']['imported_groups'][0];
|
|
|
|
$this->assertStringStartsWith( 'group_', $imported['key'] );
|
|
$this->assertStringStartsWith( 'field_', $imported['fields'][0]['key'] );
|
|
$this->assertStringStartsWith( 'field_', $imported['fields'][1]['sub_fields'][0]['key'] );
|
|
// Default location applied.
|
|
$this->assertSame( 'post_type', $imported['location'][0][0]['param'] );
|
|
$this->assertSame( (int) $imported['ID'], $result['id'] );
|
|
}
|
|
|
|
public function test_create_group_rejects_pro_types_on_free_acf(): void {
|
|
$GLOBALS['emcp_test']['acf_pro'] = false;
|
|
$result = $this->abilities->execute_create_field_group( array(
|
|
'title' => 'New Group',
|
|
'fields' => array( array( 'label' => 'Items', 'name' => 'items', 'type' => 'repeater' ) ),
|
|
) );
|
|
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'acf_pro_required', $result->get_error_code() );
|
|
$this->assertCount( 0, $GLOBALS['emcp_test']['imported_groups'] );
|
|
}
|
|
|
|
public function test_create_group_rejects_unknown_field_type(): void {
|
|
$result = $this->abilities->execute_create_field_group( array(
|
|
'title' => 'New Group',
|
|
'fields' => array( array( 'label' => 'X', 'name' => 'x', 'type' => 'unknown_type' ) ),
|
|
) );
|
|
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'invalid_field', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_create_group_validates_location_rules(): void {
|
|
$result = $this->abilities->execute_create_field_group( array(
|
|
'title' => 'New Group',
|
|
'fields' => array( array( 'label' => 'X', 'name' => 'x', 'type' => 'text' ) ),
|
|
'location' => array( array( array( 'param' => 'post_type', 'operator' => '>=', 'value' => 'post' ) ) ),
|
|
) );
|
|
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'invalid_location', $result->get_error_code() );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// update-acf-field-group
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_update_group_refuses_local_groups(): void {
|
|
$GLOBALS['emcp_test']['field_groups'][] = array( 'key' => 'group_json', 'ID' => 9, 'title' => 'From JSON', 'local' => 'json' );
|
|
|
|
$result = $this->abilities->execute_update_field_group( array( 'key' => 'group_json', 'title' => 'Renamed' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'acf_local_group', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_update_group_refuses_field_renames_and_type_changes(): void {
|
|
$GLOBALS['emcp_test']['field_groups'][] = array( 'key' => 'group_db', 'ID' => 12, 'title' => 'DB Group' );
|
|
|
|
foreach ( array( 'name', 'type' ) as $immutable ) {
|
|
$result = $this->abilities->execute_update_field_group( array(
|
|
'key' => 'group_db',
|
|
'update_fields' => array( array( 'key' => 'field_x', $immutable => 'changed' ) ),
|
|
) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'immutable_field_setting', $result->get_error_code() );
|
|
}
|
|
}
|
|
|
|
public function test_update_group_appends_fields_and_updates_settings(): void {
|
|
$GLOBALS['emcp_test']['field_groups'][] = array( 'key' => 'group_db', 'ID' => 12, 'title' => 'DB Group' );
|
|
$GLOBALS['emcp_test']['fields_by_key']['field_headline'] = array( 'key' => 'field_headline', 'name' => 'headline', 'label' => 'Old', 'type' => 'text' );
|
|
|
|
$result = $this->abilities->execute_update_field_group( array(
|
|
'key' => 'group_db',
|
|
'title' => 'Renamed Group',
|
|
'add_fields' => array( array( 'label' => 'Extra', 'name' => 'extra', 'type' => 'text' ) ),
|
|
'update_fields' => array( array( 'key' => 'field_headline', 'label' => 'New Label', 'required' => true ) ),
|
|
) );
|
|
|
|
$this->assertSame( array( 'title' ), $result['updated'] );
|
|
$this->assertSame( 'extra', $result['fields_added'][0]['name'] );
|
|
$this->assertSame( array( 'field_headline' ), $result['fields_updated'] );
|
|
|
|
// The appended field was parented to the group and the setting change kept name/type intact.
|
|
$added = $GLOBALS['emcp_test']['updated_fields'][0];
|
|
$this->assertSame( 12, $added['parent'] );
|
|
$edited = $GLOBALS['emcp_test']['updated_fields'][1];
|
|
$this->assertSame( 'New Label', $edited['label'] );
|
|
$this->assertSame( 'headline', $edited['name'] );
|
|
$this->assertSame( 'text', $edited['type'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Options pages
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_list_options_pages_returns_pages(): void {
|
|
$GLOBALS['emcp_test']['options_pages'] = array(
|
|
array( 'menu_slug' => 'site-settings', 'page_title' => 'Site Settings', 'post_id' => 'options', 'parent_slug' => '' ),
|
|
);
|
|
|
|
$result = $this->abilities->execute_list_options_pages( array() );
|
|
$this->assertTrue( $result['pro'] );
|
|
$this->assertSame( 'site-settings', $result['pages'][0]['menu_slug'] );
|
|
}
|
|
|
|
public function test_options_first_write_by_name_resolves_via_located_group(): void {
|
|
// A group located on an options page whose field has NO stored value yet:
|
|
// the name must still resolve to the key through the group index.
|
|
$GLOBALS['emcp_test']['field_groups'][] = array(
|
|
'key' => 'group_opts',
|
|
'ID' => 30,
|
|
'title' => 'Options Group',
|
|
'active' => true,
|
|
'location' => array( array( array( 'param' => 'options_page', 'operator' => '==', 'value' => 'site-settings' ) ) ),
|
|
);
|
|
$GLOBALS['emcp_test']['group_fields']['group_opts'] = array(
|
|
array( 'key' => 'field_footer', 'name' => 'footer_text', 'label' => 'Footer', 'type' => 'text' ),
|
|
);
|
|
|
|
$result = $this->abilities->execute_update_fields( array(
|
|
'options_page' => 'options',
|
|
'fields' => array( 'footer_text' => 'First write' ),
|
|
) );
|
|
|
|
$this->assertSame( array( 'footer_text' ), $result['updated'] );
|
|
$this->assertSame( array( 'field_footer', 'First write', 'options' ), $GLOBALS['emcp_test']['update_field_calls'][0] );
|
|
}
|
|
|
|
public function test_options_target_reads_via_field_objects(): void {
|
|
$GLOBALS['emcp_test']['field_objects']['options'] = array(
|
|
'footer_text' => array( 'key' => 'field_footer', 'name' => 'footer_text', 'label' => 'Footer', 'type' => 'text' ),
|
|
);
|
|
$GLOBALS['emcp_test']['values']['options'] = array( 'field_footer' => '© 2026' );
|
|
|
|
$result = $this->abilities->execute_get_fields( array( 'options_page' => 'options' ) );
|
|
$this->assertSame( '© 2026', $result['fields']['footer_text'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Custom Post Types (ACF 6.1+)
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_create_post_type_generates_key_and_imports(): void {
|
|
$result = $this->abilities->execute_create_post_type( array(
|
|
'post_type' => 'book',
|
|
'title' => 'Books',
|
|
'singular' => 'Book',
|
|
'supports' => array( 'title', 'editor', 'thumbnail' ),
|
|
) );
|
|
|
|
$this->assertIsArray( $result );
|
|
$this->assertCount( 1, $GLOBALS['emcp_test']['imported_types'] );
|
|
$imported = $GLOBALS['emcp_test']['imported_types'][0];
|
|
$this->assertStringStartsWith( 'post_type_', $imported['key'] );
|
|
$this->assertSame( 'book', $imported['post_type'] );
|
|
$this->assertSame( 'Books', $imported['labels']['name'] );
|
|
$this->assertSame( 'Book', $imported['labels']['singular_name'] );
|
|
$this->assertSame( 'book', $result['post_type'] );
|
|
}
|
|
|
|
public function test_create_post_type_rejects_existing_slug(): void {
|
|
$GLOBALS['emcp_test']['existing_types'][] = 'book';
|
|
$result = $this->abilities->execute_create_post_type( array( 'post_type' => 'book', 'title' => 'Books' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'post_type_exists', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_create_post_type_rejects_reserved_slug(): void {
|
|
$result = $this->abilities->execute_create_post_type( array( 'post_type' => 'page', 'title' => 'Pages' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'reserved_slug', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_create_post_type_rejects_overlong_slug(): void {
|
|
$result = $this->abilities->execute_create_post_type( array( 'post_type' => str_repeat( 'a', 21 ), 'title' => 'X' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'invalid_slug', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_update_post_type_rejects_slug_change(): void {
|
|
$GLOBALS['emcp_test']['acf_post_types']['post_type_x'] = array(
|
|
'key' => 'post_type_x', 'ID' => 210, 'post_type' => 'book', 'title' => 'Books', 'active' => true,
|
|
);
|
|
$result = $this->abilities->execute_update_post_type( array( 'key' => 'post_type_x', 'post_type' => 'novel' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'immutable_slug', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_update_post_type_changes_title_and_taxonomies(): void {
|
|
$GLOBALS['emcp_test']['acf_post_types']['post_type_x'] = array(
|
|
'key' => 'post_type_x', 'ID' => 210, 'post_type' => 'book', 'title' => 'Books',
|
|
'active' => true, 'labels' => array( 'name' => 'Books', 'singular_name' => 'Book' ),
|
|
);
|
|
$result = $this->abilities->execute_update_post_type( array(
|
|
'key' => 'post_type_x',
|
|
'title' => 'Publications',
|
|
'taxonomies' => array( 'genre', 'genre', 'author' ),
|
|
) );
|
|
|
|
$this->assertIsArray( $result );
|
|
$saved = $GLOBALS['emcp_test']['updated_internal'][0]['item'];
|
|
$this->assertSame( 'acf-post-type', $GLOBALS['emcp_test']['updated_internal'][0]['post_type'] );
|
|
$this->assertSame( 'Publications', $saved['title'] );
|
|
$this->assertSame( 'Publications', $saved['labels']['name'] );
|
|
$this->assertSame( array( 'genre', 'author' ), $saved['taxonomies'] ); // deduped, sanitized
|
|
}
|
|
|
|
public function test_get_post_type_missing_returns_error(): void {
|
|
$result = $this->abilities->execute_get_post_type( array( 'key' => 'post_type_nope' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'not_found', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_list_post_types_filters_inactive_and_search(): void {
|
|
$GLOBALS['emcp_test']['acf_post_types'] = array(
|
|
array( 'key' => 'post_type_a', 'ID' => 1, 'post_type' => 'book', 'title' => 'Books', 'active' => true ),
|
|
array( 'key' => 'post_type_b', 'ID' => 2, 'post_type' => 'movie', 'title' => 'Movies', 'active' => false ),
|
|
);
|
|
$active = $this->abilities->execute_list_post_types( array() );
|
|
$this->assertSame( 1, $active['total'] );
|
|
$this->assertSame( 'book', $active['post_types'][0]['post_type'] );
|
|
|
|
$all = $this->abilities->execute_list_post_types( array( 'active_only' => false ) );
|
|
$this->assertSame( 2, $all['total'] );
|
|
|
|
$search = $this->abilities->execute_list_post_types( array( 'active_only' => false, 'search' => 'movie' ) );
|
|
$this->assertSame( 1, $search['total'] );
|
|
$this->assertSame( 'movie', $search['post_types'][0]['post_type'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Taxonomies (ACF 6.1+)
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_create_taxonomy_requires_object_type(): void {
|
|
$result = $this->abilities->execute_create_taxonomy( array( 'taxonomy' => 'genre', 'title' => 'Genres', 'object_type' => array() ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'missing_params', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_create_taxonomy_generates_key_and_imports(): void {
|
|
$result = $this->abilities->execute_create_taxonomy( array(
|
|
'taxonomy' => 'genre',
|
|
'title' => 'Genres',
|
|
'object_type' => array( 'book' ),
|
|
) );
|
|
|
|
$this->assertIsArray( $result );
|
|
$this->assertCount( 1, $GLOBALS['emcp_test']['imported_taxes'] );
|
|
$imported = $GLOBALS['emcp_test']['imported_taxes'][0];
|
|
$this->assertStringStartsWith( 'taxonomy_', $imported['key'] );
|
|
$this->assertSame( 'genre', $imported['taxonomy'] );
|
|
$this->assertSame( array( 'book' ), $imported['object_type'] );
|
|
$this->assertTrue( $imported['hierarchical'] ); // default true for taxonomies
|
|
}
|
|
|
|
public function test_create_taxonomy_rejects_existing_slug(): void {
|
|
$GLOBALS['emcp_test']['existing_taxes'][] = 'genre';
|
|
$result = $this->abilities->execute_create_taxonomy( array( 'taxonomy' => 'genre', 'title' => 'Genres', 'object_type' => array( 'book' ) ) );
|
|
$this->assertInstanceOf( WP_Error::class, $result );
|
|
$this->assertSame( 'taxonomy_exists', $result->get_error_code() );
|
|
}
|
|
|
|
public function test_update_taxonomy_rejects_slug_change_and_updates_object_type(): void {
|
|
$GLOBALS['emcp_test']['acf_taxonomies']['taxonomy_x'] = array(
|
|
'key' => 'taxonomy_x', 'ID' => 310, 'taxonomy' => 'genre', 'title' => 'Genres',
|
|
'active' => true, 'object_type' => array( 'book' ), 'labels' => array( 'name' => 'Genres' ),
|
|
);
|
|
$rename = $this->abilities->execute_update_taxonomy( array( 'key' => 'taxonomy_x', 'taxonomy' => 'category2' ) );
|
|
$this->assertInstanceOf( WP_Error::class, $rename );
|
|
$this->assertSame( 'immutable_slug', $rename->get_error_code() );
|
|
|
|
$ok = $this->abilities->execute_update_taxonomy( array( 'key' => 'taxonomy_x', 'object_type' => array( 'book', 'movie' ) ) );
|
|
$this->assertIsArray( $ok );
|
|
$saved = $GLOBALS['emcp_test']['updated_internal'][0]['item'];
|
|
$this->assertSame( 'acf-taxonomy', $GLOBALS['emcp_test']['updated_internal'][0]['post_type'] );
|
|
$this->assertSame( array( 'book', 'movie' ), $saved['object_type'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// CPT/tax permissions
|
|
// -------------------------------------------------------------------
|
|
|
|
public function test_cpt_tax_tools_require_manage_options(): void {
|
|
$GLOBALS['emcp_test']['caps'] = array( 'edit_posts' ); // no manage_options
|
|
$this->assertFalse( $this->abilities->check_manage_permission() );
|
|
|
|
$GLOBALS['emcp_test']['caps'] = array( 'manage_options' );
|
|
$this->assertTrue( $this->abilities->check_manage_permission() );
|
|
}
|
|
}
|