Commit bb5ef36233 for woocommerce
commit bb5ef36233a109fd4bbffcc6bb7605880c6578bb
Author: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
Date: Sat Dec 27 15:46:45 2025 -0300
[Fraud Protection] Added classes to hook into cart actions (#62543)
* Add SessionClearanceManager for fraud protection
Implements session status management for the WooCommerce fraud protection
feature. This class tracks three session states (pending, allowed, blocked)
and provides the foundation for fraud detection decision enforcement.
Key features:
- Session status management (pending, allowed, blocked)
- Cart emptying on blocked sessions
- Logging via FraudProtectionController helper
- DEFAULT_STATUS constant for consistent default behavior
- Comprehensive unit tests (9 tests, 20 assertions)
Closes WOOSUBS-1246
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
* Address coderabbit comments
* fix lint error
* Address phpstan error
* Add SessionDataCollector foundation class and tests
Create SessionDataCollector class with basic structure to collect session data for fraud protection. This initial implementation establishes the class foundation with proper nested data structure, dependency injection, and comprehensive unit tests.
- Add SessionDataCollector class with collect() method
- Implement nested data structure with 9 top-level sections
- Add dependency injection for SessionClearanceManager
- Add UTC timestamp generation
- Create 6 focused unit tests covering foundation requirements
- Follow WooCommerce coding standards and patterns
Part of Task Group 1 for WOOSUBS-1248 (Session Data Collection Framework).
* Improve SessionDataCollector dependency injection and data collection
- Update to use init() pattern instead of constructor for dependency injection (follows WooCommerce standards)
- Add session data fallback for customer data collection (first_name, last_name, billing_email)
- Reorder email fallback chain to check WC_Customer before session data
- Add test for customer data session fallback
- Fix PHPCS issues: replace short ternaries, fix array/variable alignment, remove trailing whitespace
All tests passing (15 tests, 64 assertions).
* Implement order, cart, and address data collection (Task Group 3)
Add comprehensive order, cart item, and address data collection methods to SessionDataCollector with multiple improvements based on WooCommerce best practices.
Implementation:
- Add get_order_data() method collecting 11 order fields including totals, currency, and cart hash
- Add get_cart_items() method extracting 12 detailed fields per cart item
- Add get_billing_address() and get_shipping_address() methods with 6 fields each
- Add get_product_category_names() helper for cleaner category extraction
- Update collect() method to populate order and address sections
Key improvements:
- Use WC()->customer->get_id() for customer_id instead of user login check (proper WooCommerce abstraction)
- Use get_subtotal() for items_total (before discounts) instead of get_cart_contents_total() (after discounts)
- Use get_total('edit') for total instead of manual calculation (more reliable)
- Use wc_get_product_terms() for categories with caching (WooCommerce helper vs WordPress get_term)
- Return all product categories as comma-separated list (better fraud detection data)
- Extract category logic to reusable helper method (cleaner code)
Testing:
- Add 9 new focused tests for order, cart, and address data (24 total tests, 137 assertions)
- Fix cart state isolation between tests (add empty_cart() calls)
- Add customer reinitialization in logged-in user test
- All tests passing
Part of Task Group 3 for WOOSUBS-1248 (Session Data Collection Framework).
* Add payment data structure and complete SessionDataCollector (Task Group 4)
Complete SessionDataCollector implementation with payment data structure and comprehensive integration testing. All data collection methods now implemented with graceful degradation throughout.
Implementation:
- Add get_payment_data() method with 11 payment fields (gateway name/type populated, card details null for future gateway extensions)
- Add get_product_category_names() helper for cleaner category extraction
- Update collect() to populate payment section and extract order_id from event_data
- Update get_order_data() to accept order_id parameter from event_data
Integration testing:
- Add 6 comprehensive integration tests (35 total tests, 202 assertions)
- Test payment data structure with all 11 required fields
- Test complete collect() output with all 9 sections
- Test end-to-end data collection with full cart scenario
- Test graceful degradation across all sections (empty cart, no user, minimal data)
- Test manual triggering verification
- Fix graceful degradation test to properly initialize guest customer
Data collection improvements:
- Order ID now extracted from event_data when provided by calling code
- Category extraction uses wc_get_product_terms() with caching (WooCommerce helper)
- Returns all product categories as comma-separated list (better fraud detection data)
- Comprehensive PHPDoc comments throughout
All acceptance criteria met:
- Payment structure complete with gateway extension placeholders
- All 9 sections implemented and tested
- Graceful degradation verified end-to-end
- Manual triggering only (no automatic hooks in SessionDataCollector)
- All tests passing (35 tests, 202 assertions)
Part of Task Group 4 for WOOSUBS-1248 (Session Data Collection Framework).
* Added Cart Event Tracker class to capture cart events
* Added test for the CartEventTracker class
* Add SessionDataCollector foundation class and tests
Create SessionDataCollector class with basic structure to collect session data for fraud protection. This initial implementation establishes the class foundation with proper nested data structure, dependency injection, and comprehensive unit tests.
- Add SessionDataCollector class with collect() method
- Implement nested data structure with 9 top-level sections
- Add dependency injection for SessionClearanceManager
- Add UTC timestamp generation
- Create 6 focused unit tests covering foundation requirements
- Follow WooCommerce coding standards and patterns
Part of Task Group 1 for WOOSUBS-1248 (Session Data Collection Framework).
* Improve SessionDataCollector dependency injection and data collection
- Update to use init() pattern instead of constructor for dependency injection (follows WooCommerce standards)
- Add session data fallback for customer data collection (first_name, last_name, billing_email)
- Reorder email fallback chain to check WC_Customer before session data
- Add test for customer data session fallback
- Fix PHPCS issues: replace short ternaries, fix array/variable alignment, remove trailing whitespace
All tests passing (15 tests, 64 assertions).
* Implement order, cart, and address data collection (Task Group 3)
Add comprehensive order, cart item, and address data collection methods to SessionDataCollector with multiple improvements based on WooCommerce best practices.
Implementation:
- Add get_order_data() method collecting 11 order fields including totals, currency, and cart hash
- Add get_cart_items() method extracting 12 detailed fields per cart item
- Add get_billing_address() and get_shipping_address() methods with 6 fields each
- Add get_product_category_names() helper for cleaner category extraction
- Update collect() method to populate order and address sections
Key improvements:
- Use WC()->customer->get_id() for customer_id instead of user login check (proper WooCommerce abstraction)
- Use get_subtotal() for items_total (before discounts) instead of get_cart_contents_total() (after discounts)
- Use get_total('edit') for total instead of manual calculation (more reliable)
- Use wc_get_product_terms() for categories with caching (WooCommerce helper vs WordPress get_term)
- Return all product categories as comma-separated list (better fraud detection data)
- Extract category logic to reusable helper method (cleaner code)
Testing:
- Add 9 new focused tests for order, cart, and address data (24 total tests, 137 assertions)
- Fix cart state isolation between tests (add empty_cart() calls)
- Add customer reinitialization in logged-in user test
- All tests passing
Part of Task Group 3 for WOOSUBS-1248 (Session Data Collection Framework).
* Add payment data structure and complete SessionDataCollector (Task Group 4)
Complete SessionDataCollector implementation with payment data structure and comprehensive integration testing. All data collection methods now implemented with graceful degradation throughout.
Implementation:
- Add get_payment_data() method with 11 payment fields (gateway name/type populated, card details null for future gateway extensions)
- Add get_product_category_names() helper for cleaner category extraction
- Update collect() to populate payment section and extract order_id from event_data
- Update get_order_data() to accept order_id parameter from event_data
Integration testing:
- Add 6 comprehensive integration tests (35 total tests, 202 assertions)
- Test payment data structure with all 11 required fields
- Test complete collect() output with all 9 sections
- Test end-to-end data collection with full cart scenario
- Test graceful degradation across all sections (empty cart, no user, minimal data)
- Test manual triggering verification
- Fix graceful degradation test to properly initialize guest customer
Data collection improvements:
- Order ID now extracted from event_data when provided by calling code
- Category extraction uses wc_get_product_terms() with caching (WooCommerce helper)
- Returns all product categories as comma-separated list (better fraud detection data)
- Comprehensive PHPDoc comments throughout
All acceptance criteria met:
- Payment structure complete with gateway extension placeholders
- All 9 sections implemented and tested
- Graceful degradation verified end-to-end
- Manual triggering only (no automatic hooks in SessionDataCollector)
- All tests passing (35 tests, 202 assertions)
Part of Task Group 4 for WOOSUBS-1248 (Session Data Collection Framework).
* Remove todo comment in order to calm down the linter
* Use get_order_count on customer for optimized order count retrieval with caching support
* Fixed empty customer $lifetime_order_count
* Added comment about why we're reloading the customer
* Now we always create a unique session id
* Fixed session hash name
* Removed unused imports
* Removed unecessary dependency
* Updated isset check to the null coallese operator
* Casting $old_quantity to int
* Added check to prevent sending cart update events when the number of items didn't change
* Disabled taxes to prevent fraud tests from failing when checking the cart totals
* Lint fixes
* Added microtime to the sku so each product under test is unique
* Fixed lint errors
---------
Co-authored-by: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 5ba29aebc5..5cc81887ee 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -18,14 +18,12 @@ use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonRequestHandler;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
use Automattic\WooCommerce\Internal\MCP\MCPAdapterProvider;
use Automattic\WooCommerce\Internal\Abilities\AbilitiesRegistry;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as ProductDownloadDirectories;
use Automattic\WooCommerce\Internal\ProductImage\MatchImageBySKU;
-use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster;
use Automattic\WooCommerce\Internal\Settings\OptionSanitizer;
use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub;
@@ -33,7 +31,7 @@ use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Internal\Admin\EmailImprovements\EmailImprovements;
use Automattic\WooCommerce\Internal\Admin\Marketplace;
use Automattic\WooCommerce\Proxies\LegacyProxy;
-use Automattic\WooCommerce\Utilities\{LoggingUtil, RestApiUtil, TimeUtil};
+use Automattic\WooCommerce\Utilities\{LoggingUtil, TimeUtil};
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
use Automattic\WooCommerce\Caches\OrderCountCacheService;
use Automattic\WooCommerce\Internal\Caches\ProductVersionStringInvalidator;
@@ -377,6 +375,7 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\Orders\OrderAttributionBlocksController::class )->register();
$container->get( Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController::class )->register();
$container->get( Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController::class )->register();
+ $container->get( Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker::class )->register();
$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsController::class )->register();
$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsController::class )->register();
$container->get( Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub::class )->register();
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
new file mode 100644
index 0000000000..0c6d2ca286
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
@@ -0,0 +1,289 @@
+<?php
+/**
+ * CartEventTracker class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Tracks cart events for fraud protection analysis.
+ *
+ * This class hooks into WooCommerce cart events (add, update, remove, restore)
+ * and triggers comprehensive event tracking with full session context. It orchestrates
+ * the event tracking by collecting session data and preparing it for the fraud
+ * protection service.
+ *
+ * @since 10.5.0
+ * @internal This class is part of the internal API and is subject to change without notice.
+ */
+class CartEventTracker implements RegisterHooksInterface {
+
+ /**
+ * Session data collector instance.
+ *
+ * @var SessionDataCollector
+ */
+ private SessionDataCollector $data_collector;
+
+ /**
+ * Fraud protection controller instance.
+ *
+ * @var FraudProtectionController
+ */
+ private FraudProtectionController $fraud_protection_controller;
+
+ /**
+ * Initialize with dependencies.
+ *
+ * @internal
+ *
+ * @param SessionDataCollector $data_collector The session data collector instance.
+ * @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance.
+ */
+ final public function init(
+ SessionDataCollector $data_collector,
+ FraudProtectionController $fraud_protection_controller
+ ): void {
+ $this->data_collector = $data_collector;
+ $this->fraud_protection_controller = $fraud_protection_controller;
+ }
+
+ /**
+ * Register cart event hooks.
+ *
+ * Hooks into WooCommerce cart actions to track fraud protection events.
+ * Only registers hooks if the fraud protection feature is enabled.
+ *
+ * @return void
+ */
+ public function register(): void {
+ // Only register hooks if fraud protection is enabled.
+ if ( ! $this->fraud_protection_controller->feature_is_enabled() ) {
+ return;
+ }
+
+ add_action( 'woocommerce_add_to_cart', array( $this, 'handle_track_cart_item_added' ), 10, 6 );
+ add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'handle_track_cart_item_updated' ), 10, 4 );
+ add_action( 'woocommerce_remove_cart_item', array( $this, 'handle_track_cart_item_removed' ), 10, 2 );
+ add_action( 'woocommerce_restore_cart_item', array( $this, 'handle_track_cart_item_restored' ), 10, 2 );
+ }
+
+ /**
+ * Handle cart item added event.
+ *
+ * Triggers fraud protection event tracking when an item is added to the cart.
+ *
+ * @internal
+ *
+ * @param string $cart_item_key Cart item key.
+ * @param int $product_id Product ID.
+ * @param int $quantity Quantity added.
+ * @param int $variation_id Variation ID.
+ * @param array $variation Variation data.
+ * @param array $cart_item_data Cart item data.
+ * @return void
+ */
+ public function handle_track_cart_item_added( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ): void {
+ $event_data = $this->build_cart_event_data(
+ 'item_added',
+ $product_id,
+ $quantity,
+ $variation_id
+ );
+
+ $this->track_event( 'cart_item_added', $event_data );
+ }
+
+ /**
+ * Handle cart item quantity updated event.
+ *
+ * Triggers fraud protection event tracking when cart item quantity is updated.
+ *
+ * @internal
+ *
+ * @param string $cart_item_key Cart item key.
+ * @param int $quantity New quantity.
+ * @param int $old_quantity Old quantity.
+ * @param object $cart Cart object.
+ * @return void
+ */
+ public function handle_track_cart_item_updated( $cart_item_key, $quantity, $old_quantity, $cart ): void {
+ $cart_item = $cart->cart_contents[ $cart_item_key ] ?? null;
+
+ if ( (int) $quantity === (int) $old_quantity || ! $cart_item ) {
+ return;
+ }
+
+ $product_id = $cart_item['product_id'] ?? 0;
+ $variation_id = $cart_item['variation_id'] ?? 0;
+
+ $event_data = $this->build_cart_event_data(
+ 'item_updated',
+ $product_id,
+ (int) $quantity,
+ $variation_id
+ );
+
+ // Add old quantity for context.
+ $event_data['old_quantity'] = (int) $old_quantity;
+
+ $this->track_event( 'cart_item_updated', $event_data );
+ }
+
+ /**
+ * Handle cart item removed event.
+ *
+ * Triggers fraud protection event tracking when an item is removed from the cart.
+ *
+ * @internal
+ *
+ * @param string $cart_item_key Cart item key.
+ * @param object $cart Cart object.
+ * @return void
+ */
+ public function handle_track_cart_item_removed( $cart_item_key, $cart ): void {
+ $cart_item = $cart->removed_cart_contents[ $cart_item_key ] ?? null;
+
+ if ( ! $cart_item ) {
+ return;
+ }
+
+ $product_id = $cart_item['product_id'] ?? 0;
+ $variation_id = $cart_item['variation_id'] ?? 0;
+ $quantity = $cart_item['quantity'] ?? 0;
+
+ $event_data = $this->build_cart_event_data(
+ 'item_removed',
+ $product_id,
+ $quantity,
+ $variation_id
+ );
+
+ $this->track_event( 'cart_item_removed', $event_data );
+ }
+
+ /**
+ * Handle cart item restored event.
+ *
+ * Triggers fraud protection event tracking when a removed item is restored to the cart.
+ *
+ * @internal
+ *
+ * @param string $cart_item_key Cart item key.
+ * @param object $cart Cart object.
+ * @return void
+ */
+ public function handle_track_cart_item_restored( $cart_item_key, $cart ): void {
+ $cart_item = $cart->cart_contents[ $cart_item_key ] ?? null;
+
+ if ( ! $cart_item ) {
+ return;
+ }
+
+ $product_id = $cart_item['product_id'] ?? 0;
+ $variation_id = $cart_item['variation_id'] ?? 0;
+ $quantity = $cart_item['quantity'] ?? 0;
+
+ $event_data = $this->build_cart_event_data(
+ 'item_restored',
+ $product_id,
+ $quantity,
+ $variation_id
+ );
+
+ $this->track_event( 'cart_item_restored', $event_data );
+ }
+
+ /**
+ * Build cart event-specific data.
+ *
+ * Prepares the cart event data including action type, product details,
+ * and current cart state. This data will be merged with comprehensive
+ * session data during event tracking.
+ *
+ * @param string $action Action type (item_added, item_updated, item_removed, item_restored).
+ * @param int $product_id Product ID.
+ * @param int $quantity Quantity.
+ * @param int $variation_id Variation ID.
+ * @return array Cart event data.
+ */
+ private function build_cart_event_data( string $action, int $product_id, int $quantity, int $variation_id ): array {
+ $cart_item_count = 0;
+
+ // Get current cart item count if cart is available.
+ if ( WC()->cart instanceof \WC_Cart ) {
+ $cart_item_count = WC()->cart->get_cart_contents_count();
+ }
+
+ return array(
+ 'action' => $action,
+ 'product_id' => $product_id,
+ 'quantity' => $quantity,
+ 'variation_id' => $variation_id,
+ 'cart_item_count' => $cart_item_count,
+ );
+ }
+
+ /**
+ * Track fraud protection event with comprehensive session context.
+ *
+ * This method orchestrates the event tracking by:
+ * 1. Collecting comprehensive session data via SessionDataCollector
+ * 2. Merging with event-specific data
+ * 3. Logging the event (will call EventTracker/API client once available)
+ *
+ * The method implements graceful degradation - any errors during tracking
+ * will be logged but will not break the cart functionality.
+ *
+ * @param string $event_type Event type identifier (e.g., 'cart_item_added').
+ * @param array $event_specific_data Event-specific data to merge with session context.
+ * @return void
+ */
+ private function track_event( string $event_type, array $event_specific_data ): void {
+ try {
+ // Collect comprehensive session data.
+ $session_data = $this->data_collector->collect( $event_type, $event_specific_data );
+
+ // Once EventTracker/API client is implemented (WOOSUBS-1249), call it here:
+ // $event_tracker = wc_get_container()->get( EventTracker::class );
+ // $event_tracker->track( $event_type, $session_data );
+ //
+ // For now, log the event for debugging and verification.
+ FraudProtectionController::log(
+ 'info',
+ sprintf(
+ 'Fraud protection event tracked: %s | Product ID: %s | Quantity: %s | Session ID: %s',
+ $event_type,
+ $event_specific_data['product_id'] ?? 'N/A',
+ $event_specific_data['quantity'] ?? 'N/A',
+ $session_data['session']['session_id'] ?? 'N/A'
+ ),
+ array(
+ 'event_type' => $event_type,
+ 'event_data' => $event_specific_data,
+ 'session_data' => $session_data,
+ )
+ );
+ } catch ( \Exception $e ) {
+ // Gracefully handle errors - fraud protection should never break the cart.
+ FraudProtectionController::log(
+ 'error',
+ sprintf(
+ 'Failed to track fraud protection event: %s | Error: %s',
+ $event_type,
+ $e->getMessage()
+ ),
+ array(
+ 'event_type' => $event_type,
+ 'exception' => $e,
+ )
+ );
+ }
+ }
+}
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
index fd8944d649..826d221eb1 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
@@ -172,7 +172,7 @@ class WC_Helper_Product {
$product->set_props(
array(
'name' => 'Dummy Variable Product',
- 'sku' => 'DUMMY VARIABLE SKU',
+ 'sku' => 'DUMMY VARIABLE SKU' . microtime(),
)
);
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php
index 018b581fac..e87b682dd0 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php
@@ -134,7 +134,7 @@ class ProductHelper {
$product->set_props(
array(
'name' => 'Dummy Variable Product',
- 'sku' => 'DUMMY VARIABLE SKU',
+ 'sku' => 'DUMMY VARIABLE SKU' . microtime(),
)
);
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php
index 992947da69..5bcb990529 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php
@@ -70,7 +70,7 @@ class WC_Admin_Tests_API_Product_Attributes extends WC_REST_Unit_Test_Case {
$product_2->set_props(
array(
'name' => 'Dummy Variable Product 2',
- 'sku' => 'DUMMY VARIABLE SKU 2',
+ 'sku' => 'DUMMY VARIABLE SKU 2' . microtime(),
)
);
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
index b761aeb3be..c6bf23364f 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
@@ -66,7 +66,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
$product->set_props(
array(
'name' => 'Dummy Variable Product',
- 'sku' => 'DUMMY VARIABLE SKU',
+ 'sku' => 'DUMMY VARIABLE SKU' . microtime(),
)
);
@@ -120,7 +120,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
$product->set_props(
array(
'name' => 'Dummy Variable Product',
- 'sku' => 'DUMMY VARIABLE SKU',
+ 'sku' => 'DUMMY VARIABLE SKU' . microtime(),
)
);
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
new file mode 100644
index 0000000000..4b6f66d534
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
@@ -0,0 +1,314 @@
+<?php
+/**
+ * CartEventTrackerTest class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
+use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
+use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
+
+/**
+ * Tests for CartEventTracker.
+ *
+ * @covers \Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker
+ */
+class CartEventTrackerTest extends \WC_Unit_Test_Case {
+
+ /**
+ * The system under test.
+ *
+ * @var CartEventTracker
+ */
+ private $sut;
+
+ /**
+ * Mock session data collector.
+ *
+ * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $mock_data_collector;
+
+ /**
+ * Mock fraud protection controller.
+ *
+ * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $mock_controller;
+
+ /**
+ * Test product.
+ *
+ * @var \WC_Product
+ */
+ private $test_product;
+
+ /**
+ * Runs before each test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ // Ensure WooCommerce cart and session are available.
+ if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
+ wc_load_cart();
+ }
+
+ // Create mocks.
+ $this->mock_data_collector = $this->createMock( SessionDataCollector::class );
+ $this->mock_controller = $this->createMock( FraudProtectionController::class );
+
+ // Create system under test.
+ $this->sut = new CartEventTracker();
+ $this->sut->init(
+ $this->mock_data_collector,
+ $this->mock_controller
+ );
+
+ // Create a test product.
+ $this->test_product = \WC_Helper_Product::create_simple_product();
+
+ // Empty cart before each test.
+ WC()->cart->empty_cart();
+ }
+
+ /**
+ * Test that register does not register hooks when feature is disabled.
+ */
+ public function test_register_does_not_register_hooks_when_feature_disabled(): void {
+ // Mock feature as disabled.
+ $this->mock_controller->method( 'feature_is_enabled' )->willReturn( false );
+
+ // Call register.
+ $this->sut->register();
+
+ // Verify hooks were not registered.
+ $this->assertFalse( has_action( 'woocommerce_add_to_cart', array( $this->sut, 'handle_track_cart_item_added' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_after_cart_item_quantity_update', array( $this->sut, 'handle_track_cart_item_updated' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_remove_cart_item', array( $this->sut, 'handle_track_cart_item_removed' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_restore_cart_item', array( $this->sut, 'handle_track_cart_item_restored' ) ) );
+ }
+
+ /**
+ * Test that register registers hooks when feature is enabled.
+ */
+ public function test_register_registers_hooks_when_feature_enabled(): void {
+ // Mock feature as enabled.
+ $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
+
+ // Call register.
+ $this->sut->register();
+
+ // Verify hooks were registered with correct priority.
+ $this->assertEquals( 10, has_action( 'woocommerce_add_to_cart', array( $this->sut, 'handle_track_cart_item_added' ) ) );
+ $this->assertEquals( 10, has_action( 'woocommerce_after_cart_item_quantity_update', array( $this->sut, 'handle_track_cart_item_updated' ) ) );
+ $this->assertEquals( 10, has_action( 'woocommerce_remove_cart_item', array( $this->sut, 'handle_track_cart_item_removed' ) ) );
+ $this->assertEquals( 10, has_action( 'woocommerce_restore_cart_item', array( $this->sut, 'handle_track_cart_item_restored' ) ) );
+ }
+
+ /**
+ * Test handle_track_cart_item_added collects and logs event data.
+ */
+ public function test_handle_track_cart_item_added_tracks_event(): void {
+ // Mock the data collector to return test data.
+ $this->mock_data_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'cart_item_added' ),
+ $this->callback(
+ function ( $event_data ) {
+ return isset( $event_data['action'] )
+ && 'item_added' === $event_data['action']
+ && isset( $event_data['product_id'] )
+ && isset( $event_data['quantity'] )
+ && isset( $event_data['cart_item_count'] )
+ && is_numeric( $event_data['cart_item_count'] );
+ }
+ )
+ )
+ ->willReturn(
+ array(
+ 'event_type' => 'cart_item_added',
+ 'session' => array( 'session_id' => 'test-session' ),
+ )
+ );
+
+ // Call the handler.
+ $this->sut->handle_track_cart_item_added(
+ 'test_cart_key',
+ $this->test_product->get_id(),
+ 2,
+ 0,
+ array(),
+ array()
+ );
+ }
+
+ /**
+ * Test handle_track_cart_item_updated collects event data.
+ */
+ public function test_handle_track_cart_item_updated_tracks_event(): void {
+ // Add item to cart first.
+ $cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
+
+ // Mock the data collector.
+ $this->mock_data_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'cart_item_updated' ),
+ $this->callback(
+ function ( $event_data ) {
+ return isset( $event_data['action'] )
+ && 'item_updated' === $event_data['action']
+ && isset( $event_data['old_quantity'] );
+ }
+ )
+ )
+ ->willReturn(
+ array(
+ 'event_type' => 'cart_item_updated',
+ 'session' => array( 'session_id' => 'test-session' ),
+ )
+ );
+
+ // Call the handler.
+ $this->sut->handle_track_cart_item_updated(
+ $cart_item_key,
+ 5,
+ 1,
+ WC()->cart
+ );
+ }
+
+ /**
+ * Test handle_track_cart_item_removed collects event data.
+ */
+ public function test_handle_track_cart_item_removed_tracks_event(): void {
+ // Add item to cart.
+ $cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
+
+ // Mock the data collector.
+ $this->mock_data_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'cart_item_removed' ),
+ $this->callback(
+ function ( $event_data ) {
+ return isset( $event_data['action'] )
+ && 'item_removed' === $event_data['action'];
+ }
+ )
+ )
+ ->willReturn(
+ array(
+ 'event_type' => 'cart_item_removed',
+ 'session' => array( 'session_id' => 'test-session' ),
+ )
+ );
+
+ // Remove the item from cart.
+ WC()->cart->remove_cart_item( $cart_item_key );
+
+ // Call the handler directly (since hooks aren't registered in test context).
+ $this->sut->handle_track_cart_item_removed( $cart_item_key, WC()->cart );
+ }
+
+ /**
+ * Test handle_track_cart_item_restored collects event data.
+ */
+ public function test_handle_track_cart_item_restored_tracks_event(): void {
+ // Add item to cart.
+ $cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
+
+ // Mock the data collector.
+ $this->mock_data_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'cart_item_restored' ),
+ $this->callback(
+ function ( $event_data ) {
+ return isset( $event_data['action'] )
+ && 'item_restored' === $event_data['action'];
+ }
+ )
+ )
+ ->willReturn(
+ array(
+ 'event_type' => 'cart_item_restored',
+ 'session' => array( 'session_id' => 'test-session' ),
+ )
+ );
+
+ // Call the handler directly (simulating restore action).
+ $this->sut->handle_track_cart_item_restored(
+ $cart_item_key,
+ WC()->cart
+ );
+ }
+
+ /**
+ * Test that cart events include variation ID when present.
+ */
+ public function test_cart_events_include_variation_id(): void {
+ // Create a variable product with variation.
+ $variable_product = \WC_Helper_Product::create_variation_product();
+ $variations = $variable_product->get_available_variations();
+ $variation_id = $variations[0]['variation_id'];
+
+ // Mock the data collector to capture event data.
+ $this->mock_data_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'cart_item_added' ),
+ $this->callback(
+ function ( $event_data ) use ( $variation_id ) {
+ return isset( $event_data['variation_id'] )
+ && $variation_id === $event_data['variation_id'];
+ }
+ )
+ )
+ ->willReturn( array() );
+
+ // Call the handler with variation ID.
+ $this->sut->handle_track_cart_item_added(
+ 'test_cart_key',
+ $variable_product->get_id(),
+ 1,
+ $variation_id,
+ array(),
+ array()
+ );
+
+ // Clean up.
+ $variable_product->delete( true );
+ }
+
+ /**
+ * Cleanup after test.
+ */
+ public function tearDown(): void {
+ parent::tearDown();
+
+ // Clean up test product.
+ if ( $this->test_product ) {
+ $this->test_product->delete( true );
+ }
+
+ // Empty cart.
+ WC()->cart->empty_cart();
+
+ // Remove all registered hooks.
+ remove_all_actions( 'woocommerce_add_to_cart' );
+ remove_all_actions( 'woocommerce_after_cart_item_quantity_update' );
+ remove_all_actions( 'woocommerce_remove_cart_item' );
+ remove_all_actions( 'woocommerce_restore_cart_item' );
+ }
+}