diff --git a/.gitignore b/.gitignore index e2788a07..60836c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ assets/build/**/*.php /blob-report/ /playwright/.cache/ /artifacts/ + +# Claude settings +**/.claude/settings.local.json \ No newline at end of file diff --git a/includes/rest-api.php b/includes/rest-api.php index ba80099b..7d711055 100644 --- a/includes/rest-api.php +++ b/includes/rest-api.php @@ -3,7 +3,7 @@ * REST API * * @package Secure Custom Fields - * @since ACF 6.4.0 + * @since SCF 6.5.0 */ // Exit if accessed directly. diff --git a/includes/rest-api/class-acf-rest-types-endpoint.php b/includes/rest-api/class-acf-rest-types-endpoint.php index d1867cbd..d06cc03e 100644 --- a/includes/rest-api/class-acf-rest-types-endpoint.php +++ b/includes/rest-api/class-acf-rest-types-endpoint.php @@ -14,12 +14,19 @@ /** * Class SCF_Rest_Types_Endpoint * - * Extends the /wp/v2/types endpoint to include SCF fields. + * Extends the /wp/v2/types endpoint to include SCF fields and source filtering. * * @since SCF 6.5.0 */ class SCF_Rest_Types_Endpoint { + /** + * Cached post types for the current request + * + * @var array|null + */ + private $cached_post_types = null; + /** * Initialize the class. * @@ -27,6 +34,154 @@ class SCF_Rest_Types_Endpoint { */ public function __construct() { add_action( 'rest_api_init', array( $this, 'register_extra_fields' ) ); + add_action( 'rest_api_init', array( $this, 'register_parameters' ) ); + + // Add filter to process REST API requests by route + add_filter( 'rest_request_before_callbacks', array( $this, 'filter_types_request' ), 10, 3 ); + + // Add filter to process each post type individually + add_filter( 'rest_prepare_post_type', array( $this, 'filter_post_type' ), 10, 3 ); + + // Clean up null entries from the response + add_filter( 'rest_pre_echo_response', array( $this, 'clean_types_response' ), 10, 3 ); + } + + /** + * Filter post types requests (both collection and individual) + * + * @since SCF 6.5.0 + * + * @param mixed $response The current response, either response or null. + * @param array $handler The handler for the route. + * @param WP_REST_Request $request The request object. + * @return mixed The response or null. + */ + public function filter_types_request( $response, $handler, $request ) { + // Check if this is a types endpoint request + $route = $request->get_route(); + $is_collection = '/wp/v2/types' === $route; + $is_single_type = preg_match( '#^/wp/v2/types/([^/]+)$#', $route, $matches ); + + if ( ! $is_collection && ! $is_single_type ) { + return $response; + } + + // Get the source parameter + $source = $request->get_param( 'source' ); + + // Only proceed if source parameter is provided and valid + if ( ! $source || ! in_array( $source, array( 'core', 'scf', 'other' ), true ) ) { + return $response; + } + + // Get post types, calculating once and reusing for the entire request + if ( null === $this->cached_post_types ) { + $this->cached_post_types = $this->get_source_post_types( $source ); + } + $source_post_types = $this->cached_post_types; + + // For single post type requests, check if it matches the source + if ( $is_single_type && isset( $matches[1] ) ) { + $requested_type = $matches[1]; + + // If the requested type doesn't match the source, return 404 + if ( ! in_array( $requested_type, $source_post_types, true ) ) { + return new WP_Error( + 'rest_post_type_invalid', + __( 'Invalid post type.', 'secure-custom-fields' ), + array( 'status' => 404 ) + ); + } + } + // For collection requests, we don't need to add any filter here + // as clean_types_response will handle removing null values from the response + // and filter_post_type will handle individual filtering + + return $response; + } + + /** + * Filter individual post type in the response. + * + * @since SCF 6.5.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Post_Type $post_type The post type object. + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|null The filtered response or null to filter it out. + */ + public function filter_post_type( $response, $post_type, $request ) { + $source = $request->get_param( 'source' ); + + // Only apply filtering if source parameter is provided and valid + if ( ! $source || ! in_array( $source, array( 'core', 'scf', 'other' ), true ) ) { + return $response; + } + + if ( null === $this->cached_post_types ) { + $this->cached_post_types = $this->get_source_post_types( $source ); + } + $source_post_types = $this->cached_post_types; + + if ( ! in_array( $post_type->name, $source_post_types, true ) ) { + return null; + } + + return $response; + } + + /** + * Get an array of post types for each source. + * + * @since SCF 6.5.0 + * + * @param string $source The source to get post types for. + * @return array An array of post type names for the specified source. + */ + private function get_source_post_types( $source ) { + + $core_types = array(); + $scf_types = array(); + + if ( 'core' === $source || 'other' === $source ) { + $all_post_types = get_post_types( array( '_builtin' => true ), 'objects' ); + foreach ( $all_post_types as $post_type ) { + $core_types[] = $post_type->name; + } + } + + if ( 'scf' === $source || 'other' === $source ) { + $scf_types = array(); + + // Get SCF-managed post types + if ( function_exists( 'acf_get_internal_post_type_posts' ) ) { + $scf_managed_post_types = acf_get_internal_post_type_posts( 'acf-post-type' ); + foreach ( $scf_managed_post_types as $scf_post_type ) { + if ( isset( $scf_post_type['post_type'] ) ) { + $scf_types[] = $scf_post_type['post_type']; + } + } + } + } + + switch ( $source ) { + case 'core': + $result = $core_types; + break; + case 'scf': + $result = $scf_types; + break; + case 'other': + $result = array_diff( + array_keys( get_post_types( array(), 'objects' ) ), + array_merge( $core_types, $scf_types ) + ); + break; + default: + $result = array(); + } + + return $result; } /** @@ -40,6 +195,7 @@ public function register_extra_fields() { if ( ! (bool) get_option( 'scf_beta_feature_editor_sidebar_enabled', false ) ) { return; } + register_rest_field( 'type', 'scf_field_groups', @@ -123,4 +279,116 @@ private function get_field_schema() { 'context' => array( 'view', 'edit', 'embed' ), ); } + + /** + * Register the source parameter for the post types endpoint. + * + * @since SCF 6.5.0 + */ + public function register_parameters() { + if ( ! acf_get_setting( 'rest_api_enabled' ) ) { + return; + } + + // Register the query parameter with the REST API + add_filter( 'rest_type_collection_params', array( $this, 'add_collection_params' ) ); + add_filter( 'rest_types_collection_params', array( $this, 'add_collection_params' ) ); + + // Direct registration for OpenAPI documentation + add_filter( 'rest_endpoints', array( $this, 'add_parameter_to_endpoints' ) ); + } + + /** + * Get the source parameter definition + * + * @since SCF 6.5.0 + * + * @param bool $include_validation Whether to include validation callbacks. + * @return array Parameter definition + */ + private function get_source_param_definition( $include_validation = false ) { + $param = array( + 'description' => __( 'Filter post types by their source.', 'secure-custom-fields' ), + 'type' => 'string', + 'enum' => array( 'core', 'scf', 'other' ), + 'required' => false, + ); + + // Not needed for OpenAPI documentation + if ( $include_validation ) { + $param['validate_callback'] = 'rest_validate_request_arg'; + $param['sanitize_callback'] = 'sanitize_text_field'; + $param['default'] = null; + $param['in'] = 'query'; + } + + return $param; + } + + /** + * Add source parameter directly to the endpoints for proper documentation + * + * @since SCF 6.5.0 + * + * @param array $endpoints The REST API endpoints. + * @return array Modified endpoints + */ + public function add_parameter_to_endpoints( $endpoints ) { + $source_param = $this->get_source_param_definition(); + $endpoints_to_modify = array( '/wp/v2/types', '/wp/v2/types/(?P[\w-]+)' ); + + foreach ( $endpoints_to_modify as $route ) { + if ( isset( $endpoints[ $route ] ) ) { + foreach ( $endpoints[ $route ] as &$endpoint ) { + if ( isset( $endpoint['args'] ) ) { + $endpoint['args']['source'] = $source_param; + } + } + } + } + + return $endpoints; + } + + /** + * Add source parameter to the collection parameters for the types endpoint. + * + * @since SCF 6.5.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + * @return array Modified collection parameters. + */ + public function add_collection_params( $query_params ) { + $query_params['source'] = $this->get_source_param_definition( true ); + return $query_params; + } + + /** + * Clean up null entries from the response + * + * @since SCF 6.5.0 + * + * @param array $response The response data. + * @param WP_REST_Server $server The REST server instance. + * @param WP_REST_Request $request The original request. + * @return array The filtered response data. + */ + public function clean_types_response( $response, $server, $request ) { + if ( strpos( $request->get_route(), '/wp/v2/types' ) !== 0 ) { + return $response; + } + + // Only process collection responses (not single post type responses) + // Single post type responses have a 'slug' property, collections don't + if ( is_array( $response ) && ! isset( $response['slug'] ) ) { + $response = array_filter( + $response, + function ( $entry ) { + return null !== $entry; + } + ); + } + + return $response; + } } diff --git a/tests/e2e/plugins/scf-test-setup-post-types.php b/tests/e2e/plugins/scf-test-setup-post-types.php new file mode 100644 index 00000000..6440784d --- /dev/null +++ b/tests/e2e/plugins/scf-test-setup-post-types.php @@ -0,0 +1,117 @@ + array( + 'name' => 'Other E2E Test Type', + 'singular_name' => 'Other E2E Test Item', + ), + 'public' => true, + 'hierarchical' => false, + 'show_in_rest' => true, + 'has_archive' => true, + 'supports' => array( 'title', 'editor' ), + ) + ); +} + +/** + * Create an SCF post type entry in the database + * + * This function creates a post of type 'acf-post-type' in the database, which is how SCF + * stores its post type definitions. When the REST API endpoint calls + * acf_get_internal_post_type_posts('acf-post-type'), it will return our custom post type, + * causing it to be categorized as an SCF post type. + * + * NOTE: This is a hacky approach that uses SCF's internal APIs and should not be used + * in production. Ideally, SCF would provide a public API for registering post types + * programmatically. + */ +function scf_test_create_scf_post_type_entry() { + // Check if we've already created this post type to avoid duplicates + if ( get_option( 'scf_test_post_type_created' ) ) { + return; + } + + // Make sure SCF is fully loaded + if ( ! function_exists( 'acf_get_internal_post_type_instance' ) ) { + return; + } + + // Get the internal post type instance for managing acf-post-type entries + $instance = acf_get_internal_post_type_instance( 'acf-post-type' ); + if ( ! $instance ) { + return; + } + + // Define our post type configuration (similar to what you'd fill in the UI) + // This structure mirrors what SCF creates when you use the UI to create a post type + $post_type_config = array( + 'key' => 'scf_e2e_test_post_type', + 'title' => 'SCF E2E Test Type', + 'post_type' => 'scf-e2e-test-type', + 'description' => 'Test post type for SCF E2E testing', + 'active' => 1, + 'public' => 1, + 'show_in_rest' => 1, + 'publicly_queryable' => 1, + 'show_ui' => 1, + 'show_in_menu' => 1, + 'has_archive' => 1, + 'supports' => array( 'title', 'editor' ), + 'labels' => array( + 'name' => 'SCF E2E Test Type', + 'singular_name' => 'SCF E2E Test Item', + ), + ); + + // Create the post type entry in the database using SCF's internal API + $result = $instance->update_post( $post_type_config ); + + if ( is_array( $result ) && isset( $result['ID'] ) ) { + // Mark as created so we don't duplicate it + update_option( 'scf_test_post_type_created', true ); + } +} + +// Register hooks +add_action( 'init', 'scf_test_register_post_types', 20 ); +add_action( 'acf/init', 'scf_test_create_scf_post_type_entry', 15 ); diff --git a/tests/e2e/rest-api-types-endpoint.spec.ts b/tests/e2e/rest-api-types-endpoint.spec.ts new file mode 100644 index 00000000..f13eac6f --- /dev/null +++ b/tests/e2e/rest-api-types-endpoint.spec.ts @@ -0,0 +1,441 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require('@wordpress/e2e-test-utils-playwright'); + +test.describe('REST API Types Endpoint', () => { + const PLUGIN_SLUG = 'secure-custom-fields'; + const SCF_TEST_POST_TYPE = 'scf-e2e-test-type'; + const OTHER_TEST_POST_TYPE = 'other-e2e-test-type'; + const TEST_POST_TYPE_SLUG = SCF_TEST_POST_TYPE; // Define for backwards compatibility with existing tests + + test.beforeAll(async ({ requestUtils }) => { + // Make sure the SCF plugin is active + await requestUtils.activatePlugin(PLUGIN_SLUG); + + // Activate our test helper plugin that registers post types for testing + await requestUtils.activatePlugin('scf-test-setup-post-types'); + + // Wait a moment for the post types to be registered + await new Promise(resolve => setTimeout(resolve, 2000)); + }); + + // Basic tests that don't require creating a post type + + test('should return types with correct structure', async ({ requestUtils }) => { + try { + const types = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + // At minimum, should include these types + expect(types).toHaveProperty('post'); + expect(types).toHaveProperty('page'); + + // Verify structure of a post type object + expect(types.post).toHaveProperty('name'); + expect(types.post).toHaveProperty('slug'); + expect(types.post).toHaveProperty('rest_base'); + } catch (error) { + throw error; + } + }); + + test('should support source parameter with core value', async ({ requestUtils }) => { + try { + const types = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'core' } + }); + + // Should at least have post and page + expect(types).toHaveProperty('post'); + expect(types).toHaveProperty('page'); + + // Core post types shouldn't have _source=scf + for (const key in types) { + if (types[key]._source) { + expect(types[key]._source).not.toBe('scf'); + } + } + } catch (error) { + throw error; + } + }); + + test('should support source parameter with scf value', async ({ requestUtils }) => { + try { + // Get all source collections for comparison + const allTypes = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + const scfTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'scf' } + }); + + const coreTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'core' } + }); + + const otherTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'other' } + }); + + + // Each post type should only be in one source collection + const allPostTypes = Object.keys(allTypes); + for (const postType of allPostTypes) { + // Count how many source collections contain this post type + let sourceCount = 0; + if (postType in coreTypes) sourceCount++; + if (postType in scfTypes) sourceCount++; + if (postType in otherTypes) sourceCount++; + + // Should be in exactly one source collection + expect(sourceCount).toBe(1); + } + } catch (error) { + throw error; + } + }); + + test('should filter requests for single post types by source', async ({ requestUtils }) => { + // Test 1: Core post type with core source (should succeed) + const postWithCore = await requestUtils.rest({ + path: '/wp/v2/types/post', + params: { source: 'core' } + }); + expect(postWithCore).toHaveProperty('slug', 'post'); + + // Test 2: Core post type with scf source (should fail) + try { + await requestUtils.rest({ + path: '/wp/v2/types/post', + params: { source: 'scf' } + }); + // Should not reach here + throw new Error('Core post type should not be available with SCF source'); + } catch (error) { + // Should fail with a 404 error + expect(error.data).toHaveProperty('status', 404); + } + + // Note: We don't test internal SCF post types anymore since they're no longer + // categorized as 'scf' source - they now fall under 'other' + + // We'll instead look for our test post type which should be categorized as SCF + const customTestType = SCF_TEST_POST_TYPE; // 'scf-e2e-test-type' + + // First check if our test type exists + try { + const allTypes = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + if (customTestType in allTypes) { + // Try to access it with the SCF source parameter + const typeWithScfSource = await requestUtils.rest({ + path: `/wp/v2/types/${customTestType}`, + params: { source: 'scf' } + }); + + // Should succeed if our test post type is properly registered with SCF + expect(typeWithScfSource).toHaveProperty('slug', customTestType); + } else { + } + } catch (error) { + } + }); + + test('should validate source parameter values', async ({ requestUtils }) => { + // Test with an invalid source parameter + try { + await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'invalid' } + }); + // Should not reach here + throw new Error('Should have failed with invalid parameter'); + } catch (error) { + // Should be a REST API error + expect(error).toHaveProperty('code', 'rest_invalid_param'); + } + }); + + // Tests for SCF post types + test.describe('With SCF post type', () => { + // Test that post types are correctly identified by their source + test('should correctly categorize post types by their source', async ({ requestUtils }) => { + // Get all post types + const allTypes = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + + // Get types with source=scf + const scfTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'scf' } + }); + + + // Get types with source=core + const coreTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'core' } + }); + + + // Get types with source=other + const otherTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'other' } + }); + + + // Verify core post types are in the core source + expect(coreTypes).toHaveProperty('post'); + expect(coreTypes).toHaveProperty('page'); + + // Check if our test post types exist in the collection + const hasSCFTestType = SCF_TEST_POST_TYPE in allTypes; + const hasOtherTestType = OTHER_TEST_POST_TYPE in allTypes; + + + // Test that our other test post type is in Other source + if (hasOtherTestType) { + expect(otherTypes).toHaveProperty(OTHER_TEST_POST_TYPE); + expect(scfTypes).not.toHaveProperty(OTHER_TEST_POST_TYPE); + expect(coreTypes).not.toHaveProperty(OTHER_TEST_POST_TYPE); + } else { + } + + // Test that SCF post type is in SCF source (if it exists) + if (hasSCFTestType) { + expect(scfTypes).toHaveProperty(SCF_TEST_POST_TYPE); + expect(coreTypes).not.toHaveProperty(SCF_TEST_POST_TYPE); + expect(otherTypes).not.toHaveProperty(SCF_TEST_POST_TYPE); + } else { + } + + // Verify post type properties if they exist + if (hasSCFTestType) { + const scfPostTypeInfo = allTypes[SCF_TEST_POST_TYPE]; + expect(scfPostTypeInfo).toHaveProperty('name'); + expect(scfPostTypeInfo).toHaveProperty('slug'); + expect(scfPostTypeInfo).toHaveProperty('rest_base'); + expect(scfPostTypeInfo.slug).toBe(SCF_TEST_POST_TYPE); + } + + if (hasOtherTestType) { + const otherPostTypeInfo = allTypes[OTHER_TEST_POST_TYPE]; + expect(otherPostTypeInfo).toHaveProperty('name'); + expect(otherPostTypeInfo).toHaveProperty('slug'); + expect(otherPostTypeInfo).toHaveProperty('rest_base'); + expect(otherPostTypeInfo.slug).toBe(OTHER_TEST_POST_TYPE); + } + }); + + // Instead, test that the source parameter works for filtering + test('should filter post types by source parameter', async ({ requestUtils }) => { + // Get all post types + const allTypes = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + // Get each source type + const scfTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'scf' } + }); + + const coreTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'core' } + }); + + const otherTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'other' } + }); + + // Test 1: Core source should include post and page + expect(coreTypes).toHaveProperty('post'); + expect(coreTypes).toHaveProperty('page'); + + // Test 2: Core types should not be in the other sources + expect(scfTypes).not.toHaveProperty('post'); + expect(scfTypes).not.toHaveProperty('page'); + expect(otherTypes).not.toHaveProperty('post'); + expect(otherTypes).not.toHaveProperty('page'); + + // Test 3: Our custom post type should appear in only one source collection + const customTestType = TEST_POST_TYPE_SLUG; // 'scf-e2e-test-type' + + // Verify the test post type exists and is accessible + if (customTestType in allTypes) { + // Find where our test post type is categorized + const inScf = customTestType in scfTypes; + const inCore = customTestType in coreTypes; + const inOther = customTestType in otherTypes; + + // Test that it only appears in one source + const sourceCount = (inScf ? 1 : 0) + (inCore ? 1 : 0) + (inOther ? 1 : 0); + expect(sourceCount).toBe(1); + + // It should never be in core source + expect(inCore).toBe(false); + + // It should be in either SCF (preferred) or other source + expect(inScf || inOther).toBe(true); + + if (inScf) { + } else if (inOther) { + } + } else { + } + + // Test 4: Verify that each post type only appears in ONE source collection + const allPostTypes = Object.keys(allTypes); + for (const postType of allPostTypes) { + // Count how many source collections contain this post type + let sourceCount = 0; + if (postType in coreTypes) sourceCount++; + if (postType in scfTypes) sourceCount++; + if (postType in otherTypes) sourceCount++; + + // Should be in exactly one source collection + expect(sourceCount).toBe(1); + } + }); + + // Test single post type endpoint with source parameter + test('single post type endpoint should respect source parameter', async ({ requestUtils }) => { + const customTestType = TEST_POST_TYPE_SLUG; // 'scf-e2e-test-type' + const coreType = 'post'; + + // First verify the post types exist + const allTypes = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + // If our custom test type doesn't exist, skip parts of the test + const hasCustomType = customTestType in allTypes; + + // Test 1: Core type should be accessible with no source parameter + const postWithNoSource = await requestUtils.rest({ + path: `/wp/v2/types/${coreType}` + }); + expect(postWithNoSource).toHaveProperty('slug', coreType); + + // Test 2: Core type should be accessible with source=core + const postWithCore = await requestUtils.rest({ + path: `/wp/v2/types/${coreType}`, + params: { source: 'core' } + }); + expect(postWithCore).toHaveProperty('slug', coreType); + + // Test 3: Core type should NOT be accessible with source=other + try { + await requestUtils.rest({ + path: `/wp/v2/types/${coreType}`, + params: { source: 'other' } + }); + throw new Error('Core post type was incorrectly found with other source'); + } catch (error) { + // Should fail with a 404 error + expect(error.data).toHaveProperty('status', 404); + } + + // Only run this part if our custom type was found + if (hasCustomType) { + // First find which source our custom type belongs to + const allTypes = await requestUtils.rest({ + path: '/wp/v2/types' + }); + + const scfTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'scf' } + }); + + const otherTypes = await requestUtils.rest({ + path: '/wp/v2/types', + params: { source: 'other' } + }); + + const inScf = customTestType in scfTypes; + const inOther = customTestType in otherTypes; + + + if (inScf) { + // If in SCF source, test it works with source=scf + const customTypeWithScf = await requestUtils.rest({ + path: `/wp/v2/types/${customTestType}`, + params: { source: 'scf' } + }); + expect(customTypeWithScf).toHaveProperty('slug', customTestType); + + // And should NOT work with other or core + try { + await requestUtils.rest({ + path: `/wp/v2/types/${customTestType}`, + params: { source: 'other' } + }); + throw new Error('Custom post type incorrectly found with other source'); + } catch (error) { + expect(error.data).toHaveProperty('status', 404); + } + } else if (inOther) { + // If in Other source, test it works with source=other + const customTypeWithOther = await requestUtils.rest({ + path: `/wp/v2/types/${customTestType}`, + params: { source: 'other' } + }); + expect(customTypeWithOther).toHaveProperty('slug', customTestType); + + // And should NOT work with scf or core + try { + await requestUtils.rest({ + path: `/wp/v2/types/${customTestType}`, + params: { source: 'scf' } + }); + throw new Error('Custom post type incorrectly found with scf source'); + } catch (error) { + expect(error.data).toHaveProperty('status', 404); + } + } + + // In either case, it should NOT be accessible with source=core + try { + await requestUtils.rest({ + path: `/wp/v2/types/${customTestType}`, + params: { source: 'core' } + }); + throw new Error('Custom post type was incorrectly found with core source'); + } catch (error) { + // Should fail with a 404 error + expect(error.data).toHaveProperty('status', 404); + } + } else { + } + }); + }); + + test.afterAll(async ({ requestUtils }) => { + // Deactivate the plugins + await requestUtils.deactivatePlugin('scf-test-setup-post-types'); + await requestUtils.deactivatePlugin(PLUGIN_SLUG); + + // Clean up by deleting the option that marks the post types as created + await requestUtils.rest({ + path: '/wp/v2/settings', + method: 'POST', + data: { scf_test_post_types_created: false } + }); + }); +}); \ No newline at end of file diff --git a/tests/php/includes/rest-api/test-rest-types-endpoint.php b/tests/php/includes/rest-api/test-rest-types-endpoint.php new file mode 100644 index 00000000..cdd7af0f --- /dev/null +++ b/tests/php/includes/rest-api/test-rest-types-endpoint.php @@ -0,0 +1,211 @@ +endpoint = new SCF_Rest_Types_Endpoint(); + + // Set up reflection for accessing private methods. + $this->reflection = new ReflectionClass( $this->endpoint ); + + // Access the get_source_post_types method. + $this->source_method = $this->reflection->getMethod( 'get_source_post_types' ); + $this->source_method->setAccessible( true ); + } + + /** + * Clean up after each test. + */ + public function tear_down() { + // Check if our test post type needs to be unregistered. + if ( post_type_exists( $this->test_post_type ) ) { + unregister_post_type( $this->test_post_type ); + } + + parent::tear_down(); + } + + /** + * Test that the SCF_Rest_Types_Endpoint class exists. + */ + public function test_endpoint_class_exists() { + $this->assertTrue( class_exists( 'SCF_Rest_Types_Endpoint' ) ); + } + + /** + * Test that the source parameter is properly added to collection params. + */ + public function test_add_collection_params() { + // Test with empty parameters. + $empty_params = array(); + $modified_empty_params = $this->endpoint->add_collection_params( $empty_params ); + + $this->assertArrayHasKey( 'source', $modified_empty_params ); + $this->assertCount( 1, $modified_empty_params ); + + // Test with existing parameters. + $existing_params = array( + 'context' => array( + 'default' => 'view', + 'enum' => array( 'view', 'embed', 'edit' ), + ), + ); + $modified_existing_params = $this->endpoint->add_collection_params( $existing_params ); + + $this->assertArrayHasKey( 'source', $modified_existing_params ); + $this->assertArrayHasKey( 'context', $modified_existing_params ); + $this->assertCount( 2, $modified_existing_params ); + $this->assertEquals( 'view', $modified_existing_params['context']['default'] ); + + // Check parameter properties. + $source_param = $modified_existing_params['source']; + $this->assertEquals( 'string', $source_param['type'] ); + $this->assertFalse( $source_param['required'] ); + $this->assertContains( 'core', $source_param['enum'] ); + $this->assertContains( 'scf', $source_param['enum'] ); + $this->assertContains( 'other', $source_param['enum'] ); + $this->assertArrayHasKey( 'validate_callback', $source_param ); + $this->assertArrayHasKey( 'sanitize_callback', $source_param ); + } + + /** + * Test the get_source_post_types method for SCF post types + */ + public function test_get_source_post_types_scf() { + $scf_types = $this->source_method->invoke( $this->endpoint, 'scf' ); + + $this->assertIsArray( $scf_types, 'SCF types should be an array' ); + + // Should not include core types + $this->assertNotContains( 'post', $scf_types ); + $this->assertNotContains( 'page', $scf_types ); + } + + /** + * Test the get_source_post_types method for core post types + */ + public function test_get_source_post_types_core() { + $core_types = $this->source_method->invoke( $this->endpoint, 'core' ); + + $this->assertIsArray( $core_types, 'Core types should be an array' ); + + // Check for core post types. + $this->assertContains( 'post', $core_types ); + $this->assertContains( 'page', $core_types ); + + // Should not include SCF types. + $this->assertNotContains( 'acf-field-group', $core_types ); + $this->assertNotContains( 'acf-post-type', $core_types ); + } + + /** + * Test the get_source_post_types method for other post types + */ + public function test_get_source_post_types_other() { + // Register a test post type. + register_post_type( + $this->test_post_type, + array( + 'labels' => array( 'name' => 'Test Post Type' ), + 'public' => true, + ) + ); + + $other_types = $this->source_method->invoke( $this->endpoint, 'other' ); + + $this->assertIsArray( $other_types, 'Other types should be an array' ); + + // Should include our test post type. + $this->assertContains( $this->test_post_type, $other_types ); + + // Should not include core types + $this->assertNotContains( 'post', $other_types ); + $this->assertNotContains( 'page', $other_types ); + } + + /** + * Test the get_source_post_types method with an invalid source parameter + */ + public function test_get_source_post_types_invalid() { + $invalid_types = $this->source_method->invoke( $this->endpoint, 'invalid' ); + + $this->assertIsArray( $invalid_types ); + $this->assertEmpty( $invalid_types, 'Invalid source should return empty array' ); + } + + /** + * Test the source parameter definition. + */ + public function test_source_parameter_definition() { + $param_method = $this->reflection->getMethod( 'get_source_param_definition' ); + $param_method->setAccessible( true ); + + // Test without validation callbacks. + $param_def = $param_method->invoke( $this->endpoint, false ); + $this->assertEquals( 'string', $param_def['type'] ); + $this->assertFalse( $param_def['required'] ); + $this->assertContains( 'core', $param_def['enum'] ); + $this->assertContains( 'scf', $param_def['enum'] ); + $this->assertContains( 'other', $param_def['enum'] ); + $this->assertCount( 3, $param_def['enum'] ); + + // Test with validation callbacks. + $param_def_with_validation = $param_method->invoke( $this->endpoint, true ); + $this->assertEquals( 'string', $param_def_with_validation['type'] ); + $this->assertFalse( $param_def_with_validation['required'] ); + $this->assertContains( 'core', $param_def_with_validation['enum'] ); + $this->assertContains( 'scf', $param_def_with_validation['enum'] ); + $this->assertContains( 'other', $param_def_with_validation['enum'] ); + $this->assertArrayHasKey( 'validate_callback', $param_def_with_validation ); + $this->assertArrayHasKey( 'sanitize_callback', $param_def_with_validation ); + $this->assertEquals( 'rest_validate_request_arg', $param_def_with_validation['validate_callback'] ); + $this->assertEquals( 'sanitize_text_field', $param_def_with_validation['sanitize_callback'] ); + } +}