Commit bbd747eed9 for woocommerce
commit bbd747eed92b55bd309788e2aa78cdc928e97a1c
Author: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
Date: Wed Feb 4 14:30:36 2026 -0300
[Fraud protection] Replace Dispatcher with SessionDataCollector and Blackbox API (#63070)
Refactored the WooCommerce Fraud Protection system to replace the real-time event dispatching pattern with a session-based data collection approach, integrating directly with the Blackbox fraud protection API
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php b/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php
index c1d0609030..e71574255f 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php
@@ -12,10 +12,11 @@ use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client;
defined( 'ABSPATH' ) || exit;
/**
- * Handles communication with the WPCOM fraud protection endpoint.
+ * Handles communication with the Blackbox fraud protection API.
*
- * Uses Jetpack Connection for authenticated requests to the WPCOM endpoint
- * to get fraud protection decisions (allow, block, or challenge).
+ * Uses Jetpack Connection for authenticated requests to the Blackbox API
+ * to verify sessions and report fraud events. The API returns fraud protection
+ * decisions (allow, block, or challenge).
*
* This class implements a fail-open pattern: if the endpoint is unreachable,
* times out, or returns an error, it returns an "allow" decision to ensure
@@ -28,18 +29,27 @@ class ApiClient {
/**
* Default timeout for API requests in seconds.
+ *
+ * Using 10 seconds as a reasonable timeout for fraud verification during checkout.
+ * This balances giving the API enough time to respond while not blocking
+ * checkout for too long if the service is slow.
+ */
+ private const DEFAULT_TIMEOUT = 10;
+
+ /**
+ * Blackbox API base URL.
*/
- private const DEFAULT_TIMEOUT = 30;
+ private const BLACKBOX_API_BASE_URL = 'https://blackbox-api.wp.com/v1';
/**
- * WPCOM API version.
+ * Blackbox API verify endpoint path.
*/
- private const WPCOM_API_VERSION = '2';
+ private const VERIFY_ENDPOINT = '/verify';
/**
- * WPCOM fraud protection events endpoint path within Transact platform.
+ * Blackbox API report endpoint path.
*/
- private const EVENTS_ENDPOINT = 'transact/fraud_protection/events';
+ private const REPORT_ENDPOINT = '/report';
/**
* Decision type: allow session.
@@ -67,38 +77,90 @@ class ApiClient {
);
/**
- * Send a fraud protection event and get a decision from WPCOM endpoint.
+ * Verify a session with the Blackbox API and get a fraud decision.
*
* Implements fail-open pattern: if the endpoint is unreachable or times out,
* returns "allow" decision and logs the error.
*
* @since 10.5.0
*
- * @param string $event_type Type of event being sent (e.g., 'cart_updated', 'checkout_started').
- * @param array<string, mixed> $event_data Event data to send to the endpoint.
+ * @param string $session_id Session ID to verify.
+ * @param array<string, mixed> $payload Event data to send to the endpoint.
* @return string Decision: "allow" or "block".
*/
- public function send_event( string $event_type, array $event_data ): string {
- $payload = array_merge(
- array( 'event_type' => $event_type ),
- array_filter( $event_data, fn( $value ) => null !== $value )
+ public function verify( string $session_id, array $payload ): string {
+ FraudProtectionController::log(
+ 'info',
+ 'Verifying session with Blackbox API',
+ array(
+ 'session_id' => $session_id,
+ 'payload' => $payload,
+ )
);
+ $response = $this->make_request( 'POST', self::VERIFY_ENDPOINT, $session_id, $payload );
+
+ return $this->process_decision_response( $response, $payload );
+ }
+
+ /**
+ * Report a fraud event to the Blackbox API.
+ *
+ * Used for reporting outcomes and feedback to improve fraud detection.
+ * This is a fire-and-forget operation - errors are logged but do not
+ * affect the checkout flow.
+ *
+ * @since 10.5.0
+ *
+ * @param string $session_id Session ID to report.
+ * @param array<string, mixed> $payload Event data to send to the endpoint.
+ * @return bool True if report was sent successfully, false otherwise.
+ */
+ public function report( string $session_id, array $payload ): bool {
FraudProtectionController::log(
'info',
- sprintf( 'Sending fraud protection event: %s', $event_type ),
+ 'Reporting event to Blackbox API',
array( 'payload' => $payload )
);
- $response = $this->make_request( 'POST', self::EVENTS_ENDPOINT, $payload );
+ $response = $this->make_request( 'POST', self::REPORT_ENDPOINT, $session_id, $payload );
+ if ( is_wp_error( $response ) ) {
+ FraudProtectionController::log(
+ 'error',
+ sprintf(
+ 'Failed to report event to Blackbox API: %s',
+ $response->get_error_message()
+ ),
+ array( 'error' => $response->get_error_data() )
+ );
+ return false;
+ }
+
+ FraudProtectionController::log(
+ 'info',
+ 'Event reported successfully',
+ array( 'response' => $response )
+ );
+
+ return true;
+ }
+
+ /**
+ * Process the API response and extract the decision.
+ *
+ * @param array<string, mixed>|\WP_Error $response API response or WP_Error.
+ * @param array<string, mixed> $event_data Event data for logging.
+ * @return string Decision: "allow" or "block".
+ */
+ private function process_decision_response( $response, array $event_data ): string {
if ( is_wp_error( $response ) ) {
$error_data = $response->get_error_data() ?? array();
$error_data = is_array( $error_data ) ? $error_data : array( 'error' => $error_data );
FraudProtectionController::log(
'error',
sprintf(
- 'Event track request failed: %s. Failing open with "allow" decision.',
+ 'Blackbox API request failed: %s. Failing open with "allow" decision.',
$response->get_error_message()
),
$error_data
@@ -131,6 +193,7 @@ class ApiClient {
$session = is_array( $event_data['session'] ?? null ) ? $event_data['session'] : array();
$session_id = $session['session_id'] ?? 'unknown';
+ $event_type = $event_data['event_type'] ?? 'unknown';
FraudProtectionController::log(
'info',
sprintf(
@@ -146,14 +209,18 @@ class ApiClient {
}
/**
- * Make an HTTP request to a WPCOM endpoint via Jetpack Connection.
+ * Make an HTTP request to the Blackbox API via Jetpack Connection.
+ *
+ * Uses Jetpack's signed request mechanism which authenticates with the
+ * blog token scoped to the blog_id.
*
- * @param string $method HTTP method (GET, POST, etc.).
- * @param string $path Endpoint path (relative to sites/{blog_id}/).
- * @param array<string, mixed> $payload Request payload.
+ * @param string $method HTTP method (GET, POST, etc.).
+ * @param string $path Endpoint path (relative to Blackbox API base URL).
+ * @param string $session_id Session ID for the request.
+ * @param array<string, mixed> $payload Request payload.
* @return array<string, mixed>|\WP_Error Parsed JSON response or WP_Error on failure.
*/
- private function make_request( string $method, string $path, array $payload ) {
+ private function make_request( string $method, string $path, string $session_id, array $payload ) {
if ( ! class_exists( Jetpack_Connection_Client::class ) ) {
return new \WP_Error(
'jetpack_not_available',
@@ -169,9 +236,15 @@ class ApiClient {
);
}
- $full_path = sprintf( 'sites/%d/%s', $blog_id, $path );
+ $payload['blog_id'] = $blog_id;
- $body = \wp_json_encode( $payload );
+ $body = \wp_json_encode(
+ array(
+ 'session_id' => $session_id,
+ 'private_key' => '', // Woo will not use private keys for now.
+ 'extra' => $payload,
+ )
+ );
if ( false === $body ) {
return new \WP_Error(
@@ -181,16 +254,19 @@ class ApiClient {
);
}
- $response = Jetpack_Connection_Client::wpcom_json_api_request_as_blog(
- $full_path,
- self::WPCOM_API_VERSION,
+ $url = self::BLACKBOX_API_BASE_URL . $path;
+
+ // Use Jetpack Connection Client to make a signed request.
+ // This authenticates with the blog token automatically.
+ $response = Jetpack_Connection_Client::remote_request(
array(
- 'headers' => array( 'Content-Type' => 'application/json' ),
- 'method' => $method,
- 'timeout' => self::DEFAULT_TIMEOUT,
+ 'url' => $url,
+ 'method' => $method,
+ 'timeout' => self::DEFAULT_TIMEOUT,
+ 'headers' => array( 'Content-Type' => 'application/json' ),
+ 'auth_location' => 'header',
),
- $body,
- 'wpcom'
+ $body
);
if ( is_wp_error( $response ) ) {
@@ -210,7 +286,7 @@ class ApiClient {
if ( $response_code >= 300 ) {
return new \WP_Error(
'api_error',
- sprintf( 'Endpoint %s returned status code %d', "$method $path", $response_code ),
+ sprintf( 'Blackbox API %s %s returned status code %d', $method, $path, $response_code ),
array( 'response' => JSON_ERROR_NONE === json_last_error() ? $data : $response_body )
);
}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
index 6b6e214e50..467e824dd6 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
@@ -13,8 +13,8 @@ defined( 'ABSPATH' ) || exit;
* Tracks cart events for fraud protection analysis.
*
* This class provides methods to track cart events (add, update, remove, restore)
- * for fraud protection event dispatching. Event-specific data is passed
- * to the dispatcher which handles session data collection internally.
+ * for fraud protection. Event-specific data is passed
+ * to the SessionDataCollector which handles session data storage internally.
*
* @since 10.5.0
* @internal This class is part of the internal API and is subject to change without notice.
@@ -22,41 +22,40 @@ defined( 'ABSPATH' ) || exit;
class CartEventTracker {
/**
- * Fraud protection dispatcher instance.
+ * Session data collector instance.
*
- * @var FraudProtectionDispatcher
+ * @var SessionDataCollector
*/
- private FraudProtectionDispatcher $dispatcher;
+ private SessionDataCollector $session_data_collector;
/**
* Initialize with dependencies.
*
* @internal
*
- * @param FraudProtectionDispatcher $dispatcher The fraud protection dispatcher instance.
+ * @param SessionDataCollector $session_data_collector The session data collector instance.
*/
- final public function init( FraudProtectionDispatcher $dispatcher ): void {
- $this->dispatcher = $dispatcher;
+ final public function init( SessionDataCollector $session_data_collector ): void {
+ $this->session_data_collector = $session_data_collector;
}
/**
* Track cart page loaded event.
*
- * Triggers fraud protection event dispatching when the cart page is initially loaded.
+ * Collects session data when the cart page is initially loaded.
* This captures the initial session state before any user interactions.
*
* @internal
* @return void
*/
public function track_cart_page_loaded(): void {
- // Track the page load event. Session data will be collected by the dispatcher.
- $this->dispatcher->dispatch_event( 'cart_page_loaded', array() );
+ $this->session_data_collector->collect( 'cart_page_loaded', array() );
}
/**
* Track cart item added event.
*
- * Triggers fraud protection event dispatching when an item is added to the cart.
+ * Collects session data when an item is added to the cart.
*
* @internal
*
@@ -74,14 +73,13 @@ class CartEventTracker {
$variation_id
);
- // Trigger event dispatching.
- $this->dispatcher->dispatch_event( 'cart_item_added', $event_data );
+ $this->session_data_collector->collect( 'cart_item_added', $event_data );
}
/**
* Track cart item quantity updated event.
*
- * Triggers fraud protection event dispatching when cart item quantity is updated.
+ * Collects session data when cart item quantity is updated.
*
* @internal
*
@@ -108,17 +106,15 @@ class CartEventTracker {
$variation_id
);
- // Add old quantity for context.
$event_data['old_quantity'] = (int) $old_quantity;
- // Trigger event dispatching.
- $this->dispatcher->dispatch_event( 'cart_item_updated', $event_data );
+ $this->session_data_collector->collect( 'cart_item_updated', $event_data );
}
/**
* Track cart item removed event.
*
- * Triggers fraud protection event dispatching when an item is removed from the cart.
+ * Collects session data when an item is removed from the cart.
*
* @internal
*
@@ -144,14 +140,13 @@ class CartEventTracker {
$variation_id
);
- // Trigger event dispatching.
- $this->dispatcher->dispatch_event( 'cart_item_removed', $event_data );
+ $this->session_data_collector->collect( 'cart_item_removed', $event_data );
}
/**
* Track cart item restored event.
*
- * Triggers fraud protection event dispatching when a removed item is restored to the cart.
+ * Collects session data when a removed item is restored to the cart.
*
* @internal
*
@@ -177,8 +172,7 @@ class CartEventTracker {
$variation_id
);
- // Trigger event dispatching.
- $this->dispatcher->dispatch_event( 'cart_item_restored', $event_data );
+ $this->session_data_collector->collect( 'cart_item_restored', $event_data );
}
/**
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
index 317a76f528..b2ff257508 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
@@ -13,21 +13,13 @@ defined( 'ABSPATH' ) || exit;
* Tracks checkout events for fraud protection analysis.
*
* This class provides methods to track both WooCommerce Blocks (Store API) and traditional
- * shortcode checkout events for fraud protection event dispatching.
- * Event-specific data is passed to the dispatcher which handles session data collection internally.
+ * shortcode checkout events for fraud protection. Event-specific data is passed to the
+ * SessionDataCollector which handles session data storage internally.
*
* @since 10.5.0
* @internal This class is part of the internal API and is subject to change without notice.
*/
class CheckoutEventTracker {
-
- /**
- * Fraud protection dispatcher instance.
- *
- * @var FraudProtectionDispatcher
- */
- private FraudProtectionDispatcher $dispatcher;
-
/**
* Session data collector instance.
*
@@ -40,26 +32,23 @@ class CheckoutEventTracker {
*
* @internal
*
- * @param FraudProtectionDispatcher $dispatcher The fraud protection dispatcher instance.
- * @param SessionDataCollector $session_data_collector The session data collector instance.
+ * @param SessionDataCollector $session_data_collector The session data collector instance.
*/
- final public function init( FraudProtectionDispatcher $dispatcher, SessionDataCollector $session_data_collector ): void {
- $this->dispatcher = $dispatcher;
+ final public function init( SessionDataCollector $session_data_collector ): void {
$this->session_data_collector = $session_data_collector;
}
/**
* Track checkout page loaded event.
*
- * Triggers fraud protection event dispatching when the checkout page is initially loaded.
+ * Collects session data when the checkout page is initially loaded.
* This captures the initial session state before any user interactions.
*
* @internal
* @return void
*/
public function track_checkout_page_loaded(): void {
- // Track the page load event. Session data will be collected by the dispatcher.
- $this->dispatcher->dispatch_event( 'checkout_page_loaded', array() );
+ $this->session_data_collector->collect( 'checkout_page_loaded', array() );
}
/**
@@ -73,7 +62,7 @@ class CheckoutEventTracker {
*/
public function track_blocks_checkout_update(): void {
// At this point we don't have any payment or shipping data, so we pass an empty array.
- $this->dispatcher->dispatch_event( 'checkout_update', array() );
+ $this->session_data_collector->collect( 'checkout_update', array() );
}
/**
@@ -120,7 +109,7 @@ class CheckoutEventTracker {
// Only dispatch if either country changed.
if ( $billing_changed || $shipping_changed ) {
$event_data = $this->format_checkout_event_data( 'field_update', $data );
- $this->dispatcher->dispatch_event( 'checkout_update', $event_data );
+ $this->session_data_collector->collect( 'checkout_update', $event_data );
}
}
@@ -271,6 +260,6 @@ class CheckoutEventTracker {
'status' => $order->get_status(),
);
- $this->dispatcher->dispatch_event( 'order_placed', $event_data );
+ $this->session_data_collector->collect( 'order_placed', $event_data );
}
}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionDispatcher.php b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionDispatcher.php
deleted file mode 100644
index 73637a2b2a..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionDispatcher.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-/**
- * FraudProtectionDispatcher class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Centralized fraud protection event dispatcher.
- *
- * This class provides a unified interface for dispatching fraud protection events.
- * It coordinates data collection and transmission for fraud protection events by
- * orchestrating ApiClient and DecisionHandler components.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class FraudProtectionDispatcher {
-
- /**
- * API client instance.
- *
- * @var ApiClient
- */
- private ApiClient $api_client;
-
- /**
- * Decision handler instance.
- *
- * @var DecisionHandler
- */
- private DecisionHandler $decision_handler;
-
- /**
- * Fraud protection controller instance.
- *
- * @var FraudProtectionController
- */
- private FraudProtectionController $fraud_protection_controller;
-
- /**
- * Session data collector instance.
- *
- * @var SessionDataCollector
- */
- private SessionDataCollector $data_collector;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param ApiClient $api_client The API client instance.
- * @param DecisionHandler $decision_handler The decision handler instance.
- * @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance.
- * @param SessionDataCollector $data_collector The session data collector instance.
- */
- final public function init(
- ApiClient $api_client,
- DecisionHandler $decision_handler,
- FraudProtectionController $fraud_protection_controller,
- SessionDataCollector $data_collector
- ): void {
- $this->api_client = $api_client;
- $this->decision_handler = $decision_handler;
- $this->fraud_protection_controller = $fraud_protection_controller;
- $this->data_collector = $data_collector;
- }
-
- /**
- * Dispatch fraud protection event.
- *
- * This method collects session data and dispatches it to the fraud protection service.
- * It orchestrates the following flow:
- * 1. Check if feature is enabled (fail-open if not)
- * 2. Collect comprehensive session data via SessionDataCollector
- * 3. Apply extension data filter to allow custom data
- * 4. Send event to API and get decision
- * 5. Apply decision via DecisionHandler
- *
- * The method implements graceful degradation - any errors during tracking
- * will be logged but will not break the functionality.
- *
- * @param string $event_type Event type identifier (e.g., 'cart_item_added').
- * @param array $event_data Optional event-specific data to include with session data.
- * @return void
- */
- public function dispatch_event( string $event_type, array $event_data = array() ): void {
- try {
- // Check if feature is enabled - fail-open if not.
- if ( ! $this->fraud_protection_controller->feature_is_enabled() ) {
- FraudProtectionController::log(
- 'debug',
- sprintf(
- 'Fraud protection event not dispatched (feature disabled): %s',
- $event_type
- ),
- array( 'event_type' => $event_type )
- );
- return;
- }
-
- // Collect comprehensive session data.
- $collected_data = $this->data_collector->collect( $event_type, $event_data );
-
- /**
- * Filters the fraud protection event data before sending to the API.
- *
- * This filter allows extensions to modify or add custom data to fraud protection
- * events. Common use cases include:
- * - Adding custom payment gateway data
- * - Adding subscription-specific context
- * - Adding custom risk signals
- *
- * @since 10.5.0
- *
- * @param array $collected_data Fully-collected event data including session context.
- * @param string $event_type Event type identifier (e.g., 'cart_item_added').
- */
- $collected_data = apply_filters( 'woocommerce_fraud_protection_event_data', $collected_data, $event_type );
-
- // Send event to API and get decision.
- $decision = $this->api_client->send_event( $event_type, $collected_data );
-
- // Apply decision via DecisionHandler.
- $this->decision_handler->apply_decision( $decision, $collected_data );
- } catch ( \Exception $e ) {
- // Gracefully handle errors - fraud protection should never break functionality.
- FraudProtectionController::log(
- 'error',
- sprintf(
- 'Failed to dispatch fraud protection event: %s | Error: %s',
- $event_type,
- $e->getMessage()
- ),
- array(
- 'event_type' => $event_type,
- 'exception' => $e,
- )
- );
- }
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
index 3e7530a11f..f3aa41548a 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
@@ -13,8 +13,8 @@ defined( 'ABSPATH' ) || exit;
* Tracks payment method events for fraud protection analysis.
*
* This class provides methods to track events for adding payment methods in My Account page
- * for fraud protection.
- * Event-specific data is passed to the dispatcher which handles session data collection internally.
+ * for fraud protection. Event-specific data is passed to the SessionDataCollector which
+ * handles session data storage internally.
*
* @since 10.5.0
* @internal This class is part of the internal API and is subject to change without notice.
@@ -22,41 +22,40 @@ defined( 'ABSPATH' ) || exit;
class PaymentMethodEventTracker {
/**
- * Fraud protection dispatcher instance.
+ * Session data collector instance.
*
- * @var FraudProtectionDispatcher
+ * @var SessionDataCollector
*/
- private FraudProtectionDispatcher $dispatcher;
+ private SessionDataCollector $session_data_collector;
/**
* Initialize with dependencies.
*
* @internal
*
- * @param FraudProtectionDispatcher $dispatcher The fraud protection dispatcher instance.
+ * @param SessionDataCollector $session_data_collector The session data collector instance.
*/
- final public function init( FraudProtectionDispatcher $dispatcher ): void {
- $this->dispatcher = $dispatcher;
+ final public function init( SessionDataCollector $session_data_collector ): void {
+ $this->session_data_collector = $session_data_collector;
}
/**
* Track add payment method page loaded event.
*
- * Triggers fraud protection event dispatching when the add payment method page is initially loaded.
+ * Collects session data when the add payment method page is initially loaded.
* This captures the initial session state before any user interactions.
*
* @internal
* @return void
*/
public function track_add_payment_method_page_loaded(): void {
- // Track the page load event. Session data will be collected by the dispatcher.
- $this->dispatcher->dispatch_event( 'add_payment_method_page_loaded', array() );
+ $this->session_data_collector->collect( 'add_payment_method_page_loaded', array() );
}
/**
* Track payment method added event.
*
- * Triggers fraud protection event tracking when a payment method is added.
+ * Collects session data when a payment method is added.
*
* @internal
*
@@ -66,8 +65,7 @@ class PaymentMethodEventTracker {
public function track_payment_method_added( $token_id, $token ): void {
$event_data = $this->build_payment_method_event_data( 'added', $token );
- // Trigger event dispatching.
- $this->dispatcher->dispatch_event( 'payment_method_added', $event_data );
+ $this->session_data_collector->collect( 'payment_method_added', $event_data );
}
/**
@@ -75,7 +73,7 @@ class PaymentMethodEventTracker {
*
* Extracts relevant information from the payment token object including
* token type, gateway ID, user ID, and card details for card tokens.
- * This data will be merged with comprehensive session data during event tracking.
+ * This data will be merged with session data during collection.
*
* @param string $action Action type (added, updated, set_default, deleted, add_failed).
* @param \WC_Payment_Token $token The payment token object.
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php b/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
index 6cf284540f..a1f1975ef3 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
@@ -51,28 +51,79 @@ class SessionDataCollector {
* @since 10.5.0
*
* @param string|null $event_type Optional event type identifier (e.g., 'checkout_started', 'payment_attempt').
- * @param array $event_data Optional event-specific additional context data (may include 'order_id').
- * @return array Nested array containing all collected fraud protection data.
+ * @param array $event_data Optional event-specific additional context data.
*/
- public function collect( ?string $event_type = null, array $event_data = array() ): array {
+ public function collect( ?string $event_type = null, array $event_data = array() ): void {
// Ensure cart and session are loaded.
$this->session_clearance_manager->ensure_cart_loaded();
- // Extract order ID from event_data if provided.
- // There seem to be no universal way to get order id from session data, so we may start with passing it as a parameter when calling this method.
- $order_id_from_event = $event_data['order_id'] ?? null;
+ $data = array(
+ 'event_type' => $event_type,
+ 'timestamp' => gmdate( 'Y-m-d H:i:s' ),
+ 'event_data' => $event_data,
+ );
+
+ // Save the collected data in the session for fraud analysis tracking, preserving multiple calls.
+ if ( WC()->session instanceof \WC_Session ) {
+ // Retrieve existing data array or initialize if not present.
+ $collected_data = WC()->session->get( 'fraud_protection_collected_data' );
+ if ( ! is_array( $collected_data ) ) {
+ $collected_data = array();
+ }
+ $collected_data[] = $data;
+ $collected_data = $this->trim_to_max_size( $collected_data );
+ WC()->session->set( 'fraud_protection_collected_data', $collected_data );
+ } else {
+ FraudProtectionController::log(
+ 'error',
+ 'Attempted to save fraud protection data, but no valid WooCommerce session exists.',
+ array(
+ 'context' => 'SessionDataCollector::collect',
+ 'event_type' => $event_type,
+ 'event_data' => $event_data,
+ )
+ );
+ }
+ }
- return array(
- 'event_type' => $event_type,
- 'timestamp' => gmdate( 'Y-m-d H:i:s' ),
+ /**
+ * Get all collected fraud protection data from the session.
+ *
+ * Retrieves the array of collected event data stored during this session.
+ * Returns an empty array if no data has been collected or session is unavailable.
+ *
+ * @since 10.5.0
+ *
+ * @param int|null $order_id Optional order ID to include order data in the response.
+ * @return array Array of collected fraud protection event data.
+ */
+ public function get_collected_data( ?int $order_id = null ): array {
+ $data = array(
'wc_version' => WC()->version,
'session' => $this->get_session_data(),
'customer' => $this->get_customer_data(),
- 'order' => $this->get_order_data( $order_id_from_event ),
+ 'order' => array(),
'shipping_address' => $this->get_shipping_address(),
'billing_address' => $this->get_billing_address(),
- 'event_data' => $event_data,
+ 'collected_events' => array(),
);
+
+ if ( $order_id ) {
+ $data['order'] = $this->get_order_data( $order_id );
+ }
+
+ // Calculate base data size to ensure total response stays under limit.
+ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for size calculation only.
+ $base_size = strlen( serialize( $data ) );
+
+ if ( WC()->session instanceof \WC_Session ) {
+ $collected_data = WC()->session->get( 'fraud_protection_collected_data' );
+ if ( is_array( $collected_data ) ) {
+ $data['collected_events'] = $this->trim_to_max_size( $collected_data, $base_size );
+ }
+ }
+
+ return $data;
}
/**
@@ -604,4 +655,32 @@ class SessionDataCollector {
);
return implode( ', ', $category_names );
}
+
+ /**
+ * Trim collected data array to ensure it stays within 1 MB size limit.
+ *
+ * Removes oldest entries from the array until the serialized size is under the limit.
+ * Always keeps at least one entry (the most recent).
+ *
+ * @since 10.5.0
+ *
+ * @param array $data Array of collected event data.
+ * @param int $base_size Size in bytes of additional data that will be combined with this array.
+ * @return array Trimmed array that fits within the size limit.
+ */
+ private function trim_to_max_size( array $data, int $base_size = 0 ): array {
+ $max_size_bytes = 1 * 1024 * 1024 - $base_size; // 1 MB minus base data size.
+ $data_count = count( $data );
+ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for size calculation only.
+ $data_size = strlen( serialize( $data ) );
+
+ while ( $data_count > 1 && $data_size > $max_size_bytes ) {
+ array_shift( $data );
+ $data_count = count( $data );
+ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for size calculation only.
+ $data_size = strlen( serialize( $data ) );
+ }
+
+ return $data;
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php
index abbbcf51cd..f926f03021 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php
@@ -14,6 +14,10 @@ use WP_Error;
/**
* Tests for the ApiClient class.
+ *
+ * Tests the Blackbox API client which provides:
+ * - verify(): Verify a session and get a fraud decision (allow/block)
+ * - report(): Report fraud events for feedback
*/
class ApiClientTest extends WC_Unit_Test_Case {
@@ -24,7 +28,7 @@ class ApiClientTest extends WC_Unit_Test_Case {
*
* @var ApiClient
*/
- private $sut;
+ private ApiClient $sut;
/**
* Set up test fixtures.
@@ -48,436 +52,301 @@ class ApiClientTest extends WC_Unit_Test_Case {
parent::tearDown();
}
+ /*
+ |--------------------------------------------------------------------------
+ | verify() Tests
+ |--------------------------------------------------------------------------
+ */
+
/**
- * @testdox Send Event should return allow when Jetpack blog ID is not found.
+ * Test verify calls correct endpoint with payload.
+ *
+ * @testdox verify() calls Blackbox API /verify endpoint with the correct payload
*/
- public function test_send_event_returns_allow_when_blog_id_not_found(): void {
- update_option( 'jetpack_options', array( 'id' => null ) );
+ public function test_verify_calls_verify_endpoint(): void {
+ $captured_url = null;
+ $captured_body = null;
+
+ add_filter(
+ 'pre_http_request',
+ function ( $preempt, $args, $url ) use ( &$captured_url, &$captured_body ) {
+ unset( $preempt );
+ $captured_body = json_decode( $args['body'], true );
+ $captured_url = $url;
+ return array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode( array( 'decision' => 'allow' ) ),
+ );
+ },
+ 10,
+ 3
+ );
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged( 'error', 'Jetpack blog ID not found', array( 'source' => 'woo-fraud-protection' ) );
+ $this->assertStringContainsString( 'blackbox-api.wp.com/v1/verify', $captured_url );
+ $this->assertSame( 'test-session-id', $captured_body['session_id'] );
+ $this->assertArrayHasKey( 'extra', $captured_body );
+ $this->assertArrayHasKey( 'blog_id', $captured_body['extra'] );
+ $this->assertSame( 12345, $captured_body['extra']['blog_id'] );
}
/**
- * @testdox Send Event should return allow when HTTP request fails.
+ * Test verify returns allow decision.
+ *
+ * @testdox verify() returns allow decision from API
*/
- public function test_send_event_returns_allow_when_http_request_fails(): void {
+ public function test_verify_returns_allow_decision(): void {
add_filter(
'pre_http_request',
- function () {
- return new WP_Error( 'http_error', 'Connection failed', 'error_data' );
- }
+ fn() => array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode( array( 'decision' => 'allow' ) ),
+ )
);
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged(
- 'error',
- 'Connection failed',
- array(
- 'source' => 'woo-fraud-protection',
- 'error' => 'error_data',
- )
- );
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
}
/**
- * @testdox Send Event should return allow when API returns HTTP error status.
+ * Test verify returns block decision.
+ *
+ * @testdox verify() returns block decision from API
*/
- public function test_send_event_returns_allow_when_api_returns_http_error(): void {
+ public function test_verify_returns_block_decision(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 500 ),
- 'body' => 'Internal Server Error',
- );
- }
+ fn() => array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode( array( 'decision' => 'block' ) ),
+ )
);
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged(
- 'error',
- 'Endpoint POST transact/fraud_protection/events returned status code 500',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => 'Internal Server Error',
- )
- );
+ $this->assertSame( ApiClient::DECISION_BLOCK, $result );
}
/**
- * @testdox Send Event should return allow when API returns HTTP error with JSON body.
+ * Test verify fails open when blog_id not found.
+ *
+ * @testdox verify() fails open with allow when blog_id not found
*/
- public function test_send_event_returns_allow_when_api_returns_http_error_with_json_body(): void {
- $response = array(
- 'error' => 'invalid_request',
- 'message' => 'Missing required field',
- );
-
- add_filter(
- 'pre_http_request',
- function () use ( $response ) {
- return array(
- 'response' => array( 'code' => 400 ),
- 'body' => wp_json_encode( $response ),
- );
- }
- );
+ public function test_verify_fails_open_when_blog_id_not_found(): void {
+ update_option( 'jetpack_options', array( 'id' => null ) );
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged(
- 'error',
- 'Endpoint POST transact/fraud_protection/events returned status code 400',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => $response,
- )
- );
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
+ $this->assertLogged( 'error', 'Jetpack blog ID not found' );
}
/**
- * @testdox Send Event should return allow when API returns invalid JSON.
+ * Test verify fails open on HTTP error.
+ *
+ * @testdox verify() fails open with allow when HTTP request fails
*/
- public function test_send_event_returns_allow_when_api_returns_invalid_json(): void {
+ public function test_verify_fails_open_on_http_error(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => 'not valid json',
- );
- }
+ fn() => new WP_Error( 'http_error', 'Connection timeout' )
);
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged(
- 'error',
- 'Failed to decode JSON response: Syntax error',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => 'not valid json',
- )
- );
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
+ $this->assertLogged( 'error', 'Connection timeout' );
}
/**
- * @testdox Send Event should return allow when API response is missing decision field.
+ * Test verify fails open on server error.
+ *
+ * @testdox verify() fails open with allow when API returns 5xx error
*/
- public function test_send_event_returns_allow_when_response_missing_decision_field(): void {
+ public function test_verify_fails_open_on_server_error(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'fraud_event_id' => 123 ) ),
- );
- }
+ fn() => array(
+ 'response' => array( 'code' => 500 ),
+ 'body' => 'Internal Server Error',
+ )
);
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged(
- 'error',
- 'missing "decision" field',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => array( 'fraud_event_id' => 123 ),
- )
- );
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
+ $this->assertLogged( 'error', 'status code 500' );
}
/**
- * @testdox Send Event should return allow when API returns invalid decision value.
+ * Test verify fails open on invalid JSON.
+ *
+ * @testdox verify() fails open with allow when API returns invalid JSON
*/
- public function test_send_event_returns_allow_when_invalid_decision_value(): void {
+ public function test_verify_fails_open_on_invalid_json(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'decision' => 'invalid_decision' ) ),
- );
- }
+ fn() => array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => 'not valid json',
+ )
);
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow decision' );
- $this->assertLogged(
- 'error',
- 'Invalid decision value "invalid_decision"',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => array( 'decision' => 'invalid_decision' ),
- )
- );
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
+ $this->assertLogged( 'error', 'Failed to decode JSON' );
}
/**
- * @testdox Send Event should return allow decision from API.
+ * Test verify fails open when decision field missing.
+ *
+ * @testdox verify() fails open with allow when response missing decision field
*/
- public function test_send_event_returns_allow_decision_from_api(): void {
+ public function test_verify_fails_open_when_missing_decision(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode(
- array(
- 'fraud_event_id' => 123,
- 'decision' => 'allow',
- 'risk_score' => 10,
- )
- ),
- );
- }
- );
-
- $result = $this->sut->send_event( 'cart_updated', array( 'session_id' => 'test-session' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should return allow decision' );
- $this->assertLogged(
- 'info',
- 'Fraud decision received: allow',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => array(
- 'fraud_event_id' => 123,
- 'decision' => 'allow',
- 'risk_score' => 10,
- ),
+ fn() => array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode( array( 'risk_score' => 50 ) ),
)
);
- $this->assertNoErrorLogged();
+
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
+
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
+ $this->assertLogged( 'error', 'missing "decision" field' );
}
/**
- * @testdox Send Event should return block decision from API.
+ * Test verify fails open on invalid decision value.
+ *
+ * @testdox verify() fails open with allow when decision value is invalid
*/
- public function test_send_event_returns_block_decision_from_api(): void {
+ public function test_verify_fails_open_on_invalid_decision(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode(
- array(
- 'fraud_event_id' => 123,
- 'decision' => 'block',
- 'risk_score' => 95,
- 'reason_tags' => array( 'failures_per_ip' ),
- )
- ),
- );
- }
- );
-
- $result = $this->sut->send_event( 'checkout_started', array( 'session_id' => 'test-session' ) );
-
- $this->assertSame( ApiClient::DECISION_BLOCK, $result, 'Should return block decision' );
- $this->assertLogged(
- 'info',
- 'Fraud decision received: block',
- array(
- 'source' => 'woo-fraud-protection',
- 'response' => array(
- 'fraud_event_id' => 123,
- 'decision' => 'block',
- 'risk_score' => 95,
- 'reason_tags' => array( 'failures_per_ip' ),
- ),
+ fn() => array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode( array( 'decision' => 'unknown_value' ) ),
)
);
- $this->assertNoErrorLogged();
+
+ $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
+
+ $this->assertSame( ApiClient::DECISION_ALLOW, $result );
+ $this->assertLogged( 'error', 'Invalid decision value' );
}
+ /*
+ |--------------------------------------------------------------------------
+ | report() Tests
+ |--------------------------------------------------------------------------
+ */
+
/**
- * @testdox Send Event should return allow when API returns challenge decision (challenge flow not yet implemented).
+ * Test report calls correct endpoint.
+ *
+ * @testdox report() calls Blackbox API /report endpoint
*/
- public function test_send_event_returns_allow_when_challenge_decision_from_api(): void {
+ public function test_report_calls_report_endpoint(): void {
+ $captured_url = null;
+ $captured_body = null;
+
add_filter(
'pre_http_request',
- function () {
+ function ( $preempt, $args, $url ) use ( &$captured_url, &$captured_body ) {
+ unset( $preempt );
+ $captured_url = $url;
+ $captured_body = json_decode( $args['body'], true );
return array(
'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode(
- array(
- 'fraud_event_id' => 123,
- 'decision' => 'challenge',
- 'risk_score' => 65,
- )
- ),
+ 'body' => wp_json_encode( array( 'status' => 'ok' ) ),
);
- }
+ },
+ 10,
+ 3
);
- $result = $this->sut->send_event( 'checkout_started', array( 'session_id' => 'test-session' ) );
+ $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result, 'Should fail open with allow until challenge flow is implemented' );
- $this->assertLogged( 'error', 'Invalid decision value "challenge"' );
+ $this->assertStringContainsString( 'blackbox-api.wp.com/v1/report', $captured_url );
+ $this->assertSame( 'test-session-id', $captured_body['session_id'] );
+ $this->assertArrayHasKey( 'extra', $captured_body );
+ $this->assertArrayHasKey( 'blog_id', $captured_body['extra'] );
+ $this->assertSame( 12345, $captured_body['extra']['blog_id'] );
}
/**
- * @testdox Send Event should log session ID from nested session data structure.
+ * Test report returns true on success.
+ *
+ * @testdox report() returns true on success
*/
- public function test_send_event_logs_session_id_from_nested_structure(): void {
+ public function test_report_returns_true_on_success(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode(
- array(
- 'fraud_event_id' => 123,
- 'decision' => 'allow',
- )
- ),
- );
- }
- );
-
- // Use the nested structure that SessionDataCollector::collect() returns.
- $session_data = array(
- 'event_type' => 'cart_item_added',
- 'session' => array(
- 'session_id' => 'nested-test-session-id',
- 'ip_address' => '192.168.1.1',
- ),
- 'customer' => array(
- 'first_name' => 'Test',
- ),
+ fn() => array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => wp_json_encode( array( 'status' => 'ok' ) ),
+ )
);
- $result = $this->sut->send_event( 'cart_item_added', $session_data );
+ $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged(
- 'info',
- 'Session: nested-test-session-id',
- array( 'source' => 'woo-fraud-protection' )
- );
+ $this->assertTrue( $result );
+ $this->assertLogged( 'info', 'Event reported successfully' );
}
/**
- * @testdox Send Event should log 'unknown' session ID when session data is missing.
+ * Test report returns false when blog_id not found.
+ *
+ * @testdox report() returns false when blog_id not found
*/
- public function test_send_event_logs_unknown_when_session_data_missing(): void {
- add_filter(
- 'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode(
- array(
- 'fraud_event_id' => 123,
- 'decision' => 'allow',
- )
- ),
- );
- }
- );
-
- // Session data without the 'session' key.
- $session_data = array(
- 'event_type' => 'cart_item_added',
- 'customer' => array(
- 'first_name' => 'Test',
- ),
- );
+ public function test_report_returns_false_when_blog_id_not_found(): void {
+ update_option( 'jetpack_options', array( 'id' => null ) );
- $result = $this->sut->send_event( 'cart_item_added', $session_data );
+ $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged(
- 'info',
- 'Session: unknown',
- array( 'source' => 'woo-fraud-protection' )
- );
+ $this->assertFalse( $result );
+ $this->assertLogged( 'error', 'Jetpack blog ID not found' );
}
/**
- * @testdox Send Event should log 'unknown' session ID when session is not an array.
+ * Test report returns false on HTTP error.
+ *
+ * @testdox report() returns false when HTTP request fails
*/
- public function test_send_event_logs_unknown_when_session_is_not_array(): void {
+ public function test_report_returns_false_on_http_error(): void {
add_filter(
'pre_http_request',
- function () {
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode(
- array(
- 'fraud_event_id' => 123,
- 'decision' => 'allow',
- )
- ),
- );
- }
- );
-
- // Session data with 'session' as a non-array value.
- $session_data = array(
- 'event_type' => 'cart_item_added',
- 'session' => 'invalid-string-value',
+ fn() => new WP_Error( 'http_error', 'Connection timeout' )
);
- $result = $this->sut->send_event( 'cart_item_added', $session_data );
+ $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged(
- 'info',
- 'Session: unknown',
- array( 'source' => 'woo-fraud-protection' )
- );
+ $this->assertFalse( $result );
+ $this->assertLogged( 'error', 'Failed to report event' );
}
/**
- * @testdox Should filter out null values from session data payload.
+ * Test report returns false on server error.
+ *
+ * @testdox report() returns false when API returns error status
*/
- public function test_filters_null_values_from_payload(): void {
- $captured_request_body = null;
-
+ public function test_report_returns_false_on_server_error(): void {
add_filter(
'pre_http_request',
- function ( $preempt, $args ) use ( &$captured_request_body ) {
- $captured_request_body = $args['body'];
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'decision' => 'allow' ) ),
- );
- },
- 10,
- 2
- );
-
- $session_data = array(
- 'session_id' => 'test-session',
- 'ip_address' => '192.168.1.1',
- 'email' => null,
- 'user_agent' => 'Mozilla/5.0',
- 'billing_name' => null,
+ fn() => array(
+ 'response' => array( 'code' => 500 ),
+ 'body' => 'Internal Server Error',
+ )
);
- $this->sut->send_event( 'cart_updated', $session_data );
-
- $this->assertNotNull( $captured_request_body, 'Request body should be captured' );
+ $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
- $decoded_body = json_decode( $captured_request_body, true );
- $this->assertArrayHasKey( 'event_type', $decoded_body, 'Should include not-null event_type' );
- $this->assertArrayHasKey( 'session_id', $decoded_body, 'Should include not-null session_id' );
- $this->assertArrayHasKey( 'ip_address', $decoded_body, 'Should include not-null ip_address' );
- $this->assertArrayHasKey( 'user_agent', $decoded_body, 'Should include not-null user_agent' );
- $this->assertArrayNotHasKey( 'email', $decoded_body, 'Should filter out null email' );
- $this->assertArrayNotHasKey( 'billing_name', $decoded_body, 'Should filter out null billing_name' );
+ $this->assertFalse( $result );
+ $this->assertLogged( 'error', 'status code 500' );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
index 376fddfa0b..e4a5e8c548 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
@@ -59,6 +59,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test blocked purchase notice added on checkout.
+ *
* @testdox maybe_add_blocked_purchase_notice should add notice when session is blocked and on checkout page.
*/
public function test_blocked_purchase_notice_added_on_checkout(): void {
@@ -75,6 +77,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test blocked purchase notice added on cart.
+ *
* @testdox maybe_add_blocked_purchase_notice should add notice when session is blocked and on cart page.
*/
public function test_blocked_purchase_notice_added_on_cart(): void {
@@ -91,6 +95,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test blocked purchase notice not added when session allowed.
+ *
* @testdox maybe_add_blocked_purchase_notice should not add notice when session is not blocked.
*/
public function test_blocked_purchase_notice_not_added_when_session_allowed(): void {
@@ -107,6 +113,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test blocked purchase notice prevents duplicates.
+ *
* @testdox maybe_add_blocked_purchase_notice should not add duplicate notices.
*/
public function test_blocked_purchase_notice_prevents_duplicates(): void {
@@ -134,6 +142,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test add payment method action displays blocked message.
+ *
* @testdox Should display generic error notice when before_woocommerce_add_payment_method action fires for blocked sessions.
*/
public function test_add_payment_method_action_displays_blocked_message(): void {
@@ -150,6 +160,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test add payment method action no message for non blocked session.
+ *
* @testdox Should not display message when add payment method action fires for non-blocked sessions.
*/
public function test_add_payment_method_action_no_message_for_non_blocked_session(): void {
@@ -163,6 +175,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test get message html purchase context.
+ *
* @testdox get_message_html should return purchase-specific message when context is 'purchase'.
*/
public function test_get_message_html_purchase_context(): void {
@@ -175,6 +189,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test get message html generic context.
+ *
* @testdox get_message_html should return generic message when context is 'generic' or not specified.
*/
public function test_get_message_html_generic_context(): void {
@@ -188,6 +204,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test get message plaintext purchase context.
+ *
* @testdox get_message_plaintext should return purchase-specific message when context is 'purchase'.
*/
public function test_get_message_plaintext_purchase_context(): void {
@@ -200,6 +218,8 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
}
/**
+ * Test get message plaintext generic context.
+ *
* @testdox get_message_plaintext should return generic message when context is 'generic' or not specified.
*/
public function test_get_message_plaintext_generic_context(): void {
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php
index 44717c507d..539047e849 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php
@@ -84,6 +84,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test add to cart blocked when session blocked.
+ *
* @testdox add_to_cart returns false and adds notice when session is blocked.
*/
public function test_add_to_cart_blocked_when_session_blocked(): void {
@@ -99,6 +101,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test add to cart allowed when session allowed.
+ *
* @testdox add_to_cart succeeds when session is allowed.
*/
public function test_add_to_cart_allowed_when_session_allowed(): void {
@@ -112,6 +116,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test add to cart allowed when feature disabled.
+ *
* @testdox add_to_cart succeeds when fraud protection is disabled.
*/
public function test_add_to_cart_allowed_when_feature_disabled(): void {
@@ -125,6 +131,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test remove cart item blocked when session blocked.
+ *
* @testdox remove_cart_item returns false when session is blocked.
*/
public function test_remove_cart_item_blocked_when_session_blocked(): void {
@@ -142,6 +150,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test remove cart item allowed when session allowed.
+ *
* @testdox remove_cart_item succeeds when session is allowed.
*/
public function test_remove_cart_item_allowed_when_session_allowed(): void {
@@ -156,6 +166,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test remove cart item allowed when feature disabled.
+ *
* @testdox remove_cart_item succeeds when fraud protection is disabled.
*/
public function test_remove_cart_item_allowed_when_feature_disabled(): void {
@@ -169,6 +181,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test set quantity blocked when session blocked.
+ *
* @testdox set_quantity returns false when session is blocked.
*/
public function test_set_quantity_blocked_when_session_blocked(): void {
@@ -186,6 +200,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test set quantity allowed when session allowed.
+ *
* @testdox set_quantity succeeds when session is allowed.
*/
public function test_set_quantity_allowed_when_session_allowed(): void {
@@ -200,6 +216,8 @@ class CartBlockingTest extends \WC_Unit_Test_Case {
}
/**
+ * Test set quantity allowed when feature disabled.
+ *
* @testdox set_quantity succeeds when fraud protection is disabled.
*/
public function test_set_quantity_allowed_when_feature_disabled(): void {
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
index 79d4f02357..725285f718 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
@@ -8,8 +8,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
+use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
/**
* Tests for CartEventTracker.
@@ -26,18 +25,11 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
private $sut;
/**
- * Mock event dispatcher.
+ * Mock session data collector.
*
- * @var FraudProtectionDispatcher|\PHPUnit\Framework\MockObject\MockObject
+ * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_dispatcher;
-
- /**
- * Mock fraud protection controller.
- *
- * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_controller;
+ private $mock_collector;
/**
* Test product.
@@ -57,13 +49,12 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
wc_load_cart();
}
- // Create mocks.
- $this->mock_dispatcher = $this->createMock( FraudProtectionDispatcher::class );
- $this->mock_controller = $this->createMock( FraudProtectionController::class );
+ // Create mock.
+ $this->mock_collector = $this->createMock( SessionDataCollector::class );
// Create system under test.
$this->sut = new CartEventTracker();
- $this->sut->init( $this->mock_dispatcher );
+ $this->sut->init( $this->mock_collector );
// Create a test product.
$this->test_product = \WC_Helper_Product::create_simple_product();
@@ -73,38 +64,35 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
}
/**
- * Test track_cart_page_loaded dispatches event.
- * The CartEventTracker::track_cart_page_loaded does not add any event data.
- * The data collection is handled by the SessionDataCollector.
- * So we only need to test if the dispatcher is called with no event data.
+ * Test cart page loaded collects data.
+ *
+ * @testdox track_cart_page_loaded() collects session data with empty event data.
*/
- public function test_track_cart_page_loaded_dispatches_event(): void {
- // Mock dispatcher to verify event is dispatched with empty event data.
- $this->mock_dispatcher
+ public function test_track_cart_page_loaded_collects_data(): void {
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'cart_page_loaded' ),
$this->equalTo( array() )
);
- // Call the method.
$this->sut->track_cart_page_loaded();
}
/**
- * Test track_cart_item_added tracks event.
+ * Test cart item added collects data.
+ *
+ * @testdox track_cart_item_added() collects session data with event details.
*/
- public function test_track_cart_item_added_tracks_event(): void {
- // Mock the dispatcher to verify dispatch_event is called with event data.
- $this->mock_dispatcher
+ public function test_track_cart_item_added_collects_data(): void {
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'cart_item_added' ),
$this->callback(
function ( $event_data ) {
- // Verify the event data structure.
$this->assertArrayHasKey( 'action', $event_data );
$this->assertEquals( 'item_added', $event_data['action'] );
$this->assertArrayHasKey( 'product_id', $event_data );
@@ -116,7 +104,6 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
)
);
- // Call the method.
$this->sut->track_cart_item_added(
'test_cart_key',
$this->test_product->get_id(),
@@ -126,21 +113,20 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
}
/**
- * Test track_cart_item_updated tracks event.
+ * Test cart item updated collects data.
+ *
+ * @testdox track_cart_item_updated() collects session data with quantity change.
*/
- public function test_track_cart_item_updated_tracks_event(): void {
- // Add item to cart first.
+ public function test_track_cart_item_updated_collects_data(): void {
$cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
- // Mock the dispatcher.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'cart_item_updated' ),
$this->callback(
function ( $event_data ) {
- // Verify the event data structure.
$this->assertArrayHasKey( 'action', $event_data );
$this->assertEquals( 'item_updated', $event_data['action'] );
$this->assertArrayHasKey( 'quantity', $event_data );
@@ -152,7 +138,6 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
)
);
- // Call the method.
$this->sut->track_cart_item_updated(
$cart_item_key,
5,
@@ -162,21 +147,20 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
}
/**
- * Test track_cart_item_removed tracks event.
+ * Test cart item removed collects data.
+ *
+ * @testdox track_cart_item_removed() collects session data.
*/
- public function test_track_cart_item_removed_tracks_event(): void {
- // Add item to cart.
+ public function test_track_cart_item_removed_collects_data(): void {
$cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
- // Mock the dispatcher.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'cart_item_removed' ),
$this->callback(
function ( $event_data ) {
- // Verify the event data structure.
$this->assertArrayHasKey( 'action', $event_data );
$this->assertEquals( 'item_removed', $event_data['action'] );
return true;
@@ -184,29 +168,26 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
)
);
- // Remove the item from cart.
WC()->cart->remove_cart_item( $cart_item_key );
- // Call the method directly.
$this->sut->track_cart_item_removed( $cart_item_key, WC()->cart );
}
/**
- * Test track_cart_item_restored tracks event.
+ * Test cart item restored collects data.
+ *
+ * @testdox track_cart_item_restored() collects session data.
*/
- public function test_track_cart_item_restored_tracks_event(): void {
- // Add item to cart.
+ public function test_track_cart_item_restored_collects_data(): void {
$cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
- // Mock the dispatcher.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'cart_item_restored' ),
$this->callback(
function ( $event_data ) {
- // Verify the event data structure.
$this->assertArrayHasKey( 'action', $event_data );
$this->assertEquals( 'item_restored', $event_data['action'] );
return true;
@@ -214,7 +195,6 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
)
);
- // Call the method directly (simulating restore action).
$this->sut->track_cart_item_restored(
$cart_item_key,
WC()->cart
@@ -222,23 +202,22 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
}
/**
- * Test that cart events include variation ID when present.
+ * Test cart events include variation_id.
+ *
+ * @testdox 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 dispatcher to capture event data.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'cart_item_added' ),
$this->callback(
function ( $event_data ) use ( $variation_id ) {
- // Verify the event data structure includes variation_id.
$this->assertArrayHasKey( 'action', $event_data );
$this->assertEquals( 'item_added', $event_data['action'] );
$this->assertArrayHasKey( 'variation_id', $event_data );
@@ -248,7 +227,6 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
)
);
- // Call the method with variation ID.
$this->sut->track_cart_item_added(
'test_cart_key',
$variable_product->get_id(),
@@ -256,7 +234,6 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
$variation_id
);
- // Clean up.
$variable_product->delete( true );
}
@@ -266,12 +243,10 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
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();
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
index 68c63204f6..9e19678f9c 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
@@ -7,11 +7,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-use Automattic\WooCommerce\Internal\FraudProtection\ApiClient;
use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\DecisionHandler;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher;
use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
/**
@@ -28,26 +24,12 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
*/
private $sut;
- /**
- * Mock fraud protection dispatcher.
- *
- * @var FraudProtectionDispatcher|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_dispatcher;
-
/**
* Mock session data collector.
*
* @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_session_data_collector;
-
- /**
- * Mock fraud protection controller.
- *
- * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_controller;
+ private $mock_collector;
/**
* Runs before each test.
@@ -60,14 +42,12 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
wc_load_cart();
}
- // Create mocks.
- $this->mock_dispatcher = $this->createMock( FraudProtectionDispatcher::class );
- $this->mock_session_data_collector = $this->createMock( SessionDataCollector::class );
- $this->mock_controller = $this->createMock( FraudProtectionController::class );
+ // Create mock.
+ $this->mock_collector = $this->createMock( SessionDataCollector::class );
// Create system under test.
$this->sut = new CheckoutEventTracker();
- $this->sut->init( $this->mock_dispatcher, $this->mock_session_data_collector );
+ $this->sut->init( $this->mock_collector );
}
// ========================================
@@ -75,22 +55,19 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
// ========================================
/**
- * Test track_checkout_page_loaded dispatches event.
- * The CheckoutEventTracker::track_checkout_page_loaded does not add any event data.
- * The data collection is handled by the SessionDataCollector.
- * So we only need to test if the dispatcher is called with no event data.
+ * Test checkout page loaded collects data.
+ *
+ * @testdox track_checkout_page_loaded() collects session data with empty event data.
*/
- public function test_track_checkout_page_loaded_dispatches_event(): void {
- // Mock dispatcher to verify event is dispatched with empty event data.
- $this->mock_dispatcher
+ public function test_track_checkout_page_loaded_collects_data(): void {
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'checkout_page_loaded' ),
$this->equalTo( array() )
);
- // Call the method.
$this->sut->track_checkout_page_loaded();
}
@@ -99,22 +76,19 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
// ========================================
/**
- * Test track_blocks_checkout_update dispatches event with session data.
- * The CheckoutEventTracker::track_blocks_checkout_update does not add any event data.
- * The data collection is handled by the SessionDataCollector.
- * So we only need to test if the dispatcher is called with no event data.
+ * Test blocks checkout update collects data.
+ *
+ * @testdox track_blocks_checkout_update() collects session data with empty event data.
*/
- public function test_track_blocks_checkout_update_dispatches_event_with_empty_session_data(): void {
- // Mock dispatcher to verify event is dispatched with empty event data.
- $this->mock_dispatcher
+ public function test_track_blocks_checkout_update_collects_data(): void {
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'checkout_update' ),
$this->equalTo( array() )
);
- // Call the method.
$this->sut->track_blocks_checkout_update();
}
@@ -123,25 +97,22 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
// ========================================
/**
- * Test track_shortcode_checkout_field_update schedules event with billing email when billing country changes.
+ * Test shortcode checkout field update collects data on billing country change.
+ *
+ * @testdox track_shortcode_checkout_field_update() collects data when billing country changes.
*/
- public function test_track_shortcode_checkout_field_update_schedules_event_with_billing_email(): void {
- // Mock feature as enabled.
- $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- // Mock SessionDataCollector to return different billing country.
- $this->mock_session_data_collector
+ public function test_track_shortcode_checkout_field_update_collects_data_on_billing_country_change(): void {
+ $this->mock_collector
->method( 'get_current_billing_country' )
- ->willReturn( 'CA' ); // Current country is CA.
+ ->willReturn( 'CA' );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
->willReturn( null );
- // Mock scheduler to verify dispatch_event is called.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'checkout_update' ),
$this->callback(
@@ -154,43 +125,38 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
)
);
- // Simulate checkout field update with billing email and country change (CA -> US).
$posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_last_name=Doe&billing_country=US';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
}
/**
- * Test track_shortcode_checkout_field_update extracts billing fields correctly when country changes.
+ * Test shortcode checkout field update extracts billing fields.
+ *
+ * @testdox track_shortcode_checkout_field_update() extracts billing fields correctly.
*/
public function test_track_shortcode_checkout_field_update_extracts_billing_fields(): void {
- // Mock feature as enabled.
- $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- // Mock SessionDataCollector to return different billing country.
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_billing_country' )
- ->willReturn( 'CA' ); // Current country is CA.
+ ->willReturn( 'CA' );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
->willReturn( null );
- // Mock scheduler to capture event data.
$captured_event_data = null;
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->willReturnCallback(
function ( $event_type, $event_data ) use ( &$captured_event_data ) {
$captured_event_data = $event_data;
+ return array();
}
);
- // Simulate checkout field update with multiple billing fields and country change.
$posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_last_name=Doe&billing_country=US&billing_city=New+York';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
- // Verify extracted fields.
$this->assertNotNull( $captured_event_data );
$this->assertEquals( 'field_update', $captured_event_data['action'] );
$this->assertEquals( 'test@example.com', $captured_event_data['billing_email'] );
@@ -201,37 +167,33 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
}
/**
- * Test track_shortcode_checkout_field_update extracts shipping fields when ship_to_different_address is set and shipping country changes.
+ * Test shortcode checkout field update extracts shipping fields.
+ *
+ * @testdox track_shortcode_checkout_field_update() extracts shipping fields when ship_to_different_address is set.
*/
public function test_track_shortcode_checkout_field_update_extracts_shipping_fields(): void {
- // Mock feature as enabled.
- $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- // Mock SessionDataCollector to return different shipping country.
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_billing_country' )
->willReturn( null );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
- ->willReturn( 'CA' ); // Current shipping country is CA.
+ ->willReturn( 'CA' );
- // Mock scheduler to capture event data.
$captured_event_data = null;
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->willReturnCallback(
function ( $event_type, $event_data ) use ( &$captured_event_data ) {
$captured_event_data = $event_data;
+ return array();
}
);
- // Simulate checkout field update with shipping fields and country change.
$posted_data = 'billing_email=test@example.com&ship_to_different_address=1&shipping_first_name=Jane&shipping_last_name=Smith&shipping_city=Los+Angeles&shipping_country=US';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
- // Verify extracted fields.
$this->assertNotNull( $captured_event_data );
$this->assertEquals( 'Jane', $captured_event_data['shipping_first_name'] );
$this->assertEquals( 'Smith', $captured_event_data['shipping_last_name'] );
@@ -239,37 +201,33 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
}
/**
- * Test track_shortcode_checkout_field_update does not extract shipping fields when ship_to_different_address is not set.
+ * Test shortcode checkout field update skips shipping fields when not different address.
+ *
+ * @testdox track_shortcode_checkout_field_update() skips shipping fields when not shipping to different address.
*/
public function test_track_shortcode_checkout_field_update_skips_shipping_fields_when_not_different_address(): void {
- // Mock feature as enabled.
- $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- // Mock SessionDataCollector to return different billing country.
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_billing_country' )
- ->willReturn( 'CA' ); // Current billing country is CA.
+ ->willReturn( 'CA' );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
->willReturn( null );
- // Mock scheduler to capture event data.
$captured_event_data = null;
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->willReturnCallback(
function ( $event_type, $event_data ) use ( &$captured_event_data ) {
$captured_event_data = $event_data;
+ return array();
}
);
- // Simulate checkout field update without ship_to_different_address but with billing country change.
$posted_data = 'billing_email=test@example.com&billing_country=US&shipping_first_name=Jane&shipping_last_name=Smith';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
- // Verify shipping fields are not extracted.
$this->assertNotNull( $captured_event_data );
$this->assertArrayNotHasKey( 'shipping_first_name', $captured_event_data );
$this->assertArrayNotHasKey( 'shipping_last_name', $captured_event_data );
@@ -280,221 +238,112 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
// ========================================
/**
- * Test event is dispatched when billing country changes.
- */
- public function test_event_dispatched_when_billing_country_changes(): void {
- // Mock SessionDataCollector to return different billing country.
- $this->mock_session_data_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'CA' ); // Current country is CA.
-
- $this->mock_session_data_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( null );
-
- // Expect event to be dispatched once.
- $this->mock_dispatcher
- ->expects( $this->once() )
- ->method( 'dispatch_event' )
- ->with(
- $this->equalTo( 'checkout_update' ),
- $this->callback(
- function ( $event_data ) {
- return isset( $event_data['billing_country'] ) && 'US' === $event_data['billing_country'];
- }
- )
- );
-
- // Posted data with billing country changing from CA to US.
- $posted_data = 'billing_email=test@example.com&billing_country=US';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test event is dispatched when shipping country changes.
- */
- public function test_event_dispatched_when_shipping_country_changes(): void {
- // Mock SessionDataCollector to return current countries.
- $this->mock_session_data_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'US' ); // Current billing country matches posted.
-
- $this->mock_session_data_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( 'CA' ); // Current shipping country is CA.
-
- // Expect event to be dispatched once.
- $this->mock_dispatcher
- ->expects( $this->once() )
- ->method( 'dispatch_event' )
- ->with(
- $this->equalTo( 'checkout_update' ),
- $this->callback(
- function ( $event_data ) {
- return isset( $event_data['shipping_country'] ) && 'US' === $event_data['shipping_country'];
- }
- )
- );
-
- // Posted data with shipping country changing from CA to US.
- $posted_data = 'billing_country=US&ship_to_different_address=1&shipping_country=US';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test event is NOT dispatched when neither country changes.
+ * Test no collection when no country changes.
+ *
+ * @testdox Event is NOT collected when neither country changes.
*/
- public function test_event_not_dispatched_when_no_country_changes(): void {
- // Mock SessionDataCollector to return same countries as posted.
- $this->mock_session_data_collector
+ public function test_no_collection_when_no_country_changes(): void {
+ $this->mock_collector
->method( 'get_current_billing_country' )
- ->willReturn( 'US' ); // Same as posted.
+ ->willReturn( 'US' );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
- ->willReturn( 'US' ); // Same as posted.
+ ->willReturn( 'US' );
- // Expect event to NOT be dispatched.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->never() )
- ->method( 'dispatch_event' );
+ ->method( 'collect' );
- // Posted data with no country changes.
$posted_data = 'billing_email=test@example.com&billing_country=US&shipping_country=US';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
}
/**
- * Test event is NOT dispatched when only non-country fields change.
+ * Test no collection when only non-country fields change.
+ *
+ * @testdox Event is NOT collected when only non-country fields change.
*/
- public function test_event_not_dispatched_when_only_non_country_fields_change(): void {
- // Mock SessionDataCollector to return countries.
- $this->mock_session_data_collector
+ public function test_no_collection_when_only_non_country_fields_change(): void {
+ $this->mock_collector
->method( 'get_current_billing_country' )
->willReturn( 'US' );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
->willReturn( null );
- // Expect event to NOT be dispatched.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->never() )
- ->method( 'dispatch_event' );
+ ->method( 'collect' );
- // Posted data with only non-country fields (email, name, phone).
$posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_phone=1234567890';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
}
/**
- * Test event is NOT dispatched when ship_to_different_address is not set and current shipping matches billing.
- */
- public function test_event_not_dispatched_when_shipping_already_matches_billing(): void {
- // Mock SessionDataCollector: shipping already matches billing.
- $this->mock_session_data_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'US' );
-
- $this->mock_session_data_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( 'US' ); // Already matches billing - no change.
-
- // Expect event to NOT be dispatched (no effective change).
- $this->mock_dispatcher
- ->expects( $this->never() )
- ->method( 'dispatch_event' );
-
- // Posted data with NO ship_to_different_address flag, billing stays US.
- $posted_data = 'billing_country=US&billing_email=test@example.com';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test event is dispatched when billing country changes from null.
+ * Test collection when billing country changes from null.
+ *
+ * @testdox Event is collected when billing country changes from null.
*/
- public function test_event_dispatched_when_billing_country_changes_from_null(): void {
- // Mock SessionDataCollector to return null for current billing country.
- $this->mock_session_data_collector
+ public function test_collection_when_billing_country_changes_from_null(): void {
+ $this->mock_collector
->method( 'get_current_billing_country' )
- ->willReturn( null ); // No current billing country.
+ ->willReturn( null );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
->willReturn( null );
- // Expect event to be dispatched.
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' );
+ ->method( 'collect' );
- // Posted data with billing country (first time setting).
$posted_data = 'billing_email=test@example.com&billing_country=US';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
}
/**
- * Test event is dispatched when user unchecks ship_to_different_address and current shipping country differs from billing.
+ * Test collection when ship_to_different_address unchecked with different countries.
*
- * Scenario: User had different shipping address with different country (e.g., shipping=CA, billing=US),
- * then unchecks "ship to different address". The effective shipping country changes from CA to US.
+ * @testdox Event is collected when ship_to_different_address unchecked with different countries.
*/
- public function test_event_dispatched_when_ship_to_different_address_unchecked_with_different_countries(): void {
- // Mock SessionDataCollector: billing=US, shipping=CA (previously different).
- $this->mock_session_data_collector
+ public function test_collection_when_ship_to_different_address_unchecked_with_different_countries(): void {
+ $this->mock_collector
->method( 'get_current_billing_country' )
->willReturn( 'US' );
- $this->mock_session_data_collector
+ $this->mock_collector
->method( 'get_current_shipping_country' )
- ->willReturn( 'CA' ); // Was different.
+ ->willReturn( 'CA' );
- // Expect event to be dispatched (shipping effectively changed from CA to US).
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'checkout_update' ),
$this->anything()
);
- // Posted data: ship_to_different_address NOT set (unchecked), billing country is US.
$posted_data = 'billing_country=US&billing_email=test@example.com';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
}
- /**
- * Test event is NOT dispatched when user unchecks ship_to_different_address but countries are already the same.
- */
- public function test_event_not_dispatched_when_ship_to_different_address_unchecked_with_same_countries(): void {
- // Mock SessionDataCollector: billing=US, shipping=US (already same).
- $this->mock_session_data_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'US' );
-
- $this->mock_session_data_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( 'US' ); // Same as billing.
-
- // Expect event to NOT be dispatched (no effective change).
- $this->mock_dispatcher
- ->expects( $this->never() )
- ->method( 'dispatch_event' );
-
- // Posted data: ship_to_different_address NOT set, billing country is US.
- $posted_data = 'billing_country=US&billing_email=test@example.com';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
+ // ========================================
+ // Order Placed Tests
+ // ========================================
/**
- * Test track_order_placed dispatches event with correct data structure.
+ * Test track order placed collects data.
+ *
+ * @testdox track_order_placed() collects session data with order details.
*/
- public function test_track_order_placed_dispatches_event(): void {
+ public function test_track_order_placed_collects_data(): void {
$order = \WC_Helper_Order::create_order();
- $this->mock_dispatcher
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'order_placed' ),
$this->callback(
@@ -513,7 +362,6 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
$this->sut->track_order_placed( $order->get_id(), $order );
- // Clean up.
$order->delete( true );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php
index 50b95db534..1039a21728 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php
@@ -54,6 +54,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test apply allow decision.
+ *
* @testdox Should apply allow decision and update session to allowed when session is not blocked.
*/
public function test_apply_allow_decision(): void {
@@ -71,6 +73,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test allow decision does not overwrite blocked session.
+ *
* @testdox Should preserve blocked session status when allow decision is received.
*
* This prevents race conditions where emptying the cart during block_session
@@ -91,6 +95,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test apply block decision.
+ *
* @testdox Should apply block decision and update session to blocked.
*/
public function test_apply_block_decision(): void {
@@ -104,6 +110,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test invalid decision defaults to allow.
+ *
* @testdox Should default to allow for invalid decision and log warning.
*/
public function test_invalid_decision_defaults_to_allow(): void {
@@ -122,6 +130,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test filter can override block to allow.
+ *
* @testdox Should allow filter to override decision from block to allow.
*/
public function test_filter_can_override_block_to_allow(): void {
@@ -147,6 +157,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test filter can override allow to block.
+ *
* @testdox Should allow filter to override decision from allow to block.
*/
public function test_filter_can_override_allow_to_block(): void {
@@ -168,6 +180,8 @@ class DecisionHandlerTest extends WC_Unit_Test_Case {
}
/**
+ * Test filter invalid return uses original decision.
+ *
* @testdox Should reject invalid filter return value and use original decision.
*/
public function test_filter_invalid_return_uses_original_decision(): void {
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionDispatcherTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionDispatcherTest.php
deleted file mode 100644
index 8771c9d4b5..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionDispatcherTest.php
+++ /dev/null
@@ -1,329 +0,0 @@
-<?php
-/**
- * FraudProtectionDispatcherTest class file.
- *
- * @package WooCommerce\Tests
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\ApiClient;
-use Automattic\WooCommerce\Internal\FraudProtection\DecisionHandler;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
-
-/**
- * Tests for FraudProtectionDispatcher.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher
- */
-class FraudProtectionDispatcherTest extends \WC_Unit_Test_Case {
-
- /**
- * The system under test.
- *
- * @var FraudProtectionDispatcher
- */
- private $sut;
-
- /**
- * Mock API client.
- *
- * @var ApiClient|\PHPUnit\Framework\MockObject\MockObject
- */
- private $api_client_mock;
-
- /**
- * Mock decision handler.
- *
- * @var DecisionHandler|\PHPUnit\Framework\MockObject\MockObject
- */
- private $decision_handler_mock;
-
- /**
- * Mock fraud protection controller.
- *
- * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
- */
- private $controller_mock;
-
- /**
- * Mock session data collector.
- *
- * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
- */
- private $data_collector_mock;
-
- /**
- * Runs before each test.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Create mocks.
- $this->api_client_mock = $this->createMock( ApiClient::class );
- $this->decision_handler_mock = $this->createMock( DecisionHandler::class );
- $this->controller_mock = $this->createMock( FraudProtectionController::class );
- $this->data_collector_mock = $this->createMock( SessionDataCollector::class );
-
- // By default, feature is enabled.
- $this->controller_mock->method( 'feature_is_enabled' )->willReturn( true );
-
- // Create dispatcher and inject mocks.
- $this->sut = new FraudProtectionDispatcher();
- $this->sut->init(
- $this->api_client_mock,
- $this->decision_handler_mock,
- $this->controller_mock,
- $this->data_collector_mock
- );
- }
-
- /**
- * Test that dispatch_event collects session data and sends event to API and applies decision.
- */
- public function test_dispatch_event_sends_to_api_and_applies_decision(): void {
- $event_type = 'test_event';
- $event_data = array(
- 'action' => 'test_action',
- 'product_id' => 123,
- );
-
- $collected_data = array(
- 'session' => array( 'session_id' => 'test-session-123' ),
- 'action' => 'test_action',
- 'product_id' => 123,
- );
-
- // Expect data collector to be called with event type and event data.
- $this->data_collector_mock
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with( $event_type, $event_data )
- ->willReturn( $collected_data );
-
- // Expect API client to be called with the collected data.
- $this->api_client_mock
- ->expects( $this->once() )
- ->method( 'send_event' )
- ->with(
- $this->equalTo( $event_type ),
- $this->callback(
- function ( $data ) use ( $collected_data ) {
- // Verify the payload structure.
- $this->assertArrayHasKey( 'session', $data );
- $this->assertEquals( 'test-session-123', $data['session']['session_id'] );
- $this->assertEquals( 'test_action', $data['action'] );
- $this->assertEquals( 123, $data['product_id'] );
- return true;
- }
- )
- )
- ->willReturn( ApiClient::DECISION_ALLOW );
-
- // Expect decision handler to be called with the decision and collected data.
- $this->decision_handler_mock
- ->expects( $this->once() )
- ->method( 'apply_decision' )
- ->with( ApiClient::DECISION_ALLOW, $collected_data );
-
- // Call dispatch_event with event data.
- $this->sut->dispatch_event( $event_type, $event_data );
- }
-
- /**
- * Test that dispatch_event handles data without session gracefully.
- */
- public function test_dispatch_event_handles_missing_session_data(): void {
- $event_type = 'test_event';
- $event_data = array( 'invalid' => 'data_without_session' );
- $collected_data = array( 'invalid' => 'data_without_session' );
-
- // Expect data collector to be called.
- $this->data_collector_mock
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with( $event_type, $event_data )
- ->willReturn( $collected_data );
-
- // Expect API client to be called with the collected data.
- $this->api_client_mock
- ->expects( $this->once() )
- ->method( 'send_event' )
- ->with(
- $this->equalTo( $event_type ),
- $this->callback(
- function ( $data ) {
- // Verify the payload has the invalid key.
- $this->assertArrayHasKey( 'invalid', $data );
- $this->assertEquals( 'data_without_session', $data['invalid'] );
- // Session key should not exist or be empty.
- $this->assertFalse( isset( $data['session']['session_id'] ) || ! empty( $data['session']['session_id'] ) );
- return true;
- }
- )
- )
- ->willReturn( ApiClient::DECISION_ALLOW );
-
- // Expect decision handler to be called with collected data.
- $this->decision_handler_mock
- ->expects( $this->once() )
- ->method( 'apply_decision' )
- ->with( ApiClient::DECISION_ALLOW, $collected_data );
-
- // Call dispatch_event - should handle gracefully.
- $this->sut->dispatch_event( $event_type, $event_data );
- }
-
- /**
- * Test that dispatch_event respects block decisions.
- */
- public function test_dispatch_event_applies_block_decision(): void {
- $event_type = 'cart_item_added';
- $event_data = array(
- 'action' => 'item_added',
- 'product_id' => 456,
- );
-
- $collected_data = array(
- 'session' => array( 'session_id' => 'test' ),
- 'action' => 'item_added',
- 'product_id' => 456,
- );
-
- // Expect data collector to be called.
- $this->data_collector_mock
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with( $event_type, $event_data )
- ->willReturn( $collected_data );
-
- // API returns block decision.
- $this->api_client_mock
- ->expects( $this->once() )
- ->method( 'send_event' )
- ->with(
- $this->equalTo( $event_type ),
- $this->callback(
- function ( $data ) {
- // Verify the payload structure for cart event.
- $this->assertArrayHasKey( 'session', $data );
- $this->assertEquals( 'test', $data['session']['session_id'] );
- $this->assertEquals( 'item_added', $data['action'] );
- $this->assertEquals( 456, $data['product_id'] );
- return true;
- }
- )
- )
- ->willReturn( ApiClient::DECISION_BLOCK );
-
- // Expect decision handler to be called with block decision and collected data.
- $this->decision_handler_mock
- ->expects( $this->once() )
- ->method( 'apply_decision' )
- ->with( ApiClient::DECISION_BLOCK, $collected_data );
-
- // Call dispatch_event.
- $this->sut->dispatch_event( $event_type, $event_data );
- }
-
- /**
- * Test that dispatch_event doesn't send events when feature is disabled.
- */
- public function test_dispatch_event_skips_when_feature_disabled(): void {
- // Create fresh API and decision handler mocks that should never be called.
- $api_client_mock = $this->createMock( ApiClient::class );
- $api_client_mock->expects( $this->never() )->method( 'send_event' );
-
- $decision_handler_mock = $this->createMock( DecisionHandler::class );
- $decision_handler_mock->expects( $this->never() )->method( 'apply_decision' );
-
- // Create data collector mock that should never be called.
- $data_collector_mock = $this->createMock( SessionDataCollector::class );
- $data_collector_mock->expects( $this->never() )->method( 'collect' );
-
- // Create controller mock with feature disabled.
- $controller_mock = $this->createMock( FraudProtectionController::class );
- $controller_mock->expects( $this->once() )
- ->method( 'feature_is_enabled' )
- ->willReturn( false );
-
- // Create new dispatcher with feature disabled.
- $sut = new FraudProtectionDispatcher();
- $sut->init( $api_client_mock, $decision_handler_mock, $controller_mock, $data_collector_mock );
-
- $event_type = 'test_event';
- $event_data = array( 'product_id' => 123 );
-
- // Call dispatch_event - should bail early without calling data collector, API or decision handler.
- $sut->dispatch_event( $event_type, $event_data );
- }
-
- /**
- * Test that dispatch_event applies filter to collected data.
- */
- public function test_dispatch_event_applies_filter_to_data(): void {
- $event_type = 'test_event';
- $event_data = array(
- 'foo' => 'bar',
- );
-
- $collected_data = array(
- 'session' => array( 'session_id' => 'test' ),
- 'foo' => 'bar',
- );
-
- // Expect data collector to be called.
- $this->data_collector_mock
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with( $event_type, $event_data )
- ->willReturn( $collected_data );
-
- // Add a filter that modifies the data.
- add_filter(
- 'woocommerce_fraud_protection_event_data',
- function ( $data, $type ) use ( $event_type ) {
- $this->assertEquals( $event_type, $type );
- $data['filtered'] = true;
- return $data;
- },
- 10,
- 2
- );
-
- // Expect API client to receive the filtered data.
- $this->api_client_mock
- ->expects( $this->once() )
- ->method( 'send_event' )
- ->with(
- $this->equalTo( $event_type ),
- $this->callback(
- function ( $data ) {
- // Verify the original data is preserved.
- $this->assertArrayHasKey( 'session', $data );
- $this->assertEquals( 'test', $data['session']['session_id'] );
- $this->assertEquals( 'bar', $data['foo'] );
- // Verify the filter added the 'filtered' key.
- $this->assertArrayHasKey( 'filtered', $data );
- $this->assertTrue( $data['filtered'] );
- return true;
- }
- )
- )
- ->willReturn( ApiClient::DECISION_ALLOW );
-
- $this->decision_handler_mock
- ->expects( $this->once() )
- ->method( 'apply_decision' );
-
- // Call dispatch_event.
- $this->sut->dispatch_event( $event_type, $event_data );
-
- // Clean up filter.
- remove_all_filters( 'woocommerce_fraud_protection_event_data' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
index eb32ea0450..e508862c7a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
@@ -10,15 +10,15 @@ declare( strict_types=1 );
namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
use Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker;
-use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
+use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
/**
* Tests for the PaymentMethodEventTracker class.
+ *
+ * @covers \Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker
*/
class PaymentMethodEventTrackerTest extends \WC_Unit_Test_Case {
- use LoggerSpyTrait;
-
/**
* The System Under Test.
*
@@ -27,11 +27,11 @@ class PaymentMethodEventTrackerTest extends \WC_Unit_Test_Case {
private $sut;
/**
- * Mock fraud protection dispatcher.
+ * Mock session data collector.
*
- * @var \Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher|\PHPUnit\Framework\MockObject\MockObject
+ * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_dispatcher;
+ private $mock_collector;
/**
* Setup test.
@@ -39,52 +39,37 @@ class PaymentMethodEventTrackerTest extends \WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
- // Set jetpack_activation_source option to prevent "Cannot use bool as array" error
- // in Jetpack Connection Manager's apply_activation_source_to_args method.
- update_option( 'jetpack_activation_source', array( '', '' ) );
-
- // Enable the fraud protection feature.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
+ // Create mock.
+ $this->mock_collector = $this->createMock( SessionDataCollector::class );
- $container = wc_get_container();
- $container->reset_all_resolved();
-
- $this->sut = $container->get( PaymentMethodEventTracker::class );
+ // Create system under test with mock.
+ $this->sut = new PaymentMethodEventTracker();
+ $this->sut->init( $this->mock_collector );
}
/**
- * Test add payment method page loaded event tracking.
+ * Test add payment method page loaded collects data.
*
- * @testdox Should track add payment method page loaded event.
+ * @testdox track_add_payment_method_page_loaded() collects session data with empty event data.
*/
- public function test_track_add_payment_method_page_loaded_dispatches_event(): void {
- // Create mock dispatcher.
- $this->mock_dispatcher = $this->createMock( \Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher::class );
-
- // Create system under test with mock dispatcher.
- $sut = new PaymentMethodEventTracker();
- $sut->init( $this->mock_dispatcher );
-
- // Mock dispatcher to verify event is dispatched with empty event data.
- $this->mock_dispatcher
+ public function test_track_add_payment_method_page_loaded_collects_data(): void {
+ $this->mock_collector
->expects( $this->once() )
- ->method( 'dispatch_event' )
+ ->method( 'collect' )
->with(
$this->equalTo( 'add_payment_method_page_loaded' ),
$this->equalTo( array() )
);
- // Call the method.
- $sut->track_add_payment_method_page_loaded();
+ $this->sut->track_add_payment_method_page_loaded();
}
/**
- * Test payment method added event tracking.
+ * Test payment method added collects data.
*
- * @testdox Should track payment method added event.
+ * @testdox track_payment_method_added() collects session data with token details.
*/
- public function test_handle_payment_method_added(): void {
-
+ public function test_track_payment_method_added_collects_data(): void {
$user_id = $this->factory->user->create();
$token = new \WC_Payment_Token_CC();
@@ -97,37 +82,70 @@ class PaymentMethodEventTrackerTest extends \WC_Unit_Test_Case {
$token->set_user_id( $user_id );
$token->save();
- // Verify that the event was sent to the API with correct payload.
- $this->assertLogged(
- 'info',
- 'Sending fraud protection event: payment_method_added',
- array(
- 'source' => 'woo-fraud-protection',
- 'payload' => array(
- 'event_type' => 'payment_method_added',
- 'event_data' => array(
- 'action' => 'added',
- 'token_id' => $token->get_id(),
- 'gateway_id' => 'stripe',
- 'card_type' => 'visa',
- 'card_last4' => '4242',
- ),
- ),
- )
- );
+ $this->mock_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'payment_method_added' ),
+ $this->callback(
+ function ( $event_data ) use ( $token ) {
+ $this->assertArrayHasKey( 'action', $event_data );
+ $this->assertEquals( 'added', $event_data['action'] );
+ $this->assertArrayHasKey( 'token_id', $event_data );
+ $this->assertEquals( $token->get_id(), $event_data['token_id'] );
+ $this->assertArrayHasKey( 'token_type', $event_data );
+ $this->assertArrayHasKey( 'gateway_id', $event_data );
+ $this->assertEquals( 'stripe', $event_data['gateway_id'] );
+ $this->assertArrayHasKey( 'card_type', $event_data );
+ $this->assertEquals( 'visa', $event_data['card_type'] );
+ $this->assertArrayHasKey( 'card_last4', $event_data );
+ $this->assertEquals( '4242', $event_data['card_last4'] );
+ return true;
+ }
+ )
+ );
+
+ $this->sut->track_payment_method_added( $token->get_id(), $token );
+
+ $token->delete();
}
/**
- * Cleanup after test.
+ * Test payment method added includes expiry for CC tokens.
+ *
+ * @testdox track_payment_method_added() includes expiry info for CC tokens.
*/
- public function tearDown(): void {
- parent::tearDown();
+ public function test_track_payment_method_added_includes_expiry_for_cc_tokens(): void {
+ $user_id = $this->factory->user->create();
+
+ $token = new \WC_Payment_Token_CC();
+ $token->set_token( 'test_token_456' );
+ $token->set_gateway_id( 'stripe' );
+ $token->set_card_type( 'mastercard' );
+ $token->set_last4( '5555' );
+ $token->set_expiry_month( '06' );
+ $token->set_expiry_year( '2028' );
+ $token->set_user_id( $user_id );
+ $token->save();
+
+ $this->mock_collector
+ ->expects( $this->once() )
+ ->method( 'collect' )
+ ->with(
+ $this->equalTo( 'payment_method_added' ),
+ $this->callback(
+ function ( $event_data ) {
+ $this->assertArrayHasKey( 'expiry_month', $event_data );
+ $this->assertEquals( '06', $event_data['expiry_month'] );
+ $this->assertArrayHasKey( 'expiry_year', $event_data );
+ $this->assertEquals( '2028', $event_data['expiry_year'] );
+ return true;
+ }
+ )
+ );
- // Clean up options.
- delete_option( 'woocommerce_feature_fraud_protection_enabled' );
- delete_option( 'jetpack_activation_source' );
+ $this->sut->track_payment_method_added( $token->get_id(), $token );
- // Reset container.
- wc_get_container()->reset_all_resolved();
+ $token->delete();
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
index 1150eb8a34..f3e3d64f10 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
@@ -48,65 +48,106 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
// Disable taxes before adding products to cart.
update_option( 'woocommerce_calc_taxes', 'no' );
+
+ // Clear any existing session data before each test.
+ WC()->session->set( 'fraud_protection_collected_data', null );
+ }
+
+ /**
+ * Helper method to collect data and retrieve event from session.
+ *
+ * Events only contain: event_type, timestamp, event_data.
+ * For full data (session, customer, order, addresses), use get_collected_data().
+ *
+ * @param string|null $event_type Optional event type.
+ * @param array $event_data Optional event data.
+ * @return array The collected event data from session.
+ */
+ private function collect_and_get_event( ?string $event_type = null, array $event_data = array() ): array {
+ $this->sut->collect( $event_type, $event_data );
+ $stored_data = WC()->session->get( 'fraud_protection_collected_data' );
+ return $stored_data[0] ?? array();
}
/**
- * Test that collect() method returns properly structured nested array with 9 top-level keys.
+ * Helper method to collect data and retrieve full response via get_collected_data().
+ *
+ * Returns: wc_version, session, customer, shipping_address, billing_address, collected_events.
+ *
+ * @param string|null $event_type Optional event type.
+ * @param array $event_data Optional event data.
+ * @return array The full collected data response.
*/
- public function test_collect_returns_properly_structured_nested_array() {
- $result = $this->sut->collect();
+ private function collect_and_get_data( ?string $event_type = null, array $event_data = array() ): array {
+ $this->sut->collect( $event_type, $event_data );
+ return $this->sut->get_collected_data();
+ }
+
+ /**
+ * @testdox collect() stores properly structured event with 3 top-level keys.
+ */
+ public function test_collect_stores_properly_structured_event(): void {
+ $event = $this->collect_and_get_event();
+
+ $this->assertIsArray( $event );
+ $this->assertArrayHasKey( 'event_type', $event );
+ $this->assertArrayHasKey( 'timestamp', $event );
+ $this->assertArrayHasKey( 'event_data', $event );
+ $this->assertCount( 3, $event );
+ }
+
+ /**
+ * @testdox get_collected_data() returns properly structured response with 7 top-level keys.
+ */
+ public function test_get_collected_data_returns_properly_structured_response(): void {
+ $result = $this->collect_and_get_data();
$this->assertIsArray( $result );
- $this->assertArrayHasKey( 'event_type', $result );
- $this->assertArrayHasKey( 'timestamp', $result );
$this->assertArrayHasKey( 'wc_version', $result );
$this->assertArrayHasKey( 'session', $result );
$this->assertArrayHasKey( 'customer', $result );
$this->assertArrayHasKey( 'order', $result );
$this->assertArrayHasKey( 'shipping_address', $result );
$this->assertArrayHasKey( 'billing_address', $result );
- $this->assertArrayHasKey( 'event_data', $result );
- $this->assertCount( 9, $result );
+ $this->assertArrayHasKey( 'collected_events', $result );
+ $this->assertCount( 7, $result );
}
/**
* Test that collect() accepts event_type and event_data parameters.
*/
- public function test_collect_accepts_event_type_and_event_data_parameters() {
+ public function test_collect_accepts_event_type_and_event_data_parameters(): void {
$event_type = 'checkout_started';
$event_data = array(
'page' => 'checkout',
'source' => 'test',
);
- $result = $this->sut->collect( $event_type, $event_data );
+ $event = $this->collect_and_get_event( $event_type, $event_data );
- $this->assertEquals( $event_type, $result['event_type'] );
- $this->assertEquals( $event_data, $result['event_data'] );
+ $this->assertEquals( $event_type, $event['event_type'] );
+ $this->assertEquals( $event_data, $event['event_data'] );
}
/**
- * Test graceful degradation when session is unavailable.
+ * @testdox collect() degrades gracefully when session is unavailable.
*/
- public function test_graceful_degradation_when_session_unavailable() {
+ public function test_graceful_degradation_when_session_unavailable(): void {
// This test verifies that collect() doesn't throw exceptions even if session is unavailable.
// We can't easily simulate session being unavailable in unit tests without mocking,
- // but we can verify that calling collect() returns a valid structure.
- $result = $this->sut->collect();
+ // but we can verify that calling collect() stores valid event structure.
+ $event = $this->collect_and_get_event();
- $this->assertIsArray( $result );
- $this->assertCount( 9, $result );
- // All sections should be initialized even if session unavailable.
- $this->assertIsArray( $result['session'] );
- $this->assertIsArray( $result['customer'] );
- $this->assertIsArray( $result['order'] );
+ $this->assertIsArray( $event );
+ $this->assertCount( 3, $event );
}
/**
- * Test wc_version field is included in collected data.
+ * Test wc_version field is included in get_collected_data response.
*/
- public function test_wc_version_is_included() {
- $result = $this->sut->collect();
+ public function test_wc_version_is_included(): void {
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertEquals( WC()->version, $result['wc_version'] );
}
@@ -114,17 +155,17 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test timestamp format is UTC (gmdate format).
*/
- public function test_timestamp_format_is_utc() {
- $result = $this->sut->collect();
+ public function test_timestamp_format_is_utc(): void {
+ $event = $this->collect_and_get_event();
- $this->assertArrayHasKey( 'timestamp', $result );
- $this->assertNotEmpty( $result['timestamp'] );
+ $this->assertArrayHasKey( 'timestamp', $event );
+ $this->assertNotEmpty( $event['timestamp'] );
// Verify timestamp is in Y-m-d H:i:s format.
- $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $result['timestamp'] );
+ $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $event['timestamp'] );
// Verify timestamp is recent (within last 10 seconds).
- $timestamp = strtotime( $result['timestamp'] );
+ $timestamp = strtotime( $event['timestamp'] );
$current_time = time();
$time_difference = abs( $current_time - $timestamp );
$this->assertLessThanOrEqual( 10, $time_difference, 'Timestamp should be recent (within 10 seconds)' );
@@ -133,31 +174,35 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test that collect() uses default values when parameters not provided.
*/
- public function test_collect_uses_default_values_when_parameters_not_provided() {
- $result = $this->sut->collect();
+ public function test_collect_uses_default_values_when_parameters_not_provided(): void {
+ $event = $this->collect_and_get_event();
- $this->assertNull( $result['event_type'] );
- $this->assertEquals( array(), $result['event_data'] );
+ $this->assertNull( $event['event_type'] );
+ $this->assertEquals( array(), $event['event_data'] );
}
/**
- * Test that nested sections are initialized as arrays.
+ * @testdox Nested sections are initialized as arrays.
*/
- public function test_nested_sections_initialized_as_arrays() {
- $result = $this->sut->collect();
+ public function test_nested_sections_initialized_as_arrays(): void {
+ $result = $this->collect_and_get_data();
$this->assertIsArray( $result['session'] );
$this->assertIsArray( $result['customer'] );
$this->assertIsArray( $result['order'] );
$this->assertIsArray( $result['shipping_address'] );
$this->assertIsArray( $result['billing_address'] );
+ $this->assertIsArray( $result['collected_events'] );
+
+ $this->assertCount( 1, $result['collected_events'] );
}
/**
* Test session data includes all 6 required fields.
*/
- public function test_session_data_includes_all_required_fields() {
- $result = $this->sut->collect();
+ public function test_session_data_includes_all_required_fields(): void {
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertIsArray( $result['session'] );
$this->assertArrayHasKey( 'session_id', $result['session'] );
@@ -171,8 +216,9 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test session_id is retrieved from SessionClearanceManager.
*/
- public function test_session_id_retrieved_from_session_clearance_manager() {
- $result = $this->sut->collect();
+ public function test_session_id_retrieved_from_session_clearance_manager(): void {
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertArrayHasKey( 'session_id', $result['session'] );
// Session ID should be a string when session is available.
@@ -185,7 +231,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test email collection fallback chain for logged-in user.
*/
- public function test_email_collection_for_logged_in_user() {
+ public function test_email_collection_for_logged_in_user(): void {
// Create a test user and log them in.
$user_id = $this->factory->user->create(
array(
@@ -194,7 +240,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
);
wp_set_current_user( $user_id );
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertArrayHasKey( 'email', $result['session'] );
$this->assertEquals( 'testuser@example.com', $result['session']['email'] );
@@ -203,7 +250,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test email collection from WC_Customer when user not logged in.
*/
- public function test_email_collection_from_wc_customer() {
+ public function test_email_collection_from_wc_customer(): void {
// Ensure no user is logged in.
wp_set_current_user( 0 );
@@ -212,7 +259,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_billing_email( 'customer@example.com' );
}
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertArrayHasKey( 'email', $result['session'] );
// Email should be from customer object if available.
@@ -224,8 +272,9 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test customer data includes all 4 required fields.
*/
- public function test_customer_data_includes_all_required_fields() {
- $result = $this->sut->collect();
+ public function test_customer_data_includes_all_required_fields(): void {
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertIsArray( $result['customer'] );
$this->assertArrayHasKey( 'first_name', $result['customer'] );
@@ -237,13 +286,14 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test customer name collection from WC_Customer.
*/
- public function test_customer_name_collection_from_wc_customer() {
+ public function test_customer_name_collection_from_wc_customer(): void {
if ( isset( WC()->customer ) ) {
WC()->customer->set_billing_first_name( 'John' );
WC()->customer->set_billing_last_name( 'Doe' );
}
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertArrayHasKey( 'first_name', $result['customer'] );
$this->assertArrayHasKey( 'last_name', $result['customer'] );
@@ -257,7 +307,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test customer data fallback to session when WC_Customer not available.
*/
- public function test_customer_data_fallback_to_session() {
+ public function test_customer_data_fallback_to_session(): void {
// Ensure no user is logged in.
wp_set_current_user( 0 );
@@ -277,7 +327,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$original_customer = WC()->customer;
WC()->customer = null;
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
// Restore original customer.
WC()->customer = $original_customer;
@@ -293,7 +344,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test lifetime_order_count field exists and uses WC_Customer::get_order_count().
*/
- public function test_lifetime_order_count_for_registered_customer() {
+ public function test_lifetime_order_count_for_registered_customer(): void {
// Create a test user.
$user_id = $this->factory->user->create(
array(
@@ -310,7 +361,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_billing_last_name( 'Doe' );
WC()->customer->set_billing_email( 'customer@example.com' );
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
// Verify lifetime_order_count field exists and returns a valid integer.
// In test environment, the method returns 0 because the cache is not automatically
@@ -323,7 +375,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test graceful degradation when customer data unavailable.
*/
- public function test_graceful_degradation_when_customer_data_unavailable() {
+ public function test_graceful_degradation_when_customer_data_unavailable(): void {
// Ensure no user is logged in.
wp_set_current_user( 0 );
@@ -334,7 +386,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_billing_email( '' );
}
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
// Should return customer section with fields, even if empty/null.
$this->assertIsArray( $result['customer'] );
@@ -345,14 +398,17 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
}
/**
- * Test order data includes all required fields with proper structure.
+ * @testdox Order data includes all required fields with proper structure when order_id is provided.
*/
- public function test_order_data_includes_all_required_fields() {
- // Add a product to cart.
+ public function test_order_data_includes_all_required_fields(): void {
$product = \WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertIsArray( $result['order'] );
$this->assertArrayHasKey( 'order_id', $result['order'] );
@@ -370,13 +426,11 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
}
/**
- * Test order totals are collected from cart.
+ * @testdox Order totals are collected from cart when order_id is provided.
*/
- public function test_order_totals_collected_from_cart() {
- // Empty cart first to ensure clean state.
+ public function test_order_totals_collected_from_cart(): void {
WC()->cart->empty_cart();
- // Add a product to cart.
$product = \WC_Helper_Product::create_simple_product();
$product->set_regular_price( 50.00 );
$product->save();
@@ -384,39 +438,42 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 2 );
WC()->cart->calculate_totals();
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertArrayHasKey( 'items_total', $result['order'] );
$this->assertArrayHasKey( 'total', $result['order'] );
- // Verify items_total matches expected value.
$this->assertEquals( 100.00, $result['order']['items_total'] );
}
/**
- * Test shipping_tax_rate calculation.
+ * @testdox Shipping tax rate is calculated correctly when order_id is provided.
*/
- public function test_shipping_tax_rate_calculation() {
- // Add a product to cart.
+ public function test_shipping_tax_rate_calculation(): void {
$product = \WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertArrayHasKey( 'shipping_tax_rate', $result['order'] );
- // When shipping total is zero, shipping_tax_rate should be null.
if ( 0 === (float) $result['order']['shipping_total'] ) {
$this->assertNull( $result['order']['shipping_tax_rate'] );
}
}
/**
- * Test cart item data includes all 12 required fields.
+ * @testdox Cart item data includes all 12 required fields when order_id is provided.
*/
- public function test_cart_item_includes_all_required_fields() {
- // Empty cart first to ensure clean state.
+ public function test_cart_item_includes_all_required_fields(): void {
WC()->cart->empty_cart();
- // Add a product to cart.
$product = \WC_Helper_Product::create_simple_product();
$product->set_name( 'Test Product' );
$product->set_description( 'Test product description' );
@@ -426,7 +483,11 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 2 );
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertArrayHasKey( 'items', $result['order'] );
$this->assertIsArray( $result['order']['items'] );
@@ -446,7 +507,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertArrayHasKey( 'is_downloadable', $item );
$this->assertArrayHasKey( 'attributes', $item );
- // Verify values match product data.
$this->assertEquals( 'Test Product', $item['name'] );
$this->assertEquals( 'Test product description', $item['description'] );
$this->assertEquals( 'TEST-SKU-123', $item['sku'] );
@@ -457,7 +517,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test billing address includes all required fields.
*/
- public function test_billing_address_includes_all_required_fields() {
+ public function test_billing_address_includes_all_required_fields(): void {
// Set billing address data.
if ( isset( WC()->customer ) ) {
WC()->customer->set_billing_address_1( '123 Main St' );
@@ -468,7 +528,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_billing_postcode( '10001' );
}
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertIsArray( $result['billing_address'] );
$this->assertArrayHasKey( 'address_1', $result['billing_address'] );
@@ -492,7 +553,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
/**
* Test shipping address includes all required fields.
*/
- public function test_shipping_address_includes_all_required_fields() {
+ public function test_shipping_address_includes_all_required_fields(): void {
// Set shipping address data.
if ( isset( WC()->customer ) ) {
WC()->customer->set_shipping_address_1( '456 Oak Ave' );
@@ -503,7 +564,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_shipping_postcode( '90001' );
}
- $result = $this->sut->collect();
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
$this->assertIsArray( $result['shipping_address'] );
$this->assertArrayHasKey( 'address_1', $result['shipping_address'] );
@@ -525,50 +587,51 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
}
/**
- * Test graceful degradation when cart is empty.
+ * @testdox Order data degrades gracefully when cart is empty and order_id is provided.
*/
- public function test_graceful_degradation_when_cart_is_empty() {
- // Ensure cart is empty.
+ public function test_graceful_degradation_when_cart_is_empty(): void {
WC()->cart->empty_cart();
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
- // Order section should still exist even with empty cart.
$this->assertIsArray( $result['order'] );
$this->assertArrayHasKey( 'items', $result['order'] );
$this->assertIsArray( $result['order']['items'] );
$this->assertEmpty( $result['order']['items'] );
- // Totals should be zero or null.
$this->assertEquals( 0, $result['order']['items_total'] );
$this->assertEquals( 0, $result['order']['total'] );
}
/**
- * Test customer_id for guest users.
+ * @testdox customer_id is set to 'guest' for guest users when order_id is provided.
*/
- public function test_customer_id_for_guest_users() {
- // Ensure no user is logged in.
+ public function test_customer_id_for_guest_users(): void {
wp_set_current_user( 0 );
- // Reinitialize customer as guest (ID will be 0).
WC()->customer = new \WC_Customer( 0, true );
- // Add a product to cart.
$product = \WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertArrayHasKey( 'customer_id', $result['order'] );
$this->assertEquals( 'guest', $result['order']['customer_id'] );
}
/**
- * Test customer_id for logged-in users.
+ * @testdox customer_id is set to user ID for logged-in users when order_id is provided.
*/
- public function test_customer_id_for_logged_in_users() {
- // Create a test user and log them in.
+ public function test_customer_id_for_logged_in_users(): void {
$user_id = $this->factory->user->create(
array(
'user_email' => 'logged-in-user@example.com',
@@ -576,24 +639,25 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
);
wp_set_current_user( $user_id );
- // Reinitialize customer with logged-in user.
WC()->customer = new \WC_Customer( $user_id, true );
- // Add a product to cart.
$product = \WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertArrayHasKey( 'customer_id', $result['order'] );
$this->assertEquals( $user_id, $result['order']['customer_id'] );
}
/**
- * Test complete collect() output includes all 8 top-level sections with data.
+ * @testdox get_collected_data() output includes all 7 top-level sections with data.
*/
- public function test_complete_collect_output_includes_all_sections() {
- // Create a logged-in user.
+ public function test_complete_collect_output_includes_all_sections(): void {
$user_id = $this->factory->user->create(
array(
'user_email' => 'complete-test@example.com',
@@ -601,7 +665,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
);
wp_set_current_user( $user_id );
- // Set customer data.
if ( isset( WC()->customer ) ) {
WC()->customer->set_billing_first_name( 'Test' );
WC()->customer->set_billing_last_name( 'User' );
@@ -611,41 +674,44 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_billing_country( 'US' );
}
- // Add a product to cart.
$product = \WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
- $result = $this->sut->collect( 'checkout_started', array( 'test' => 'data' ) );
+ $order = wc_create_order();
+ $order->save();
- // Verify all 8 sections exist.
- $this->assertArrayHasKey( 'event_type', $result );
- $this->assertArrayHasKey( 'timestamp', $result );
+ $this->sut->collect( 'checkout_started', array( 'test' => 'data' ) );
+ $result = $this->sut->get_collected_data( $order->get_id() );
+
+ $this->assertArrayHasKey( 'wc_version', $result );
$this->assertArrayHasKey( 'session', $result );
$this->assertArrayHasKey( 'customer', $result );
$this->assertArrayHasKey( 'order', $result );
$this->assertArrayHasKey( 'shipping_address', $result );
$this->assertArrayHasKey( 'billing_address', $result );
- $this->assertArrayHasKey( 'event_data', $result );
+ $this->assertArrayHasKey( 'collected_events', $result );
- // Verify sections contain expected data types.
- $this->assertEquals( 'checkout_started', $result['event_type'] );
- $this->assertIsString( $result['timestamp'] );
+ $this->assertIsString( $result['wc_version'] );
$this->assertIsArray( $result['session'] );
$this->assertIsArray( $result['customer'] );
$this->assertIsArray( $result['order'] );
$this->assertIsArray( $result['shipping_address'] );
$this->assertIsArray( $result['billing_address'] );
- $this->assertEquals( array( 'test' => 'data' ), $result['event_data'] );
+ $this->assertIsArray( $result['collected_events'] );
+
+ $this->assertCount( 1, $result['collected_events'] );
+ $event = $result['collected_events'][0];
+ $this->assertEquals( 'checkout_started', $event['event_type'] );
+ $this->assertIsString( $event['timestamp'] );
+ $this->assertEquals( array( 'test' => 'data' ), $event['event_data'] );
}
/**
- * Test end-to-end data collection with full cart scenario.
+ * @testdox End-to-end data collection with full cart scenario works correctly.
*/
- public function test_end_to_end_data_collection_with_full_cart() {
- // Empty cart first.
+ public function test_end_to_end_data_collection_with_full_cart(): void {
WC()->cart->empty_cart();
- // Create logged-in user.
$user_id = $this->factory->user->create(
array(
'user_email' => 'e2e-test@example.com',
@@ -653,13 +719,11 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
);
wp_set_current_user( $user_id );
- // Create completed order for lifetime count.
- $order = wc_create_order();
- $order->set_customer_id( $user_id );
- $order->set_status( 'completed' );
- $order->save();
+ $existing_order = wc_create_order();
+ $existing_order->set_customer_id( $user_id );
+ $existing_order->set_status( 'completed' );
+ $existing_order->save();
- // Set customer data.
if ( isset( WC()->customer ) ) {
WC()->customer = new \WC_Customer( $user_id, true );
WC()->customer->set_billing_first_name( 'John' );
@@ -678,7 +742,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->customer->set_shipping_postcode( '10001' );
}
- // Add products to cart.
$product1 = \WC_Helper_Product::create_simple_product();
$product1->set_name( 'Product 1' );
$product1->set_regular_price( 100.00 );
@@ -693,94 +756,96 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product2->get_id(), 1 );
WC()->cart->calculate_totals();
- // Collect data.
- $result = $this->sut->collect( 'payment_attempt', array( 'gateway' => 'stripe' ) );
+ $new_order = wc_create_order();
+ $new_order->save();
- // Verify comprehensive data collection.
- $this->assertEquals( 'payment_attempt', $result['event_type'] );
- $this->assertNotEmpty( $result['timestamp'] );
+ $this->sut->collect( 'payment_attempt', array( 'gateway' => 'stripe' ) );
+ $result = $this->sut->get_collected_data( $new_order->get_id() );
+
+ $this->assertArrayHasKey( 'wc_version', $result );
+ $this->assertArrayHasKey( 'collected_events', $result );
+ $this->assertCount( 1, $result['collected_events'] );
+
+ $event = $result['collected_events'][0];
+
+ $this->assertEquals( 'payment_attempt', $event['event_type'] );
+ $this->assertNotEmpty( $event['timestamp'] );
- // Session data.
$this->assertNotEmpty( $result['session']['session_id'] );
$this->assertEquals( 'e2e-test@example.com', $result['session']['email'] );
- // Customer data.
$this->assertEquals( 'John', $result['customer']['first_name'] );
$this->assertEquals( 'Doe', $result['customer']['last_name'] );
- // Lifetime order count will be >= 0 (depends on WC_Customer::get_order_count() availability).
$this->assertIsInt( $result['customer']['lifetime_order_count'] );
$this->assertGreaterThanOrEqual( 0, $result['customer']['lifetime_order_count'] );
- // Order data.
$this->assertGreaterThan( 0, $result['order']['total'] );
$this->assertCount( 2, $result['order']['items'] );
- // Billing address.
$this->assertEquals( '123 Test St', $result['billing_address']['address_1'] );
$this->assertEquals( 'Test City', $result['billing_address']['city'] );
- // Shipping address.
$this->assertEquals( '456 Ship St', $result['shipping_address']['address_1'] );
$this->assertEquals( 'Ship City', $result['shipping_address']['city'] );
- // Event data.
- $this->assertEquals( array( 'gateway' => 'stripe' ), $result['event_data'] );
+ $this->assertEquals( array( 'gateway' => 'stripe' ), $event['event_data'] );
}
/**
- * Test graceful degradation across all sections.
+ * @testdox Graceful degradation across all sections when data is minimal.
*/
- public function test_graceful_degradation_across_all_sections() {
- // Ensure no user logged in.
+ public function test_graceful_degradation_across_all_sections(): void {
wp_set_current_user( 0 );
- // Reinitialize customer as guest (ID will be 0).
WC()->customer = new \WC_Customer( 0, true );
- // Empty cart.
WC()->cart->empty_cart();
- // Clear customer data.
if ( isset( WC()->customer ) ) {
WC()->customer->set_billing_first_name( '' );
WC()->customer->set_billing_last_name( '' );
WC()->customer->set_billing_email( '' );
}
- // Collect should still succeed and return valid structure.
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
- // Verify structure is intact even with minimal data.
$this->assertIsArray( $result );
- $this->assertCount( 9, $result );
+ $this->assertCount( 7, $result );
- // All sections should be arrays.
$this->assertIsArray( $result['session'] );
$this->assertIsArray( $result['customer'] );
$this->assertIsArray( $result['order'] );
$this->assertIsArray( $result['shipping_address'] );
$this->assertIsArray( $result['billing_address'] );
+ $this->assertIsArray( $result['collected_events'] );
+
+ $this->assertCount( 1, $result['collected_events'] );
- // Key fields should have appropriate defaults.
$this->assertEquals( 'guest', $result['order']['customer_id'] );
$this->assertEquals( 0, $result['customer']['lifetime_order_count'] );
$this->assertEmpty( $result['order']['items'] );
}
/**
- * Test manual triggering only (no automatic hooks).
+ * @testdox Data collection requires manual triggering (no automatic hooks).
*/
- public function test_manual_triggering_only() {
+ public function test_manual_triggering_only(): void {
// This test verifies that SessionDataCollector doesn't automatically
// hook into WooCommerce events. It should only collect data when
// collect() is explicitly called.
- // Add a product to cart (should not trigger automatic collection).
$product = \WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
- // Verify collect() must be called manually.
- $result = $this->sut->collect();
+ $order = wc_create_order();
+ $order->save();
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data( $order->get_id() );
$this->assertIsArray( $result );
$this->assertCount( 1, $result['order']['items'] );
@@ -788,4 +853,118 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
// No automatic data collection should have occurred.
// This is a design verification test - the class should not register hooks.
}
+
+ /**
+ * Test collect stores event data in session.
+ *
+ * @testdox collect() stores event data in WooCommerce session under 'fraud_protection_collected_data' key.
+ */
+ public function test_collect_stores_event_data_in_session(): void {
+ // Collect data with a specific event type.
+ $this->sut->collect( 'cart_page_loaded', array( 'source' => 'test' ) );
+
+ // Verify data was stored in session.
+ $stored_data = WC()->session->get( 'fraud_protection_collected_data' );
+
+ $this->assertIsArray( $stored_data );
+ $this->assertCount( 1, $stored_data );
+ $this->assertEquals( 'cart_page_loaded', $stored_data[0]['event_type'] );
+ $this->assertEquals( array( 'source' => 'test' ), $stored_data[0]['event_data'] );
+ }
+
+ /**
+ * Test multiple collect calls append data to session.
+ *
+ * @testdox Multiple collect() calls append data to session array, preserving event history.
+ */
+ public function test_multiple_collect_calls_append_data_to_session(): void {
+ // First collect call.
+ $this->sut->collect( 'cart_page_loaded', array() );
+
+ // Second collect call.
+ $this->sut->collect( 'checkout_page_loaded', array() );
+
+ // Third collect call.
+ $this->sut->collect( 'order_placed', array( 'order_id' => 123 ) );
+
+ // Verify all three events are stored.
+ $stored_data = WC()->session->get( 'fraud_protection_collected_data' );
+
+ $this->assertIsArray( $stored_data );
+ $this->assertCount( 3, $stored_data );
+ $this->assertEquals( 'cart_page_loaded', $stored_data[0]['event_type'] );
+ $this->assertEquals( 'checkout_page_loaded', $stored_data[1]['event_type'] );
+ $this->assertEquals( 'order_placed', $stored_data[2]['event_type'] );
+ $this->assertEquals( 123, $stored_data[2]['event_data']['order_id'] );
+ }
+
+ /**
+ * Test get_collected_data returns structure with empty collected_events when no data collected.
+ *
+ * @testdox get_collected_data() returns structure with empty collected_events when no data has been collected.
+ */
+ public function test_get_collected_data_returns_empty_collected_events_when_no_data_collected(): void {
+ $result = $this->sut->get_collected_data();
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'collected_events', $result );
+ $this->assertEmpty( $result['collected_events'] );
+ }
+
+ /**
+ * Test get_collected_data returns structure with empty collected_events when session unavailable.
+ *
+ * @testdox get_collected_data() returns structure with empty collected_events when session is unavailable.
+ */
+ public function test_get_collected_data_returns_empty_collected_events_when_session_unavailable(): void {
+ // Store original session.
+ $original_session = WC()->session;
+
+ // Set session to null to simulate unavailability.
+ WC()->session = null;
+
+ $result = $this->sut->get_collected_data();
+
+ // Restore original session.
+ WC()->session = $original_session;
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'collected_events', $result );
+ $this->assertEmpty( $result['collected_events'] );
+ }
+
+ /**
+ * @testdox get_collected_data() returns empty order array when no order_id is provided.
+ */
+ public function test_get_collected_data_returns_empty_order_when_no_order_id(): void {
+ $product = \WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+
+ $this->sut->collect();
+ $result = $this->sut->get_collected_data();
+
+ $this->assertArrayHasKey( 'order', $result );
+ $this->assertIsArray( $result['order'] );
+ $this->assertEmpty( $result['order'] );
+ }
+
+ /**
+ * @testdox get_collected_data() returns collected_events array after collect() is called.
+ */
+ public function test_get_collected_data_returns_data_after_collect(): void {
+ // Collect some data.
+ $this->sut->collect( 'cart_page_loaded', array( 'source' => 'test' ) );
+ $this->sut->collect( 'checkout_started', array( 'gateway' => 'stripe' ) );
+
+ // Get collected data using the new method.
+ $result = $this->sut->get_collected_data();
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'collected_events', $result );
+ $this->assertCount( 2, $result['collected_events'] );
+ $this->assertEquals( 'cart_page_loaded', $result['collected_events'][0]['event_type'] );
+ $this->assertEquals( array( 'source' => 'test' ), $result['collected_events'][0]['event_data'] );
+ $this->assertEquals( 'checkout_started', $result['collected_events'][1]['event_type'] );
+ $this->assertEquals( array( 'gateway' => 'stripe' ), $result['collected_events'][1]['event_data'] );
+ }
}