From 0ae48c647b4de6549120706385ad15b9423bd1eb Mon Sep 17 00:00:00 2001 From: Csaba Maulis Date: Wed, 19 Apr 2023 15:48:32 +0800 Subject: [PATCH 1/6] Add `featured` field to the images array for Product REST API --- .../class-wc-rest-products-controller.php | 3 ++ ...lass-wc-rest-products-controller-tests.php | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index 1a61edd6c4d..be8ffdb8ee2 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -45,9 +45,11 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { protected function get_images( $product ) { $images = array(); $attachment_ids = array(); + $featured_id = null; // Add featured image. if ( $product->get_image_id() ) { + $featured_id = $product->get_image_id(); $attachment_ids[] = $product->get_image_id(); } @@ -68,6 +70,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { $images[] = array( 'id' => (int) $attachment_id, + 'featured' => (int) $featured_id === (int) $attachment_id, 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php index c4419b92499..8e45f7b42c5 100644 --- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php +++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php @@ -204,6 +204,36 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case { } } + /** + * Test that all fields are returned when requested one by one. + */ + public function test_products_get_images_array_contains_featured() { + global $wpdb; + + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $gallery_url = media_sideload_image( 'https://cldup.com/6L9h56D9Bw.jpg', $product->get_id(), 'gallery', 'src' ); + $this->assertNotWPError( $gallery_url ); + $gallery_id = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $gallery_url ) ); + + $featured_url = media_sideload_image( 'http://cldup.com/Dr1Bczxq4q.png', $product->get_id(), 'featured', 'src' ); + $this->assertNotWPError( $featured_url ); + $featured_id = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $featured_url ) ); + + $product->set_image_id( $featured_id[0] ); + $product->set_gallery_image_ids( $gallery_id[0] ); + $product->save(); + + $request = new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ); + $request->set_param( '_fields', 'images' ); + $response = $this->server->dispatch( $request ); + $images_array = $response->get_data()['images']; + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( $images_array[0]['featured'] ); + $this->assertEquals( 'featured', $images_array[0]['name'] ); + $this->assertFalse( $images_array[1]['featured'] ); + $this->assertEquals( 'gallery', $images_array[1]['name'] ); + } + /** * Test that the `search` parameter does partial matching in the product name, but not the SKU. * From 7c9ae871c3a17cb0f8f3fe0d2156ae6d59daf0a1 Mon Sep 17 00:00:00 2001 From: Csaba Maulis Date: Wed, 19 Apr 2023 15:52:36 +0800 Subject: [PATCH 2/6] Add changelog entry --- .../changelog/add-rest-api-products-featured-image | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-rest-api-products-featured-image diff --git a/plugins/woocommerce/changelog/add-rest-api-products-featured-image b/plugins/woocommerce/changelog/add-rest-api-products-featured-image new file mode 100644 index 00000000000..0592a983c8f --- /dev/null +++ b/plugins/woocommerce/changelog/add-rest-api-products-featured-image @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Add `featured` field to the images array for Product REST API From 135d6f2b273b0a5e927dc7565370af71a9cd0eed Mon Sep 17 00:00:00 2001 From: Csaba Maulis Date: Thu, 20 Apr 2023 13:14:02 +0800 Subject: [PATCH 3/6] Add featured field to the schema with tests --- .../class-wc-rest-products-controller.php | 6 +++ ...lass-wc-rest-products-controller-tests.php | 50 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index be8ffdb8ee2..7614225c720 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -1256,6 +1256,12 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), + 'featured' => array( + 'description' => __( 'Featured image.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php index 8e45f7b42c5..d82474093c9 100644 --- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php +++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php @@ -205,7 +205,55 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case { } /** - * Test that all fields are returned when requested one by one. + * Test that featured field exists under images in product argument. + */ + public function test_products_args_includes_featured_field() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products' ); + $request->set_param( '_fields', 'endpoints.1.args.images.items.properties' ); + $response = $this->server->dispatch( $request ); + $image_arg = $response->get_data()['endpoints'][1]['args']['images']['items']['properties']; + $this->assertArrayHasKey( 'featured', $image_arg ); + $this->assertEquals( + array( + 'description' => 'Featured image.', + 'type' => 'boolean', + 'default' => false, + 'context' => + array( + 0 => 'view', + 1 => 'edit', + ), + ), + $image_arg['featured'] + ); + } + + /** + * Test that featured field exists under images in product schema. + */ + public function test_products_schema_includes_featured_field() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products' ); + $request->set_param( '_fields', 'schema.properties.images.items.properties' ); + $response = $this->server->dispatch( $request ); + $image_schema = $response->get_data()['schema']['properties']['images']['items']['properties']; + $this->assertArrayHasKey( 'featured', $image_schema ); + $this->assertEquals( + array( + 'description' => 'Featured image.', + 'type' => 'boolean', + 'default' => false, + 'context' => + array( + 0 => 'view', + 1 => 'edit', + ), + ), + $image_schema['featured'] + ); + } + + /** + * Test that feature field is returned inside images. */ public function test_products_get_images_array_contains_featured() { global $wpdb; From 46dceffecebd6eaf9a41dc15584498bc6f303bde Mon Sep 17 00:00:00 2001 From: Csaba Maulis Date: Thu, 20 Apr 2023 13:16:21 +0800 Subject: [PATCH 4/6] Implement new featured field check Internal logic: - If the featured field is absent from all image objects in the request, set the first image in the array to featured. - If the featured field is present in all image objects, we respect the value and set the featured image accordingly. - If the featured field is absent from some image object, we set the featured image based on whether a true value exists; otherwise, we set the first image as featured. - if multiple images have the featured field set to true, return a new 400 bad request response.. --- .../class-wc-rest-products-controller.php | 29 +++++++++++++++++-- ...lass-wc-rest-products-controller-tests.php | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index 7614225c720..1529e0506fd 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -308,6 +308,31 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { $images = is_array( $images ) ? array_filter( $images ) : array(); if ( ! empty( $images ) ) { + $featured_image_index = 0; + $featured_image_count = 0; + + // Collect featured field usage. + foreach ( $images as $index => $image ) { + if ( isset( $image['featured'] ) && $image['featured'] ) { + $featured_image_index = $index; + $featured_image_count++; + } + } + + // Handle multiple featured images. + if ($featured_image_count > 1) { + throw new WC_REST_Exception( + 'woocommerce_rest_product_featured_image_count', + __( 'Only one featured image is allowed.', 'woocommerce' ), + 400 + ); + } + + // If no featured image is set, and the first image explicitly set to false do not set featured at all. + if ( 0 === $featured_image_count && isset( $images[0]['featured'] ) && false === $images[0]['featured'] ) { + $featured_image_index = null; + } + $gallery = array(); foreach ( $images as $index => $image ) { @@ -332,9 +357,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); } - $featured_image = $product->get_image_id(); - - if ( 0 === $index ) { + if ( $featured_image_index === $index ) { $product->set_image_id( $attachment_id ); } else { $gallery[] = $attachment_id; diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php index d82474093c9..71569ba483b 100644 --- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php +++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php @@ -263,7 +263,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case { $this->assertNotWPError( $gallery_url ); $gallery_id = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $gallery_url ) ); - $featured_url = media_sideload_image( 'http://cldup.com/Dr1Bczxq4q.png', $product->get_id(), 'featured', 'src' ); + $featured_url = media_sideload_image( 'https://cldup.com/Dr1Bczxq4q.png', $product->get_id(), 'featured', 'src' ); $this->assertNotWPError( $featured_url ); $featured_id = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $featured_url ) ); From 48d8bec2e370bb21ac4f8ec7d6f20bf975030a3d Mon Sep 17 00:00:00 2001 From: Csaba Maulis Date: Thu, 20 Apr 2023 15:48:23 +0800 Subject: [PATCH 5/6] Add CRUD tests --- .../tests/products/products-crud.test.js | 232 +++++++++++++++++- 1 file changed, 230 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js index 9ab7a6163e1..00fcd5b58d7 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js @@ -265,7 +265,7 @@ test.describe('Products API tests: CRUD', () => { expect(responseJSON.type).toEqual('select'); expect(responseJSON.order_by).toEqual('name'); // the below has_archives test is currently not working as expected - // an issue (https://github.com/woocommerce/woocommerce/issues/34991) + // an issue (https://github.com/woocommerce/woocommerce/issues/34991) // has been raised and this test can be // updated as appropriate after triage // expect(responseJSON.has_archives).toEqual(true); @@ -913,7 +913,6 @@ test.describe('Products API tests: CRUD', () => { }); }); - test.describe('Product tags tests: CRUD', () => { let productTagId; @@ -1074,6 +1073,235 @@ test.describe('Products API tests: CRUD', () => { }); }); + test.describe( 'Product images tests: CRUD', () => { + let productId; + let images; + + test( 'can add product with an image', async ( { request } ) => { + const response = await request.post( 'wp-json/wc/v3/products', { + data: { + images: [ { src: 'https://cldup.com/6L9h56D9Bw.jpg' } ], + }, + } ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 201 ); + expect( responseJSON.images ).toHaveLength( 1 ); + expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' ); + expect( responseJSON.images[ 0 ].featured ).toBeTruthy(); + expect( responseJSON.images[ 0 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 0 ].src ).toContain( '6L9h56D9Bw' ); + + // Cleanup: Delete the used product + await request.delete( + `wp-json/wc/v3/products/${ responseJSON.id }`, + { + data: { + force: true, + }, + } + ); + } ); + + test( 'can add product with multiple images (backward compatible)', async ( { + request, + } ) => { + const response = await request.post( 'wp-json/wc/v3/products', { + data: { + images: [ + { src: 'https://cldup.com/6L9h56D9Bw.jpg' }, + { src: 'https://cldup.com/Dr1Bczxq4q.png' }, + ], + }, + } ); + const responseJSON = await response.json(); + productId = responseJSON.id; + images = responseJSON.images.map( ( image ) => image.id ); + + expect( response.status() ).toEqual( 201 ); + expect( responseJSON.images ).toHaveLength( 2 ); + expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' ); + expect( responseJSON.images[ 0 ].featured ).toBeTruthy(); + expect( responseJSON.images[ 1 ].name ).toContain( 'Dr1Bczxq4q' ); + expect( responseJSON.images[ 1 ].featured ).toBeFalsy(); + } ); + + test( 'can add product with multiple images (explicit featured)', async ( { + request, + } ) => { + const response = await request.post( 'wp-json/wc/v3/products', { + data: { + images: [ + { + src: 'https://cldup.com/6L9h56D9Bw.jpg', + featured: false, + }, + { + src: 'https://cldup.com/Dr1Bczxq4q.png', + featured: true, + }, + ], + }, + } ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 201 ); + expect( responseJSON.images ).toHaveLength( 2 ); + // When retrieving, the featured image is always the first one. + expect( responseJSON.images[ 0 ].name ).toContain( 'Dr1Bczxq4q' ); + expect( responseJSON.images[ 0 ].featured ).toBeTruthy(); + expect( responseJSON.images[ 1 ].name ).toContain( '6L9h56D9Bw' ); + expect( responseJSON.images[ 1 ].featured ).toBeFalsy(); + + // Cleanup: Delete the used product + await request.delete( + `wp-json/wc/v3/products/${ responseJSON.id }`, + { + data: { + force: true, + }, + } + ); + } ); + + test( 'can add product with multiple images (no featured)', async ( { + request, + } ) => { + const response = await request.post( 'wp-json/wc/v3/products', { + data: { + images: [ + { + src: 'https://cldup.com/6L9h56D9Bw.jpg', + featured: false, + }, + { + src: 'https://cldup.com/Dr1Bczxq4q.png', + featured: false, + }, + ], + }, + } ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 201 ); + expect( responseJSON.images ).toHaveLength( 2 ); + expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' ); + expect( responseJSON.images[ 0 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 0 ].featured ).toBeFalsy(); + expect( responseJSON.images[ 1 ].name ).toContain( 'Dr1Bczxq4q' ); + expect( responseJSON.images[ 1 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 1 ].featured ).toBeFalsy(); + + // Cleanup: Delete the used product + await request.delete( + `wp-json/wc/v3/products/${ responseJSON.id }`, + { + data: { + force: true, + }, + } + ); + } ); + + test( 'cannot add product with multiple images (all featured)', async ( { + request, + } ) => { + const response = await request.post( 'wp-json/wc/v3/products', { + data: { + images: [ + { + src: 'https://cldup.com/6L9h56D9Bw.jpg', + featured: true, + }, + { + src: 'https://cldup.com/Dr1Bczxq4q.png', + featured: true, + }, + ], + }, + } ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 400 ); + expect( responseJSON.code ).toEqual( + 'woocommerce_rest_product_featured_image_count' + ); + expect( responseJSON.message ).toEqual( + 'Only one featured image is allowed.' + ); + expect( responseJSON.data ).toEqual( { status: 400 } ); + } ); + + test( 'can retrieve product images', async ( { request } ) => { + const response = await request.get( + `wp-json/wc/v3/products/${ productId }` + ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.images ).toHaveLength( 2 ); + expect( responseJSON.images[ 0 ].id ).toEqual( images[ 0 ] ); + expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' ); + expect( responseJSON.images[ 0 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 0 ].featured ).toBeTruthy(); + } ); + + test( 'can update a product images', async ( { request } ) => { + // call API to update a product + const response = await request.put( + `wp-json/wc/v3/products/${ productId }`, + { + data: { + images: [ + { id: images[ 0 ], featured: false }, + { id: images[ 1 ], featured: true }, + ], + }, + } + ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.images ).toHaveLength( 2 ); + // When retrieving, the featured image is always the first one. + expect( responseJSON.images[ 0 ].id ).toEqual( images[ 1 ] ); + expect( responseJSON.images[ 0 ].name ).toContain( 'Dr1Bczxq4q' ); + expect( responseJSON.images[ 0 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 0 ].featured ).toBeTruthy(); + expect( responseJSON.images[ 1 ].id ).toEqual( images[ 0 ] ); + expect( responseJSON.images[ 1 ].name ).toContain( '6L9h56D9Bw' ); + expect( responseJSON.images[ 1 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 1 ].featured ).toBeFalsy(); + } ); + + test( 'can remove an image from a product', async ( { request } ) => { + // Delete the product attribute. + const response = await request.put( + `wp-json/wc/v3/products/${ productId }`, + { + data: { + images: [ { id: images[ 1 ] } ], + }, + } + ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.images ).toHaveLength( 1 ); + expect( responseJSON.images[ 0 ].id ).toEqual( images[ 1 ] ); + expect( responseJSON.images[ 0 ].name ).toContain( 'Dr1Bczxq4q' ); + expect( responseJSON.images[ 0 ].alt ).toEqual( '' ); + expect( responseJSON.images[ 0 ].featured ).toBeTruthy(); + + // Cleanup: Delete the used product + await request.delete( `wp-json/wc/v3/products/${ productId }`, { + data: { + force: true, + }, + } ); + } ); + } ); + test('can add a virtual product', async ({ request }) => { From 53276e786d667f39c4fb4cd345d93e1d6a3d9981 Mon Sep 17 00:00:00 2001 From: Csaba Maulis Date: Thu, 20 Apr 2023 16:38:00 +0800 Subject: [PATCH 6/6] Fix CS --- .../Controllers/Version3/class-wc-rest-products-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index 1529e0506fd..ce0ba6bb95d 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -320,7 +320,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { } // Handle multiple featured images. - if ($featured_image_count > 1) { + if ( $featured_image_count > 1 ) { throw new WC_REST_Exception( 'woocommerce_rest_product_featured_image_count', __( 'Only one featured image is allowed.', 'woocommerce' ),