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..e3ac2d18 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,146 @@ 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 ); + + // Clear cache after each request to make the endpoint stateless + add_action( 'rest_request_after_callbacks', array( $this, 'clear_cache_for_types_request' ), 10, 1 ); + } + + /** + * Filter post types requests, fires for both collection and individual requests. + * We only want to handle individual requets to ensure the post type requested matches the source. + * + * @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 ) { + // We only want to handle individual requests + $route = $request->get_route(); + if ( ! preg_match( '#^/wp/v2/types/([^/]+)$#', $route, $matches ) ) { + return $response; + } + + $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 with caching within this single request + if ( null === $this->cached_post_types ) { + $this->cached_post_types = $this->get_source_post_types( $source ); + } + $source_post_types = $this->cached_post_types; + + // Check if the requested type matches the source + $requested_type = $matches[1]; + 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 ) + ); + } + + 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; + } + + // Get post types with caching within this single request + 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 ) { + // 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 +187,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 +271,129 @@ 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; + } + + /** + * Clear cache for types endpoint requests to prevent cross-contamination + * + * @since SCF 6.5.0 + * + * @param mixed $response The current response. + */ + public function clear_cache_for_types_request( $response ) { + // Clear cache to prevent cross-contamination and make the endpoint stateless + $this->cached_post_types = null; + 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..331c5c58 --- /dev/null +++ b/tests/e2e/plugins/scf-test-setup-post-types.php @@ -0,0 +1,123 @@ + 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'] ) ) { + // Store the post ID so we can delete it later + update_option( 'scf_test_post_type_created', $result['ID'] ); + } +} + +/** + * Clean up on plugin deactivation + */ +function scf_test_cleanup() { + // Get the stored post ID and delete the post + $post_id = get_option( 'scf_test_post_type_created' ); + if ( $post_id ) { + wp_delete_post( $post_id, true ); + } + + // Clean up the option + delete_option( 'scf_test_post_type_created' ); +} + +// Register hooks +add_action( 'init', 'scf_test_register_post_types', 20 ); +add_action( 'acf/init', 'scf_test_create_scf_post_type_entry', 15 ); +register_deactivation_hook( __FILE__, 'scf_test_cleanup' ); 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..0c474bbf --- /dev/null +++ b/tests/e2e/rest-api-types-endpoint.spec.ts @@ -0,0 +1,390 @@ +/** + * 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 that 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 that 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 ); + } + + // We look for the test post type which should be categorized as SCF + const customTestType = SCF_TEST_POST_TYPE; // 'scf-e2e-test-type' + + // Get all types and verify our test type exists + const allTypes = await requestUtils.rest( { + path: '/wp/v2/types', + } ); + + // Test post type should exist - fail fast if it doesn't + expect( allTypes ).toHaveProperty( customTestType ); + + // 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 ); + } ); + + 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' ); + + // Test post types should exist - fail fast if they don't + expect( allTypes ).toHaveProperty( SCF_TEST_POST_TYPE ); + expect( allTypes ).toHaveProperty( OTHER_TEST_POST_TYPE ); + + // Test that our other test post type is in Other source + expect( otherTypes ).toHaveProperty( OTHER_TEST_POST_TYPE ); + expect( scfTypes ).not.toHaveProperty( OTHER_TEST_POST_TYPE ); + expect( coreTypes ).not.toHaveProperty( OTHER_TEST_POST_TYPE ); + + // Test that SCF post type is in SCF source + expect( scfTypes ).toHaveProperty( SCF_TEST_POST_TYPE ); + expect( coreTypes ).not.toHaveProperty( SCF_TEST_POST_TYPE ); + expect( otherTypes ).not.toHaveProperty( SCF_TEST_POST_TYPE ); + + // Verify post type properties + 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 ); + + 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 that core source should include post and page + expect( coreTypes ).toHaveProperty( 'post' ); + expect( coreTypes ).toHaveProperty( 'page' ); + + // Test that 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 that our custom post type should appear in only one source collection + const customTestType = TEST_POST_TYPE_SLUG; // 'scf-e2e-test-type' + + // Test post type should exist - fail fast if it doesn't + expect( allTypes ).toHaveProperty( customTestType ); + + // 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 ); + + // Test 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', + } ); + + // Test post type should exist - fail fast if it doesn't + expect( allTypes ).toHaveProperty( customTestType ); + + // Test that 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 that 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 that 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 ); + } + + // SCF test type should be accessible with SCF source + const customTypeWithScf = await requestUtils.rest( { + path: `/wp/v2/types/${ customTestType }`, + params: { source: 'scf' }, + } ); + expect( customTypeWithScf ).toHaveProperty( + 'slug', + customTestType + ); + + // SCF test type should NOT be accessible with other source + try { + await requestUtils.rest( { + path: `/wp/v2/types/${ customTestType }`, + params: { source: 'other' }, + } ); + throw new Error( + 'SCF test type incorrectly found with other source' + ); + } catch ( error ) { + expect( error.data ).toHaveProperty( 'status', 404 ); + } + + // SCF test type should NOT be accessible with core source + try { + await requestUtils.rest( { + path: `/wp/v2/types/${ customTestType }`, + params: { source: 'core' }, + } ); + throw new Error( + 'SCF test type incorrectly found with core source' + ); + } catch ( error ) { + expect( error.data ).toHaveProperty( 'status', 404 ); + } + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'scf-test-setup-post-types' ); + await requestUtils.deactivatePlugin( PLUGIN_SLUG ); + } ); +} ); 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'] ); + } +}