Skip to content

Add the built-in WP REST API validation to CountryCodeTrait to avoid errors when clearing all audience countries in the onboarding flow #2003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 30, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public function get_collection_params(): array {
'type' => 'string',
],
'required' => true,
'minItems' => 1,
],
];
}
Expand Down
1 change: 1 addition & 0 deletions src/API/Site/Controllers/Ads/CampaignController.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ protected function get_schema_properties(): array {
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_supported_country_code_validate_callback(),
'required' => true,
'minItems' => 1,
'items' => [
'type' => 'string',
],
Expand Down
36 changes: 22 additions & 14 deletions src/API/Site/Controllers/CountryCodeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelperAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\HelperTraits\ISO3166Awareness;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\League\ISO3166\Exception\OutOfBoundsException;
use WP_REST_Request as Request;
use Exception;
use Throwable;

Expand Down Expand Up @@ -35,24 +36,29 @@ protected function validate_country_code( string $country ): void {
}

/**
* Validate that a country or a list of countries is valid and supported.
* Validate that a country or a list of countries is valid and supported,
* and also validate the data by the built-in validation of WP REST API with parameter’s schema.
*
* @param mixed $countries An individual string or an array of strings.
* @param bool $check_supported_country Whether to check the country is supported.
* @param bool $check_supported_country Whether to check the country is supported.
* @param mixed $countries An individual string or an array of strings.
* @param Request $request The request to validate.
* @param string $param The parameter name, used in error messages.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can add somewhere a short explanation of your findings of why rest_validate_request_arg is not being called automatically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 4eeffe8.

*
* @return mixed
* @throws Exception When the country is not supported.
* @throws OutOfBoundsException When the country code cannot be found.
*/
protected function validate_country_codes( $countries, bool $check_supported_country ) {
protected function validate_country_codes( bool $check_supported_country, $countries, $request, $param ) {
$valid = rest_validate_request_arg( $countries, $request, $param );

if ( true !== $valid ) {
return $valid;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest renaming this as $validation_result, $validation or something like that. Since it can return a WP_Error object, hence is not "valid".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in bdfb365.


try {
// This is used for individual strings and an array of strings.
$countries = (array) $countries;

if ( empty( $countries ) ) {
throw new Exception( __( 'No countries provided.', 'google-listings-and-ads' ) );
}

foreach ( $countries as $country ) {
$this->validate_country_code( $country );
if ( $check_supported_country ) {
Expand Down Expand Up @@ -91,24 +97,26 @@ protected function get_country_code_sanitize_callback(): callable {
}

/**
* Get a callable function for validating that a provided country code is recognized.
* Get a callable function for validating that a provided country code is recognized
* and fulfilled the given parameter's schema.
*
* @return callable
*/
protected function get_country_code_validate_callback(): callable {
return function( $value ) {
return $this->validate_country_codes( $value, false );
return function( ...$args ) {
return $this->validate_country_codes( false, ...$args );
};
}

/**
* Get a callable function for validating that a provided country code is recognized and supported.
* Get a callable function for validating that a provided country code is recognized, supported,
* and fulfilled the given parameter's schema..
*
* @return callable
*/
protected function get_supported_country_code_validate_callback(): callable {
return function( $value ) {
return $this->validate_country_codes( $value, true );
return function( ...$args ) {
return $this->validate_country_codes( true, ...$args );
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ public function test_get_budget_recommendation_without_query_parameters() {
$this->assertEquals( 400, $response->get_status() );
}

/**
* Test a failed query of budget recommendation with empty country codes.
*/
public function test_get_budget_recommendation_with_empty_country_codes() {
$budget_recommendation_params = [
'country_codes' => [],
];

$response = $this->do_request( self::ROUTE_BUDGET_RECOMMENDATION, 'GET', $budget_recommendation_params );

$this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] );
$this->assertEquals( 'Invalid parameter(s): country_codes', $response->get_data()['message'] );
$this->assertEquals( 400, $response->get_status() );
}

public function test_get_budget_recommendation_with_nonexistent_country_code() {
$budget_recommendation_params = [
'country_codes' => [ 'AAAAA' ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,6 @@ public function test_create_campaign_empty_targeted_locations() {
'targeted_locations' => [],
];

$expected = [
'id' => self::TEST_CAMPAIGN_ID,
'status' => 'enabled',
'country' => self::BASE_COUNTRY,
] + $campaign_data;

$response = $this->do_request( self::ROUTE_CAMPAIGNS, 'POST', $campaign_data );

$this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe We can add some tests for:

  • Countries with wrong format return Error
  • Countries not supported can be added without errors.


namespace Automattic\WooCommerce\GoogleListingsAndAds\Tests\Unit\API\Site\Controllers\MerchantCenter;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter\ShippingTimeBatchController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\RESTControllerUnitTest;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\League\Container\Container;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\League\ISO3166\ISO3166DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use WP_REST_Response as Response;

/**
* Class ShippingTimeBatchControllerTest
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Tests\Unit\API\Site\Controllers\MerchantCenter
*/
class ShippingTimeBatchControllerTest extends RESTControllerUnitTest {

/** @var MockObject|Container $container */
protected $container;

/** @var MockObject|ISO3166DataProvider $iso_provider */
protected $iso_provider;

/** @var ShippingTimeBatchController $controller */
protected $controller;

protected const ROUTE_SHIPPING_TIME_BATCH = '/wc/gla/mc/shipping/times/batch';

/**
* Runs before each test is executed.
*/
public function setUp(): void {
parent::setUp();

$this->container = $this->createMock( Container::class );
$this->iso_provider = $this->createMock( ISO3166DataProvider::class );

$this->container->method( 'get' )->willReturn( $this->server );

$this->server->register_route(
'/wc/gla',
'/mc/shipping/times',
[
'methods' => TransportMethods::CREATABLE,
'callback' => function( $request ) {
return new Response( $request->get_param( 'country_code' ), 201 );
},
]
);

$this->controller = new ShippingTimeBatchController( $this->container );

$this->controller->set_iso3166_provider( $this->iso_provider );
$this->controller->register();
}

/**
* Test a successful dispatch request for the batch creation of shipping times.
*/
public function test_create_shipping_time_batch() {
$payload = [
'country_codes' => [ 'US', 'GB' ],
'time' => 5,
];

$response = $this->do_request( self::ROUTE_SHIPPING_TIME_BATCH, 'POST', $payload );

$expected = [
'errors' => [],
'success' => $payload['country_codes'],
];

$this->assertEquals( $expected, $response->get_data() );
$this->assertEquals( 201, $response->get_status() );
}

/**
* Test a failed batch creation of shipping times with empty country codes.
*/
public function test_create_shipping_time_batch_empty_country_codes() {
$payload = [
'country_codes' => [],
];

$response = $this->do_request( self::ROUTE_SHIPPING_TIME_BATCH, 'POST', $payload );

$this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] );
$this->assertEquals( 'Invalid parameter(s): country_codes', $response->get_data()['message'] );
$this->assertEquals( 400, $response->get_status() );
}

/**
* Test a failed batch creation of shipping times with duplicate country codes.
*/
public function test_create_shipping_time_batch_duplicate_country_codes() {
$payload = [
'country_codes' => [ 'US', 'GB', 'US' ],
];

$response = $this->do_request( self::ROUTE_SHIPPING_TIME_BATCH, 'POST', $payload );

$this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] );
$this->assertEquals( 'Invalid parameter(s): country_codes', $response->get_data()['message'] );
$this->assertEquals( 400, $response->get_status() );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
Copy link
Contributor

@puntope puntope Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


namespace Automattic\WooCommerce\GoogleListingsAndAds\Tests\Unit\API\Site\Controllers\MerchantCenter;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\MerchantCenter\TargetAudienceController;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Shipping\ShippingZone;
use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\RESTControllerUnitTest;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\League\ISO3166\ISO3166DataProvider;
use PHPUnit\Framework\MockObject\MockObject;

/**
* Class TargetAudienceControllerTest
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Tests\Unit\API\Site\Controllers\MerchantCenter
*/
class TargetAudienceControllerTest extends RESTControllerUnitTest {

/** @var WP $wp */
protected $wp;

/** @var WC $wc */
protected $wc;

/** @var MockObject|ShippingZone $shipping_zone */
protected $shipping_zone;

/** @var MockObject|ISO3166DataProvider $iso_provider */
protected $iso_provider;

/** @var MockObject|GoogleHelper $google_helper */
protected $google_helper;

/** @var MockObject|OptionsInterface $options */
protected $options;

/** @var TargetAudienceController $controller */
protected $controller;

protected const ROUTE_TARGET_AUDIENCE = '/wc/gla/mc/target_audience';

/**
* Runs before each test is executed.
*/
public function setUp(): void {
parent::setUp();

$this->wp = $this->createMock( WP::class );
$this->wc = $this->createMock( WC::class );
$this->shipping_zone = $this->createMock( ShippingZone::class );
$this->iso_provider = $this->createMock( ISO3166DataProvider::class );
$this->google_helper = $this->createMock( GoogleHelper::class );
$this->options = $this->createMock( OptionsInterface::class );

$this->controller = new TargetAudienceController( $this->server, $this->wp, $this->wc, $this->shipping_zone, $this->google_helper );

$this->controller->set_iso3166_provider( $this->iso_provider );
$this->controller->set_options_object( $this->options );
$this->controller->register();

$this->google_helper->method( 'is_country_supported' )->willReturn( true );
}

/**
* Test a successful update of target audience.
*/
public function test_update_target_audience() {
$payload = [
'location' => 'selected',
'countries' => [ 'US', 'GB' ],
];

$this->options->expects( $this->once() )
->method( 'update' )
->with( OptionsInterface::TARGET_AUDIENCE, $payload );

$response = $this->do_request( self::ROUTE_TARGET_AUDIENCE, 'POST', $payload );

$this->assertEquals( 'success', $response->get_data()['status'] );
$this->assertEquals( 201, $response->get_status() );
}

/**
* Test a successful update of target audience with empty country codes.
*/
public function test_update_target_audience_empty_countries() {
$payload = [
'location' => 'all',
'countries' => [],
];

$this->options->expects( $this->once() )
->method( 'update' )
->with( OptionsInterface::TARGET_AUDIENCE, $payload );

$response = $this->do_request( self::ROUTE_TARGET_AUDIENCE, 'POST', $payload );

$this->assertEquals( 'success', $response->get_data()['status'] );
$this->assertEquals( 201, $response->get_status() );
}
}