Add tests for product deletion in LookupDataStoreTest.

This commit is contained in:
Nestor Soriano 2021-06-17 15:31:23 +02:00
parent 0192ed0b93
commit 48c44a6128
No known key found for this signature in database
GPG Key ID: 08110F3518C12CAD
4 changed files with 437 additions and 11 deletions

View File

@ -9,6 +9,7 @@
*/ */
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore;
use Automattic\WooCommerce\Proxies\LegacyProxy;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
@ -298,18 +299,18 @@ class WC_Post_Data {
* @param mixed $id ID of post being deleted. * @param mixed $id ID of post being deleted.
*/ */
public static function delete_post( $id ) { public static function delete_post( $id ) {
if ( ! current_user_can( 'delete_posts' ) || ! $id ) { $container = wc_get_container();
if ( ! $container->get( LegacyProxy::class )->call_function( 'current_user_can', 'delete_posts' ) || ! $id ) {
return; return;
} }
$post_type = get_post_type( $id ); $post_type = self::get_post_type( $id );
switch ( $post_type ) { switch ( $post_type ) {
case 'product': case 'product':
$data_store = WC_Data_Store::load( 'product-variable' ); $data_store = WC_Data_Store::load( 'product-variable' );
$data_store->delete_variations( $id, true ); $data_store->delete_variations( $id, true );
$data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' );
wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); $container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id );
$parent_id = wp_get_post_parent_id( $id ); $parent_id = wp_get_post_parent_id( $id );
if ( $parent_id ) { if ( $parent_id ) {
@ -321,7 +322,7 @@ class WC_Post_Data {
$data_store = WC_Data_Store::load( 'product' ); $data_store = WC_Data_Store::load( 'product' );
$data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' );
wc_delete_product_transients( wp_get_post_parent_id( $id ) ); wc_delete_product_transients( wp_get_post_parent_id( $id ) );
wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); $container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id );
break; break;
case 'shop_order': case 'shop_order':
@ -348,7 +349,7 @@ class WC_Post_Data {
return; return;
} }
$post_type = get_post_type( $id ); $post_type = self::get_post_type( $id );
// If this is an order, trash any refunds too. // If this is an order, trash any refunds too.
if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) {
@ -382,7 +383,7 @@ class WC_Post_Data {
return; return;
} }
$post_type = get_post_type( $id ); $post_type = self::get_post_type( $id );
if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) {
global $wpdb; global $wpdb;
@ -407,6 +408,16 @@ class WC_Post_Data {
} }
} }
/**
* Get the post type for a given post.
*
* @param int $id The post id.
* @return string The post type.
*/
private static function get_post_type( $id ) {
return wc_get_container()->get( LegacyProxy::class )->call_function( 'get_post_type', $id );
}
/** /**
* Before deleting an order, do some cleanup. * Before deleting an order, do some cleanup.
* *

View File

@ -238,17 +238,20 @@ AND table_name = %s;',
} }
$product = WC()->call_function( 'wc_get_product', $product_id ); $product = WC()->call_function( 'wc_get_product', $product_id );
if ( ! $product ) {
$action = self::ACTION_DELETE;
}
switch ( $action ) { switch ( $action ) {
case self::ACTION_INSERT: case self::ACTION_INSERT:
$this->delete_data_for( $product->get_id() ); $this->delete_data_for( $product_id );
$this->create_data_for( $product ); $this->create_data_for( $product );
break; break;
case self::ACTION_UPDATE_STOCK: case self::ACTION_UPDATE_STOCK:
$this->update_stock_status_for( $product ); $this->update_stock_status_for( $product );
break; break;
case self::ACTION_DELETE: case self::ACTION_DELETE:
$this->delete_data_for( $product->get_id() ); $this->delete_data_for( $product_id );
break; break;
} }
} }

View File

@ -72,7 +72,13 @@ class FakeQueue implements \WC_Queue_Interface {
} }
public function search( $args = array(), $return_format = OBJECT ) { public function search( $args = array(), $return_format = OBJECT ) {
// TODO: Implement search() method. $result = array();
foreach ( $this->methods_called as $method_called ) {
if ( $method_called['args'] === $args['args'] && $method_called['hook'] === $args['hook'] ) {
$result[] = $method_called;
}
}
return $result;
} }
// phpcs:enable Squiz.Commenting.FunctionComment.Missing // phpcs:enable Squiz.Commenting.FunctionComment.Missing

View File

@ -9,6 +9,7 @@ use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Testing\Tools\FakeQueue; use Automattic\WooCommerce\Testing\Tools\FakeQueue;
use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack;
/** /**
* Tests for the LookupDataStore class. * Tests for the LookupDataStore class.
@ -23,12 +24,20 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
*/ */
private $sut; private $sut;
/**
* The lookup table name.
*
* @var string
*/
private $lookup_table_name;
/** /**
* Runs before each test. * Runs before each test.
*/ */
public function setUp() { public function setUp() {
global $wpdb; global $wpdb;
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
$this->sut = new LookupDataStore(); $this->sut = new LookupDataStore();
// Initiating regeneration with a fake queue will just create the lookup table in the database. // Initiating regeneration with a fake queue will just create the lookup table in the database.
@ -39,6 +48,11 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
} }
); );
$this->get_instance_of( DataRegenerator::class )->initiate_regeneration(); $this->get_instance_of( DataRegenerator::class )->initiate_regeneration();
$queue = WC()->get_instance_of( \WC_Queue::class );
$queue->methods_called = array();
$this->reset_legacy_proxy_mocks();
} }
/** /**
@ -331,6 +345,329 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
$this->assertEquals( sort( $expected ), sort( $actual ) ); $this->assertEquals( sort( $expected ), sort( $actual ) );
} }
/**
* @testdox Deleting a simple product schedules deletion of lookup table entries when the "direct updates" option is off.
*
* @testWith ["wp_trash_post"]
* ["delete_post"]
* ["delete_method_in_product"]
* ["force_delete_method_in_product"]
*
* @param string $deletion_mechanism The mechanism used for deletion, one of: 'wp_trash_post', 'delete_post', 'delete_method_in_product', 'force_delete_method_in_product'.
*/
public function test_deleting_simple_product_schedules_deletion( $deletion_mechanism ) {
$this->set_direct_update_option( false );
$product = new \WC_Product_Simple();
$product_id = 10;
$product->set_id( $product_id );
$this->save( $product );
$this->register_legacy_proxy_function_mocks(
array(
'get_post_type' => function( $id ) use ( $product ) {
if ( $id === $product->get_id() || $id === $product ) {
return 'product';
} else {
return get_post_type( $id );
}
},
'time' => function() {
return 100;
},
'current_user_can' => function( $capability, ...$args ) {
if ( 'delete_posts' === $capability ) {
return true;
} else {
return current_user_can( $capability, $args );
}
},
)
);
$this->delete_product( $product, $deletion_mechanism );
$queue_calls = WC()->get_instance_of( \WC_Queue::class )->methods_called;
$this->assertEquals( 1, count( $queue_calls ) );
$expected = array(
'method' => 'schedule_single',
'args' =>
array(
$product_id,
LookupDataStore::ACTION_DELETE,
),
'group' => 'woocommerce-db-updates',
'timestamp' => 101,
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
);
$this->assertEquals( $expected, $queue_calls[0] );
}
/**
* Delete a product or variation.
*
* @param \WC_Product $product The product to delete.
* @param string $deletion_mechanism The mechanism used for deletion, one of: 'wp_trash_post', 'delete_post', 'delete_method_in_product', 'force_delete_method_in_product'.
*/
private function delete_product( \WC_Product $product, string $deletion_mechanism ) {
// We can't use the 'wp_trash_post' and 'delete_post' functions directly
// because these invoke 'get_post', which fails because tests runs within an
// uncommitted database transaction. Being WP core functions they can't be mocked or hacked.
// So instead, we trigger the actions that the tested functionality captures.
switch ( $deletion_mechanism ) {
case 'wp_trash_post':
do_action( 'wp_trash_post', $product );
break;
case 'delete_post':
do_action( 'delete_post', $product->get_id() );
break;
case 'delete_method_in_product':
$product->delete( false );
break;
case 'force_delete_method_in_product':
$product->delete( true );
break;
}
}
/**
* @testdox Deleting a variable product schedules deletion of lookup table entries when the "direct updates" option is off.
*
* @testWith ["wp_trash_post"]
* ["delete_post"]
* ["delete_method_in_product"]
* ["force_delete_method_in_product"]
*
* @param string $deletion_mechanism The mechanism used for deletion, one of: 'wp_trash_post', 'delete_post', 'delete_method_in_product', 'force_delete_method_in_product'.
*/
public function test_deleting_variable_product_schedules_deletion( $deletion_mechanism ) {
$this->set_direct_update_option( false );
$product = new \WC_Product_Variable();
$product->set_id( 1000 );
$variation = new \WC_Product_Variation();
$variation->set_id( 1001 );
$product->set_children( array( 1001 ) );
$this->save( $product );
$product_id = $product->get_id();
$this->register_legacy_proxy_function_mocks(
array(
'get_post_type' => function( $id ) use ( $product, $variation ) {
if ( $id === $product->get_id() || $id === $product ) {
return 'product';
} elseif ( $id === $variation->get_id() || $id === $variation ) {
return 'product_variation';
} else {
return get_post_type( $id );
}
},
'time' => function() {
return 100;
},
'current_user_can' => function( $capability, ...$args ) {
if ( 'delete_posts' === $capability ) {
return true;
} else {
return current_user_can( $capability, $args );
}
},
)
);
$this->delete_product( $product, $deletion_mechanism );
$queue_calls = WC()->get_instance_of( \WC_Queue::class )->methods_called;
$this->assertEquals( 1, count( $queue_calls ) );
$expected = array(
'method' => 'schedule_single',
'args' =>
array(
$product_id,
LookupDataStore::ACTION_DELETE,
),
'group' => 'woocommerce-db-updates',
'timestamp' => 101,
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
);
$this->assertEquals( $expected, $queue_calls[0] );
}
/**
* @testdox Deleting a variation schedules deletion of lookup table entries when the "direct updates" option is off.
*
* @testWith ["wp_trash_post"]
* ["delete_post"]
* ["delete_method_in_product"]
* ["force_delete_method_in_product"]
*
* @param string $deletion_mechanism The mechanism used for deletion, one of: 'wp_trash_post', 'delete_post', 'delete_method_in_product', 'force_delete_method_in_product'.
*/
public function test_deleting_variation_schedules_deletion( $deletion_mechanism ) {
$this->set_direct_update_option( false );
$product = new \WC_Product_Variable();
$product->set_id( 1000 );
$variation = new \WC_Product_Variation();
$variation->set_id( 1001 );
$product->set_children( array( 1001 ) );
$this->save( $product );
$variation_id = $product->get_id();
$this->register_legacy_proxy_function_mocks(
array(
'get_post_type' => function( $id ) use ( $product, $variation ) {
if ( $id === $product->get_id() || $id === $product ) {
return 'product';
} elseif ( $id === $variation->get_id() || $id === $variation ) {
return 'product_variation';
} else {
return get_post_type( $id );
}
},
'time' => function() {
return 100;
},
'current_user_can' => function( $capability, ...$args ) {
if ( 'delete_posts' === $capability ) {
return true;
} else {
return current_user_can( $capability, $args );
}
},
)
);
$this->delete_product( $product, $deletion_mechanism );
$queue_calls = WC()->get_instance_of( \WC_Queue::class )->methods_called;
$this->assertEquals( 1, count( $queue_calls ) );
$expected = array(
'method' => 'schedule_single',
'args' =>
array(
$variation_id,
LookupDataStore::ACTION_DELETE,
),
'group' => 'woocommerce-db-updates',
'timestamp' => 101,
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
);
$this->assertEquals( $expected, $queue_calls[0] );
}
/**
* @testdox 'on_product_deleted' doesn't schedule duplicate deletions (for the same product).
*/
public function test_no_duplicate_deletions_are_scheduled() {
$this->set_direct_update_option( false );
$this->register_legacy_proxy_function_mocks(
array(
'time' => function() {
return 100;
},
)
);
$this->sut->on_product_deleted( 1 );
$this->sut->on_product_deleted( 1 );
$this->sut->on_product_deleted( 2 );
$queue_calls = WC()->get_instance_of( \WC_Queue::class )->methods_called;
$this->assertEquals( 2, count( $queue_calls ) );
$expected = array(
array(
'method' => 'schedule_single',
'args' =>
array(
1,
LookupDataStore::ACTION_DELETE,
),
'group' => 'woocommerce-db-updates',
'timestamp' => 101,
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
),
array(
'method' => 'schedule_single',
'args' =>
array(
2,
LookupDataStore::ACTION_DELETE,
),
'group' => 'woocommerce-db-updates',
'timestamp' => 101,
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
),
);
$this->assertEquals( $expected, $queue_calls );
}
/**
* @testdox 'on_product_deleted' deletes the data for a variation when the "direct updates" option is on.
*/
public function test_direct_deletion_of_variation() {
global $wpdb;
$this->set_direct_update_option( true );
$variation = new \WC_Product_Variation();
$variation->set_id( 2 );
$this->save( $variation );
$this->insert_lookup_table_data( 1, 1, 'pa_foo', 10, false, true );
$this->insert_lookup_table_data( 2, 1, 'pa_bar', 20, true, true );
$this->sut->on_product_deleted( $variation );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$rows = $wpdb->get_results( 'SELECT DISTINCT product_id FROM ' . $this->lookup_table_name, ARRAY_N );
$this->assertEquals( array( 1 ), $rows[0] );
}
/**
* @testdox 'on_product_deleted' deletes the data for a product and its variations when the "direct updates" option is on.
*/
public function test_direct_deletion_of_product() {
global $wpdb;
$this->set_direct_update_option( true );
$product = new \WC_Product();
$product->set_id( 1 );
$this->save( $product );
$this->insert_lookup_table_data( 1, 1, 'pa_foo', 10, false, true );
$this->insert_lookup_table_data( 2, 1, 'pa_bar', 20, true, true );
$this->insert_lookup_table_data( 3, 3, 'pa_foo', 10, false, true );
$this->sut->on_product_deleted( $product );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$rows = $wpdb->get_results( 'SELECT DISTINCT product_id FROM ' . $this->lookup_table_name, ARRAY_N );
$this->assertEquals( array( 3 ), $rows[0] );
}
/** /**
* Set the product attributes from an array with this format: * Set the product attributes from an array with this format:
* *
@ -380,4 +717,73 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
return $result; return $result;
} }
/**
* Set the value of the option for direct lookup table updates.
*
* @param bool $value True to set the option to 'yes', false for 'no'.
*/
private function set_direct_update_option( bool $value ) {
update_option( 'woocommerce_attribute_lookup__direct_updates', $value ? 'yes' : 'no' );
}
/**
* Save a product and delete any lookup table data that may have been automatically inserted
* (for the purposes of unit testing we want to insert this data manually)
*
* @param \WC_Product $product The product to save and delete lookup table data for.
*/
private function save( \WC_Product $product ) {
global $wpdb;
$product->save();
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}wc_product_attributes_lookup WHERE product_id = %d",
$product->get_id()
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
$queue = WC()->get_instance_of( \WC_Queue::class );
$queue->methods_called = array();
}
/**
* Insert one entry in the lookup table.
*
* @param int $product_id The product id.
* @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations.
* @param string $taxonomy Taxonomy name.
* @param int $term_id Term id.
* @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations.
* @param bool $has_stock True if the product is in stock.
*/
private function insert_lookup_table_data( int $product_id, int $product_or_parent_id, string $taxonomy, int $term_id, bool $is_variation_attribute, bool $has_stock ) {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
$wpdb->prepare(
'INSERT INTO ' . $this->lookup_table_name . ' (
product_id,
product_or_parent_id,
taxonomy,
term_id,
is_variation_attribute,
in_stock)
VALUES
( %d, %d, %s, %d, %d, %d )',
$product_id,
$product_or_parent_id,
$taxonomy,
$term_id,
$is_variation_attribute ? 1 : 0,
$has_stock ? 1 : 0
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
} }