From 91647f305188b80adfd9c66242da7ba85687811a Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 7 Jun 2019 17:16:28 +0100 Subject: [PATCH] ProductReviews/Products tests --- .../Version4/Controllers/ProductReviews.php | 30 +- tests/AbstractRestApiTest.php | 42 +- tests/Version4/Coupons.php | 12 +- tests/Version4/Orders.php | 20 +- tests/Version4/ProductReviews.php | 400 +++++++++ tests/Version4/product-reviews.php | 50 -- tests/Version4/products.php | 809 +++++++++++++++++- 7 files changed, 1228 insertions(+), 135 deletions(-) create mode 100644 tests/Version4/ProductReviews.php delete mode 100644 tests/Version4/product-reviews.php diff --git a/src/RestApi/Version4/Controllers/ProductReviews.php b/src/RestApi/Version4/Controllers/ProductReviews.php index e7ebff54136..351ded63a1c 100644 --- a/src/RestApi/Version4/Controllers/ProductReviews.php +++ b/src/RestApi/Version4/Controllers/ProductReviews.php @@ -297,11 +297,11 @@ class ProductReviews extends AbstractController { } /** - * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API. + * Filters arguments, before passing to \WP_Comment_Query, when querying reviews via the REST API. * * @since 3.5.0 - * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ - * @param array $prepared_args Array of arguments for WP_Comment_Query. + * @link https://developer.wordpress.org/reference/classes/\WP_Comment_Query/ + * @param array $prepared_args Array of arguments for \WP_Comment_Query. * @param \WP_REST_Request $request The current request. */ $prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request ); @@ -310,7 +310,7 @@ class ProductReviews extends AbstractController { $prepared_args['type'] = 'review'; // Query reviews. - $query = new WP_Comment_Query(); + $query = new \WP_Comment_Query(); $query_result = $query->query( $prepared_args ); $reviews = array(); @@ -330,7 +330,7 @@ class ProductReviews extends AbstractController { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'], $prepared_args['offset'] ); - $query = new WP_Comment_Query(); + $query = new \WP_Comment_Query(); $prepared_args['count'] = true; $total_reviews = $query->query( $prepared_args ); @@ -368,7 +368,7 @@ class ProductReviews extends AbstractController { * Create a single review. * * @param \WP_REST_Request $request Full details about the request. - * @return \WP_Error|WP_REST_Response + * @return \WP_Error|\WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { @@ -499,7 +499,7 @@ class ProductReviews extends AbstractController { * Get a single product review. * * @param \WP_REST_Request $request Full details about the request. - * @return \WP_Error|WP_REST_Response + * @return \WP_Error|\WP_REST_Response */ public function get_item( $request ) { $review = $this->get_review( $request['id'] ); @@ -517,7 +517,7 @@ class ProductReviews extends AbstractController { * Updates a review. * * @param \WP_REST_Request $request Full details about the request. - * @return \WP_Error|WP_REST_Response Response object on success, or error object on failure. + * @return \WP_Error|\WP_REST_Response Response object on success, or error object on failure. */ public function update_item( $request ) { $review = $this->get_review( $request['id'] ); @@ -603,7 +603,7 @@ class ProductReviews extends AbstractController { * Deletes a review. * * @param \WP_REST_Request $request Full details about the request. - * @return \WP_Error|WP_REST_Response Response object on success, or error object on failure. + * @return \WP_Error|\WP_REST_Response Response object on success, or error object on failure. */ public function delete_item( $request ) { $review = $this->get_review( $request['id'] ); @@ -629,7 +629,7 @@ class ProductReviews extends AbstractController { if ( $force ) { $previous = $this->prepare_item_for_response( $review, $request ); $result = wp_delete_comment( $review->comment_ID, true ); - $response = new WP_REST_Response(); + $response = new \WP_REST_Response(); $response->set_data( array( 'deleted' => true, @@ -660,7 +660,7 @@ class ProductReviews extends AbstractController { * Fires after a review is deleted via the REST API. * * @param WP_Comment $review The deleted review data. - * @param WP_REST_Response $response The response returned from the API. + * @param \WP_REST_Response $response The response returned from the API. * @param \WP_REST_Request $request The request sent to the API. */ do_action( 'woocommerce_rest_delete_review', $review, $response, $request ); @@ -673,7 +673,7 @@ class ProductReviews extends AbstractController { * * @param WP_Comment $review Product review object. * @param \WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. + * @return \WP_REST_Response $response Response data. */ public function prepare_item_for_response( $review, $request ) { $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -725,7 +725,7 @@ class ProductReviews extends AbstractController { /** * Filter product reviews object returned from the REST API. * - * @param WP_REST_Response $response The response object. + * @param \WP_REST_Response $response The response object. * @param WP_Comment $review Product review object used to create response. * @param \WP_REST_Request $request Request object. */ @@ -1021,8 +1021,8 @@ class ProductReviews extends AbstractController { * Filter collection parameters for the reviews controller. * * This filter registers the collection parameter, but does not map the - * collection parameter to an internal WP_Comment_Query parameter. Use the - * `wc_rest_review_query` filter to set WP_Comment_Query parameters. + * collection parameter to an internal \WP_Comment_Query parameter. Use the + * `wc_rest_review_query` filter to set \WP_Comment_Query parameters. * * @since 3.5.0 * @param array $params JSON Schema-formatted collection parameters. diff --git a/tests/AbstractRestApiTest.php b/tests/AbstractRestApiTest.php index 3809d31461e..1d60be6f376 100644 --- a/tests/AbstractRestApiTest.php +++ b/tests/AbstractRestApiTest.php @@ -90,60 +90,28 @@ abstract class AbstractRestApiTest extends WC_REST_Unit_Test_Case { } /** - * Classes should test creation using this method. + * Test creation using this method. * If read-only, test to confirm this. */ abstract public function test_create(); /** - * Classes should test get/read using this method. + * Test get/read using this method. */ abstract public function test_read(); /** - * Classes should test updates using this method. + * Test updates using this method. * If read-only, test to confirm this. */ abstract public function test_update(); /** - * Classes should test delete using this method. + * Test delete using this method. * If read-only, test to confirm this. */ abstract public function test_delete(); - /** - * Tests delete when there is no user logged in. - */ - public function test_guest_create() { - wp_set_current_user( 0 ); - $this->assertEquals( 0, get_current_user_id() ); - } - - /** - * Tests delete when there is no user logged in. - */ - public function test_guest_read() { - wp_set_current_user( 0 ); - $this->assertEquals( 0, get_current_user_id() ); - } - - /** - * Tests delete when there is no user logged in. - */ - public function test_guest_update() { - wp_set_current_user( 0 ); - $this->assertEquals( 0, get_current_user_id() ); - } - - /** - * Tests delete when there is no user logged in. - */ - public function test_guest_delete() { - wp_set_current_user( 0 ); - $this->assertEquals( 0, get_current_user_id() ); - } - /** * Perform a request and return the status and returned data. * @@ -153,7 +121,7 @@ abstract class AbstractRestApiTest extends WC_REST_Unit_Test_Case { * @return object */ protected function do_request( $endpoint, $type = 'GET', $params = [] ) { - $request = new \WP_REST_Request( $type, $endpoint ); + $request = new \WP_REST_Request( $type, untrailingslashit( $endpoint ) ); 'GET' === $type ? $request->set_query_params( $params ) : $request->set_body_params( $params ); $response = $this->server->dispatch( $request ); diff --git a/tests/Version4/Coupons.php b/tests/Version4/Coupons.php index 7c6d65cc0dd..b456b2ded0f 100644 --- a/tests/Version4/Coupons.php +++ b/tests/Version4/Coupons.php @@ -189,8 +189,7 @@ class Coupons extends AbstractRestApiTest { * Test read. */ public function test_guest_create() { - parent::test_guest_create(); - + wp_set_current_user( 0 ); $valid_data = [ 'code' => 'test-coupon', 'amount' => '5.00', @@ -225,8 +224,7 @@ class Coupons extends AbstractRestApiTest { * Test read. */ public function test_guest_read() { - parent::test_guest_read(); - + wp_set_current_user( 0 ); $response = $this->do_request( '/wc/v4/coupons', 'GET' ); $this->assertExpectedResponse( $response, 401 ); } @@ -235,8 +233,7 @@ class Coupons extends AbstractRestApiTest { * Test update. */ public function test_guest_update() { - parent::test_guest_update(); - + wp_set_current_user( 0 ); $coupon = \WC_Helper_Coupon::create_coupon( 'testcoupon-1' ); $response = $this->do_request( '/wc/v4/coupons/' . $coupon->get_id(), @@ -253,8 +250,7 @@ class Coupons extends AbstractRestApiTest { * Test delete. */ public function test_guest_delete() { - parent::test_guest_delete(); - + wp_set_current_user( 0 ); $coupon = \WC_Helper_Coupon::create_coupon( 'testcoupon-1' ); $result = $this->do_request( '/wc/v4/coupons/' . $coupon->get_id(), 'DELETE', [ 'force' => false ] ); $this->assertEquals( 401, $result->status ); diff --git a/tests/Version4/Orders.php b/tests/Version4/Orders.php index 15ee8e399a3..55ceeb59d1d 100644 --- a/tests/Version4/Orders.php +++ b/tests/Version4/Orders.php @@ -371,8 +371,7 @@ class Orders extends AbstractRestApiTest { * Test read. */ public function test_guest_create() { - parent::test_guest_create(); - + wp_set_current_user( 0 ); $product = \WC_Helper_Product::create_simple_product(); $data = [ 'currency' => 'ZAR', @@ -428,8 +427,7 @@ class Orders extends AbstractRestApiTest { * Test read. */ public function test_guest_read() { - parent::test_guest_read(); - + wp_set_current_user( 0 ); $response = $this->do_request( '/wc/v4/orders', 'GET' ); $this->assertExpectedResponse( $response, 401 ); } @@ -438,8 +436,7 @@ class Orders extends AbstractRestApiTest { * Test update. */ public function test_guest_update() { - parent::test_guest_update(); - + wp_set_current_user( 0 ); $order = \WC_Helper_Order::create_order(); $data = [ 'payment_method' => 'test-update', @@ -460,8 +457,7 @@ class Orders extends AbstractRestApiTest { * Test delete. */ public function test_guest_delete() { - parent::test_guest_delete(); - + wp_set_current_user( 0 ); $order = \WC_Helper_Order::create_order(); $response = $this->do_request( '/wc/v4/orders/' . $order->get_id(), 'DELETE', [ 'force' => true ] ); $this->assertEquals( 401, $response->status ); @@ -547,7 +543,7 @@ class Orders extends AbstractRestApiTest { $fee_data = current( $order->get_items( 'fee' ) ); $response = $this->do_request( - '/wc/v3/orders/' . $order->get_id(), + '/wc/v4/orders/' . $order->get_id(), 'PUT', [ 'fee_lines' => array( @@ -575,7 +571,7 @@ class Orders extends AbstractRestApiTest { $coupon->save(); $response = $this->do_request( - '/wc/v3/orders/' . $order->get_id(), + '/wc/v4/orders/' . $order->get_id(), 'PUT', [ 'coupon_lines' => array( @@ -609,7 +605,7 @@ class Orders extends AbstractRestApiTest { $coupon_data = current( $order->get_items( 'coupon' ) ); $response = $this->do_request( - '/wc/v3/orders/' . $order->get_id(), + '/wc/v4/orders/' . $order->get_id(), 'PUT', [ 'coupon_lines' => array( @@ -640,7 +636,7 @@ class Orders extends AbstractRestApiTest { public function test_invalid_coupon() { $order = \WC_Helper_Order::create_order(); $response = $this->do_request( - '/wc/v3/orders/' . $order->get_id(), + '/wc/v4/orders/' . $order->get_id(), 'PUT', [ 'coupon_lines' => array( diff --git a/tests/Version4/ProductReviews.php b/tests/Version4/ProductReviews.php new file mode 100644 index 00000000000..f0548796fb1 --- /dev/null +++ b/tests/Version4/ProductReviews.php @@ -0,0 +1,400 @@ +[\d]+)', + '/wc/v4/products/reviews/batch', + ]; + + /** + * The endpoint schema. + * + * @var array Keys are property names, values are supported context. + */ + protected $properties = [ + 'id' => array( 'view', 'edit' ), + 'date_created' => array( 'view', 'edit' ), + 'date_created_gmt' => array( 'view', 'edit' ), + 'product_id' => array( 'view', 'edit' ), + 'status' => array( 'view', 'edit' ), + 'reviewer' => array( 'view', 'edit' ), + 'reviewer_email' => array( 'view', 'edit' ), + 'review' => array( 'view', 'edit' ), + 'rating' => array( 'view', 'edit' ), + 'verified' => array( 'view', 'edit' ), + 'reviewer_avatar_urls' => array( 'view', 'edit' ), + ]; + + /** + * Test creation using this method. + * If read-only, test to confirm this. + */ + public function test_create() { + $product = \WC_Helper_Product::create_simple_product(); + $data = [ + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + 'rating' => '5', + 'product_id' => $product->get_id(), + ]; + $response = $this->do_request( '/wc/v4/products/reviews', 'POST', $data ); + $this->assertExpectedResponse( $response, 201, $data ); + $this->assertEquals( + array( + 'id' => $response->data['id'], + 'date_created' => $response->data['date_created'], + 'date_created_gmt' => $response->data['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => 'Hello world.', + 'rating' => 5, + 'verified' => false, + 'reviewer_avatar_urls' => $response->data['reviewer_avatar_urls'], + ), + $response->data + ); + } + + /** + * Test get/read using this method. + */ + public function test_read() { + $product = \WC_Helper_Product::create_simple_product(); + for ( $i = 0; $i < 10; $i++ ) { + $review_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + } + + // Invalid. + $response = $this->do_request( '/wc/v4/products/0/reviews' ); + $this->assertExpectedResponse( $response, 404 ); + + // Collections. + $response = $this->do_request( '/wc/v4/products/reviews' ); + $product_reviews = $response->data; + + $this->assertExpectedResponse( $response, 200 ); + $this->assertEquals( 10, count( $product_reviews ) ); + $this->assertContains( + array( + 'id' => $review_id, + 'date_created' => $product_reviews[0]['date_created'], + 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

Review content here

\n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v4/products/reviews/' . $review_id ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v4/products/reviews' ), + ), + ), + 'up' => array( + array( + 'embeddable' => true, + 'href' => rest_url( '/wc/v4/products/' . $product->get_id() ), + ), + ), + ), + ), + $product_reviews + ); + } + + /** + * Test updates using this method. + * If read-only, test to confirm this. + */ + public function test_update() { + $product = \WC_Helper_Product::create_simple_product(); + $product_review_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + + $response = $this->do_request( '/wc/v4/products/reviews/' . $product_review_id ); + $this->assertEquals( 200, $response->status ); + $this->assertEquals( "

Review content here

\n", $response->data['review'] ); + $this->assertEquals( 'admin', $response->data['reviewer'] ); + $this->assertEquals( 'woo@woo.local', $response->data['reviewer_email'] ); + $this->assertEquals( 0, $response->data['rating'] ); + + $data = [ + 'review' => 'Hello world - updated.', + 'reviewer' => 'Justin', + 'reviewer_email' => 'woo2@woo.local', + 'rating' => 3, + ]; + $response = $this->do_request( '/wc/v4/products/reviews/' . $product_review_id, 'PUT', $data ); + + $this->assertExpectedResponse( $response, 200, $data ); + + foreach ( $this->get_properties( 'view' ) as $property ) { + $this->assertArrayHasKey( $property, $response->data ); + } + } + + /** + * Test delete using this method. + * If read-only, test to confirm this. + */ + public function test_delete() { + // Invalid. + $result = $this->do_request( '/wc/v4/products/reviews/0', 'DELETE', [ 'force' => true ] ); + $this->assertEquals( 404, $result->status ); + + // Valid. + $product = \WC_Helper_Product::create_simple_product(); + $product_review_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + $result = $this->do_request( '/wc/v4/products/reviews/' . $product_review_id, 'DELETE', [ 'force' => true ] ); + $this->assertEquals( 200, $result->status ); + } + + /** + * Test get/read using this method. + */ + public function test_guest_read() { + wp_set_current_user( 0 ); + $result = $this->do_request( '/wc/v4/products/reviews' ); + $this->assertEquals( 401, $result->status ); + } + + /** + * Tests getting a single product review. + * + * @since 3.5.0 + */ + public function test_get_product_review() { + $product = \WC_Helper_Product::create_simple_product(); + $product_review_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + + $response = $this->server->dispatch( new \WP_REST_Request( 'GET', '/wc/v4/products/reviews/' . $product_review_id ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $product_review_id, + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

Review content here

\n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $data['reviewer_avatar_urls'], + ), + $data + ); + } + + /** + * Tests getting a product review with an invalid ID. + * + * @since 3.5.0 + */ + public function test_get_product_review_invalid_id() { + $product = \WC_Helper_Product::create_simple_product(); + $response = $this->server->dispatch( new \WP_REST_Request( 'GET', '/wc/v4/products/reviews/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests creating a product review without required fields. + * + * @since 3.5.0 + */ + public function test_create_product_review_invalid_fields() { + $product = \WC_Helper_Product::create_simple_product(); + + // missing review + $request = new \WP_REST_Request( 'POST', '/wc/v4/products/reviews' ); + $request->set_body_params( + array( + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + + // Missing reviewer. + $request = new \WP_REST_Request( 'POST', '/wc/v4/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer_email' => 'woo@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + + // missing reviewer_email + $request = new \WP_REST_Request( 'POST', '/wc/v4/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests updating a product review without the correct permissions. + * + * @since 3.5.0 + */ + public function test_update_product_review_without_permission() { + wp_set_current_user( 0 ); + $product = \WC_Helper_Product::create_simple_product(); + $product_review_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + + $request = new \WP_REST_Request( 'PUT', '/wc/v4/products/reviews/' . $product_review_id ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.dev', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests that updating a product review with an invalid id fails. + * + * @since 3.5.0 + */ + public function test_update_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \WC_Helper_Product::create_simple_product(); + + $request = new \WP_REST_Request( 'PUT', '/wc/v4/products/reviews/0' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.dev', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test deleting a product review without permission/creds. + * + * @since 3.5.0 + */ + public function test_delete_product_without_permission() { + wp_set_current_user( 0 ); + $product = \WC_Helper_Product::create_simple_product(); + $product_review_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + + $request = new \WP_REST_Request( 'DELETE', '/wc/v4/products/reviews/' . $product_review_id ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch managing product reviews. + * + * @since 3.5.0 + */ + public function test_product_reviews_batch() { + wp_set_current_user( $this->user ); + $product = \WC_Helper_Product::create_simple_product(); + + $review_1_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + $review_2_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + $review_3_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + $review_4_id = \WC_Helper_Product::create_product_review( $product->get_id() ); + + $request = new \WP_REST_Request( 'POST', '/wc/v4/products/reviews/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $review_1_id, + 'review' => 'Updated review.', + ), + ), + 'delete' => array( + $review_2_id, + $review_3_id, + ), + 'create' => array( + array( + 'review' => 'New review.', + 'reviewer' => 'Justin', + 'reviewer_email' => 'woo3@woo.local', + 'product_id' => $product->get_id(), + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'Updated review.', $data['update'][0]['review'] ); + $this->assertEquals( 'New review.', $data['create'][0]['review'] ); + $this->assertEquals( $review_2_id, $data['delete'][0]['previous']['id'] ); + $this->assertEquals( $review_3_id, $data['delete'][1]['previous']['id'] ); + + $request = new \WP_REST_Request( 'GET', '/wc/v4/products/reviews' ); + $request->set_param( 'product', $product->get_id() ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } +} + diff --git a/tests/Version4/product-reviews.php b/tests/Version4/product-reviews.php deleted file mode 100644 index 41571b8aaef..00000000000 --- a/tests/Version4/product-reviews.php +++ /dev/null @@ -1,50 +0,0 @@ -user = $this->factory->user->create( - array( - 'role' => 'administrator', - ) - ); - } - - /** - * Test product reviews shows product field as embeddable. - */ - public function test_product_review_embed() { - wp_set_current_user( $this->user ); - $product = WC_Helper_Product::create_simple_product(); - WC_Helper_Product::create_product_review( $product->get_id() ); - - $request = new WP_REST_Request( 'GET', '/wc/v4/products/reviews' ); - - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $this->assertTrue( $data[0]['_links']['up'][0]['embeddable'] ); - - $product->delete( true ); - } -} diff --git a/tests/Version4/products.php b/tests/Version4/products.php index 3ff0a150c82..9d0d30f56d0 100644 --- a/tests/Version4/products.php +++ b/tests/Version4/products.php @@ -1,33 +1,816 @@ [\d]+)', + '/wc/v4/products/batch', + ]; /** - * Setup test data. Called before every test. + * Test creation using this method. + * If read-only, test to confirm this. */ - public function setUp() { - parent::setUp(); + public function test_create() { - $this->user = $this->factory->user->create( + } + + /** + * Test get/read using this method. + */ + public function test_read() { + + } + + /** + * Test updates using this method. + * If read-only, test to confirm this. + */ + public function test_update() { + + } + + /** + * Test delete using this method. + * If read-only, test to confirm this. + */ + public function test_delete() { + + } + + /** + * Test getting products. + * + * @since 3.5.0 + */ + public function test_get_products() { + wp_set_current_user( $this->user ); + WC_Helper_Product::create_external_product(); + sleep( 1 ); // So both products have different timestamps. + WC_Helper_Product::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products' ) ); + $products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 2, count( $products ) ); + $this->assertEquals( 'Dummy Product', $products[0]['name'] ); + $this->assertEquals( 'DUMMY SKU', $products[0]['sku'] ); + $this->assertEquals( 'Dummy External Product', $products[1]['name'] ); + $this->assertEquals( 'DUMMY EXTERNAL SKU', $products[1]['sku'] ); + } + + /** + * Test getting products without permission. + * + * @since 3.5.0 + */ + public function test_get_products_without_permission() { + wp_set_current_user( 0 ); + WC_Helper_Product::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single product. + * + * @since 3.5.0 + */ + public function test_get_product() { + wp_set_current_user( $this->user ); + $simple = WC_Helper_Product::create_external_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $simple->get_id() ) ); + $product = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( array( - 'role' => 'administrator', + 'id' => $simple->get_id(), + 'name' => 'Dummy External Product', + 'type' => 'simple', + 'status' => 'publish', + 'sku' => 'DUMMY EXTERNAL SKU', + 'regular_price' => 10, + ), + $product + ); + } + + /** + * Test getting single product without permission. + * + * @since 3.5.0 + */ + public function test_get_product_without_permission() { + wp_set_current_user( 0 ); + $product = WC_Helper_Product::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single product. + * + * @since 3.5.0 + */ + public function test_delete_product() { + wp_set_current_user( $this->user ); + $product = WC_Helper_Product::create_simple_product(); + + $request = new WP_REST_Request( 'DELETE', '/wc/v4/products/' . $product->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products' ) ); + $variations = $response->get_data(); + $this->assertEquals( 0, count( $variations ) ); + } + + /** + * Test deleting a single product without permission. + * + * @since 3.5.0 + */ + public function test_delete_product_without_permission() { + wp_set_current_user( 0 ); + $product = WC_Helper_Product::create_simple_product(); + $request = new WP_REST_Request( 'DELETE', '/wc/v4/products/' . $product->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single product with an invalid ID. + * + * @since 3.5.0 + */ + public function test_delete_product_with_invalid_id() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'DELETE', '/wc/v4/products/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test editing a single product. Tests multiple product types. + * + * @since 3.5.0 + */ + public function test_update_product() { + wp_set_current_user( $this->user ); + + // test simple products. + $product = WC_Helper_Product::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) ); + $data = $response->get_data(); + $date_created = date( 'Y-m-d\TH:i:s', current_time( 'timestamp' ) ); + + $this->assertEquals( 'DUMMY SKU', $data['sku'] ); + $this->assertEquals( 10, $data['regular_price'] ); + $this->assertEmpty( $data['sale_price'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v4/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU', + 'sale_price' => '8', + 'description' => 'Testing', + 'date_created' => $date_created, + 'images' => array( + array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + ), ) ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Testing', $data['description'] ); + $this->assertEquals( '8', $data['price'] ); + $this->assertEquals( '8', $data['sale_price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertEquals( 'FIXED-SKU', $data['sku'] ); + $this->assertEquals( $date_created, $data['date_created'] ); + $this->assertContains( 'Dr1Bczxq4q', $data['images'][0]['src'] ); + $this->assertContains( 'test upload image', $data['images'][0]['alt'] ); + $product->delete( true ); + + // test variable product (variations are tested in product-variations.php). + $product = WC_Helper_Product::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + foreach ( array( 'small', 'large' ) as $term_name ) { + $this->assertContains( $term_name, $data['attributes'][0]['options'] ); + } + + $request = new WP_REST_Request( 'PUT', '/wc/v4/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'attributes' => array( + array( + 'id' => 0, + 'name' => 'pa_color', + 'options' => array( + 'red', + 'yellow', + ), + 'visible' => false, + 'variation' => 1, + ), + array( + 'id' => 0, + 'name' => 'pa_size', + 'options' => array( + 'small', + ), + 'visible' => false, + 'variation' => 1, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( array( 'small' ), $data['attributes'][0]['options'] ); + + foreach ( array( 'red', 'yellow' ) as $term_name ) { + $this->assertContains( $term_name, $data['attributes'][1]['options'] ); + } + + $product->delete( true ); + + // test external product. + $product = WC_Helper_Product::create_external_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 'Buy external product', $data['button_text'] ); + $this->assertEquals( 'http://woocommerce.com', $data['external_url'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v4/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'button_text' => 'Test API Update', + 'external_url' => 'http://automattic.com', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'Test API Update', $data['button_text'] ); + $this->assertEquals( 'http://automattic.com', $data['external_url'] ); + } + + /** + * Test updating a single product without permission. + * + * @since 3.5.0 + */ + public function test_update_product_without_permission() { + wp_set_current_user( 0 ); + $product = WC_Helper_Product::create_simple_product(); + $request = new WP_REST_Request( 'PUT', '/wc/v4/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single product with an invalid ID. + * + * @since 3.5.0 + */ + public function test_update_product_with_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/0' ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-INVALID-ID', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single product. + * + * @since 3.5.0 + */ + public function test_create_product() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v4/products/shipping_classes' ); + $request->set_body_params( + array( + 'name' => 'Test', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $shipping_class_id = $data['id']; + + // Create simple. + $request = new WP_REST_Request( 'POST', '/wc/v4/products' ); + $request->set_body_params( + array( + 'type' => 'simple', + 'name' => 'Test Simple Product', + 'sku' => 'DUMMY SKU SIMPLE API', + 'regular_price' => '10', + 'shipping_class' => 'test', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10', $data['price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertTrue( $data['purchasable'] ); + $this->assertEquals( 'DUMMY SKU SIMPLE API', $data['sku'] ); + $this->assertEquals( 'Test Simple Product', $data['name'] ); + $this->assertEquals( 'simple', $data['type'] ); + $this->assertEquals( $shipping_class_id, $data['shipping_class_id'] ); + + // Create external. + $request = new WP_REST_Request( 'POST', '/wc/v4/products' ); + $request->set_body_params( + array( + 'type' => 'external', + 'name' => 'Test External Product', + 'sku' => 'DUMMY SKU EXTERNAL API', + 'regular_price' => '10', + 'button_text' => 'Test Button', + 'external_url' => 'https://wordpress.org', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10', $data['price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertFalse( $data['purchasable'] ); + $this->assertEquals( 'DUMMY SKU EXTERNAL API', $data['sku'] ); + $this->assertEquals( 'Test External Product', $data['name'] ); + $this->assertEquals( 'external', $data['type'] ); + $this->assertEquals( 'Test Button', $data['button_text'] ); + $this->assertEquals( 'https://wordpress.org', $data['external_url'] ); + + // Create variable. + $request = new WP_REST_Request( 'POST', '/wc/v4/products' ); + $request->set_body_params( + array( + 'type' => 'variable', + 'name' => 'Test Variable Product', + 'sku' => 'DUMMY SKU VARIABLE API', + 'attributes' => array( + array( + 'id' => 0, + 'name' => 'pa_size', + 'options' => array( + 'small', + 'medium', + ), + 'visible' => false, + 'variation' => 1, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'DUMMY SKU VARIABLE API', $data['sku'] ); + $this->assertEquals( 'Test Variable Product', $data['name'] ); + $this->assertEquals( 'variable', $data['type'] ); + $this->assertEquals( array( 'small', 'medium' ), $data['attributes'][0]['options'] ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products' ) ); + $products = $response->get_data(); + $this->assertEquals( 3, count( $products ) ); + } + + /** + * Test creating a single product without permission. + * + * @since 3.5.0 + */ + public function test_create_product_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v4/products' ); + $request->set_body_params( + array( + 'name' => 'Test Product', + 'regular_price' => '12', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch managing products. + * + * @since 3.5.0 + */ + public function test_products_batch() { + wp_set_current_user( $this->user ); + $product = WC_Helper_Product::create_simple_product(); + $product_2 = WC_Helper_Product::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v4/products/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $product->get_id(), + 'description' => 'Updated description.', + ), + ), + 'delete' => array( + $product_2->get_id(), + ), + 'create' => array( + array( + 'sku' => 'DUMMY SKU BATCH TEST 1', + 'regular_price' => '10', + 'name' => 'Test Batch Create 1', + 'type' => 'external', + 'button_text' => 'Test Button', + ), + array( + 'sku' => 'DUMMY SKU BATCH TEST 2', + 'regular_price' => '20', + 'name' => 'Test Batch Create 2', + 'type' => 'simple', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Updated description.', $data['update'][0]['description'] ); + $this->assertEquals( 'DUMMY SKU BATCH TEST 1', $data['create'][0]['sku'] ); + $this->assertEquals( 'DUMMY SKU BATCH TEST 2', $data['create'][1]['sku'] ); + $this->assertEquals( 'Test Button', $data['create'][0]['button_text'] ); + $this->assertEquals( 'external', $data['create'][0]['type'] ); + $this->assertEquals( 'simple', $data['create'][1]['type'] ); + $this->assertEquals( $product_2->get_id(), $data['delete'][0]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v4/products' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Tests to make sure you can filter products post statuses by both + * the status query arg and WP_Query. + * + * @since 3.5.0 + */ + public function test_products_filter_post_status() { + wp_set_current_user( $this->user ); + for ( $i = 0; $i < 8; $i++ ) { + $product = WC_Helper_Product::create_simple_product(); + if ( 0 === $i % 2 ) { + wp_update_post( + array( + 'ID' => $product->get_id(), + 'post_status' => 'draft', + ) + ); + } + } + + // Test filtering with status=publish. + $request = new WP_REST_Request( 'GET', '/wc/v4/products' ); + $request->set_param( 'status', 'publish' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 4, count( $products ) ); + foreach ( $products as $product ) { + $this->assertEquals( 'publish', $product['status'] ); + } + + // Test filtering with status=draft. + $request = new WP_REST_Request( 'GET', '/wc/v4/products' ); + $request->set_param( 'status', 'draft' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 4, count( $products ) ); + foreach ( $products as $product ) { + $this->assertEquals( 'draft', $product['status'] ); + } + + // Test filtering with no filters - which should return 'any' (all 8). + $request = new WP_REST_Request( 'GET', '/wc/v4/products' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 8, count( $products ) ); + } + + /** + * Test product category. + * + * @since 3.5.0 + */ + public function test_get_products_by_category() { + wp_set_current_user( $this->user ); + + // Create one product with a category. + $category = wp_insert_term( 'Some Category', 'product_cat' ); + + $product = new WC_Product_Simple(); + $product->set_category_ids( array( $category['term_id'] ) ); + $product->save(); + + // Create one product without category, i.e. Uncategorized. + $product_2 = new WC_Product_Simple(); + $product_2->save(); + + // Test product assigned to a single category. + $query_params = array( + 'category' => (string) $category['term_id'], + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $product->get_id(), $response_product['id'] ); + $this->assertEquals( $product->get_category_ids(), wp_list_pluck( $response_product['categories'], 'id' ) ); + } + + // Test product without categories. + $request = new WP_REST_Request( 'GET', '/wc/v2/products/' . $product_2->get_id() ); + $response = $this->server->dispatch( $request ); + $response_product = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $response_product['categories'], print_r( $response_product, true ) ); + $this->assertEquals( 'uncategorized', $response_product['categories'][0]['slug'] ); + + } + + /** + * Test getting products by product type. + * + * @since 3.5.0 + */ + public function test_get_products_by_type() { + wp_set_current_user( $this->user ); + + $simple = WC_Helper_Product::create_simple_product(); + $external = WC_Helper_Product::create_external_product(); + $grouped = WC_Helper_Product::create_grouped_product(); + $variable = WC_Helper_Product::create_variation_product(); + + $product_ids_for_type = array( + 'simple' => array( $simple->get_id() ), + 'external' => array( $external->get_id() ), + 'grouped' => array( $grouped->get_id() ), + 'variable' => array( $variable->get_id() ), + ); + + foreach ( $grouped->get_children() as $additional_product ) { + $product_ids_for_type['simple'][] = $additional_product; + } + + foreach ( $product_ids_for_type as $product_type => $product_ids ) { + $query_params = array( + 'type' => $product_type, + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $product_ids ), count( $response_products ) ); + foreach ( $response_products as $response_product ) { + $this->assertContains( $response_product['id'], $product_ids_for_type[ $product_type ], 'REST API: ' . $product_type . ' not found correctly' ); + } + } + } + + /** + * Test getting products by featured property. + * + * @since 3.5.0 + */ + public function test_get_featured_products() { + wp_set_current_user( $this->user ); + + // Create a featured product. + $feat_product = WC_Helper_Product::create_simple_product(); + $feat_product->set_featured( true ); + $feat_product->save(); + + // Create a non-featured product. + $nonfeat_product = WC_Helper_Product::create_simple_product(); + $nonfeat_product->save(); + + $query_params = array( + 'featured' => 'true', + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $feat_product->get_id(), $response_product['id'], 'REST API: Featured product not found correctly' ); + } + + $query_params = array( + 'featured' => 'false', + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $nonfeat_product->get_id(), $response_product['id'], 'REST API: Featured product not found correctly' ); + } + } + + /** + * Test getting products by shipping class property. + * + * @since 3.5.0 + */ + public function test_get_products_by_shipping_class() { + wp_set_current_user( $this->user ); + + $shipping_class_1 = wp_insert_term( 'Bulky', 'product_shipping_class' ); + + $product_1 = new WC_Product_Simple(); + $product_1->set_shipping_class_id( $shipping_class_1['term_id'] ); + $product_1->save(); + + $query_params = array( + 'shipping_class' => (string) $shipping_class_1['term_id'], + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $product_1->get_id(), $response_product['id'] ); + } + } + + /** + * Test getting products by tag. + * + * @since 3.5.0 + */ + public function test_get_products_by_tag() { + wp_set_current_user( $this->user ); + + $test_tag_1 = wp_insert_term( 'Tag 1', 'product_tag' ); + + // Product with a tag. + $product = WC_Helper_Product::create_simple_product(); + $product->set_tag_ids( array( $test_tag_1['term_id'] ) ); + $product->save(); + + // Product without a tag. + $product_2 = WC_Helper_Product::create_simple_product(); + + $query_params = array( + 'tag' => (string) $test_tag_1['term_id'], + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $product->get_id(), $response_product['id'] ); + } + } + + /** + * Test getting products by global attribute. + * + * @since 3.5.0 + */ + public function test_get_products_by_attribute() { + global $wpdb; + wp_set_current_user( $this->user ); + + // Variable product with 2 different variations. + $variable_product = WC_Helper_Product::create_variation_product(); + + // Terms created by variable product. + $term_large = get_term_by( 'slug', 'large', 'pa_size' ); + $term_small = get_term_by( 'slug', 'small', 'pa_size' ); + + // Simple product without attribute. + $product_1 = WC_Helper_Product::create_simple_product(); + + // Simple product with attribute size = large. + $product_2 = WC_Helper_Product::create_simple_product(); + $product_2->set_attributes( array( 'pa_size' => 'large' ) ); + $product_2->save(); + + // Link the product to the term. + $wpdb->insert( + $wpdb->prefix . 'term_relationships', + array( + 'object_id' => $product_2->get_id(), + 'term_taxonomy_id' => $term_large->term_id, + 'term_order' => 0, + ) + ); + + // Products with attribute size == large. + $expected_product_ids = array( + $variable_product->get_id(), + $product_2->get_id(), + ); + $query_params = array( + 'attribute' => 'pa_size', + 'attribute_term' => (string) $term_large->term_id, + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $expected_product_ids ), count( $response_products ) ); + foreach ( $response_products as $response_product ) { + $this->assertContains( $response_product['id'], $expected_product_ids ); + } + + // Products with attribute size == small. + $expected_product_ids = array( + $variable_product->get_id(), + ); + $query_params = array( + 'attribute' => 'pa_size', + 'attribute_term' => (string) $term_small->term_id, + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $expected_product_ids ), count( $response_products ) ); + foreach ( $response_products as $response_product ) { + $this->assertContains( $response_product['id'], $expected_product_ids ); + } } /**