From 430333073a0e59c996d0cac64dd023e094880e42 Mon Sep 17 00:00:00 2001 From: brent Date: Mon, 14 Oct 2024 17:08:01 -0700 Subject: [PATCH 01/12] Add basic EventsTest infra. --- .nvmrc | 1 + README.md | 8 ++--- src/inc/woocommerce-actions.php | 17 ++++++++-- tests/EventsTest.php | 58 +++++++++++++++++++++++++++++++++ tests/bootstrap.php | 3 +- 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 .nvmrc create mode 100644 tests/EventsTest.php diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bf5ad0 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +stable diff --git a/README.md b/README.md index 4ba7092..f773e15 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ As you develop your plugin, update the README.md file with detailed information ## Local Development -Use [`wp-env`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/env#readme) to run a local development environment. +1. `npm install` +2. `composer install` +3. `npx wp-env start` -- This starts a local WordPress environment available at +4. `npm test` -```bash -wp-env start -``` diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index 3e86286..1d22c28 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -4,6 +4,8 @@ namespace WPCOMSpecialProjects\SiftDecisions\WooCommerce_Actions; +use WC_Order_Item_Product; + /** * Class Events */ @@ -264,14 +266,18 @@ public static function link_session_to_user( string $session_id, string $user_id */ public static function add_to_cart( string $cart_item_key ) { $cart_item = \WC()->cart->get_cart_item( $cart_item_key ); - $product = $cart_item['data']; + $product = $cart_item['data'] ?? null; $user = wp_get_current_user(); + if ( ! $product ) { + return; + } + self::add( '$add_item_to_cart', array( - '$user_id' => $user->ID ? $user->ID : null, - '$user_email' => $user->user_email ? $user->user_email : null, + '$user_id' => $user->ID ?? null, + '$user_email' => $user->user_email ?? null, '$session_id' => \WC()->session->get_customer_unique_id(), '$item' => array( '$item_id' => $cart_item_key, @@ -350,6 +356,11 @@ public static function create_order( string $order_id, array $posted_data, \WC_O $physical_or_electronic = '$electronic'; $items = array(); foreach ( $order->get_items( 'line_item' ) as $item ) { + if ( ! $item instanceof WC_Order_Item_Product ) { + // log an error... + wc_get_logger()->error( 'Item not Product Item.' ); + continue; + } // Most of this we're basing off return value from `WC_Order_Item_Product::get_product()` as it will return the correct variation. $product = $item->get_product(); diff --git a/tests/EventsTest.php b/tests/EventsTest.php new file mode 100644 index 0000000..43d0a68 --- /dev/null +++ b/tests/EventsTest.php @@ -0,0 +1,58 @@ + in_array( $event['event'], $event_types, true ) ); + } + + + /** + * Test that the add_to_cart event is triggered. + * + * @return void + */ + public function test_add_item_to_cart() { + do_action( 'woocommerce_add_to_cart', 1 ); + $this->assertContains( 'add_item_to_cart', WPCOMSpecialProjects\SiftDecisions\WooCommerce_Actions\Events::$to_send ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 517fca2..a524895 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -31,7 +31,8 @@ * Manually load the plugin being tested. */ function _manually_load_plugin() { - require dirname( dirname( __FILE__ ) ) . '/sift-decisions.php'; + require dirname( dirname( __DIR__ ) ) . '/woocommerce/woocommerce.php'; + require dirname( __DIR__ ) . '/sift-decisions.php'; } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); From 3ec1f2bb72ad05da7a410e46ca7c79367f0c448b Mon Sep 17 00:00:00 2001 From: brent Date: Tue, 15 Oct 2024 12:51:34 -0700 Subject: [PATCH 02/12] Configure and document using xdebug --- .wp-env.json | 4 ++-- README.md | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.wp-env.json b/.wp-env.json index 0e2a7cb..078c422 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,8 +1,8 @@ { "plugins": [ "https://downloads.wordpress.org/plugin/woocommerce.9.3.3.zip", - "./bin/test-payment-gateway", - "." + ".", + "./bin/test-payment-gateway" ], "lifecycleScripts": { "afterStart": "./bin/bootstrap-woocommerce.sh" diff --git a/README.md b/README.md index f773e15..5b3fa15 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,14 @@ As you develop your plugin, update the README.md file with detailed information 3. `npx wp-env start` -- This starts a local WordPress environment available at 4. `npm test` +### Using XDEBUG + +Start environment with XDEBUG enabled: `npx wp-env start --xdebug`. Configure your IDE to listen for XDEBUG connections on port 9003. When you browse in the browser, XDEBUG should connect to your IDE. + +To get tests working with XDEBUG, it requires a little more work. Configure your IDE server name to be something like `XDEBUG_OMATTIC` and then launch the tests by running `npx wp-env run tests-cli --env-cwd=wp-content/plugins/sift-decisions bash`. At the new prompt you need to run: `PHP_IDE_CONFIG=serverName=XDEBUG_OMATTIC vendor/bin/phpunit`. + +### Troubleshooting: + +#### `Error response from daemon: error while creating mount source path` + +Restart docker. From 50882c16c8487f7693a54f9c51a0c4059957538f Mon Sep 17 00:00:00 2001 From: brent Date: Tue, 15 Oct 2024 15:47:30 -0700 Subject: [PATCH 03/12] Add item to cart test. --- src/inc/woocommerce-actions.php | 14 ++++++-- tests/EventsTest.php | 63 ++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index 1d22c28..6ee91e1 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -266,6 +266,7 @@ public static function link_session_to_user( string $session_id, string $user_id */ public static function add_to_cart( string $cart_item_key ) { $cart_item = \WC()->cart->get_cart_item( $cart_item_key ); + /** @var \WC_Abstract_Legacy_Product $product */ $product = $cart_item['data'] ?? null; $user = wp_get_current_user(); @@ -273,6 +274,15 @@ public static function add_to_cart( string $cart_item_key ) { return; } + // Generate the category from the product category ids. + // - SIFT wants a single string so we'll sort them and implode them. + $categories = array(); + foreach ( $product->get_category_ids() as $category_id ) { + $categories[] = get_term( $category_id )->name; + } + sort( $categories, SORT_STRING ); + $category = implode( ', ', $categories ); + self::add( '$add_item_to_cart', array( @@ -286,8 +296,8 @@ public static function add_to_cart( string $cart_item_key ) { '$price' => $product->get_price() * 1000000, // $39.99 '$currency_code' => get_woocommerce_currency(), '$quantity' => $cart_item['quantity'], - '$category' => $product->get_categories(), - '$tags' => wp_list_pluck( get_the_terms( $product->ID, 'product_tag' ), 'name' ), + '$category' => $category, + '$tags' => wp_list_pluck( get_the_terms( $product->get_id(), 'product_tag' ), 'name' ), ), '$browser' => self::get_client_browser(), '$site_domain' => wp_parse_url( site_url(), PHP_URL_HOST ), diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 43d0a68..4e8665a 100644 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -14,6 +14,51 @@ */ class EventsTest extends WP_UnitTestCase { + private static int $product_id = 0; + + /** + * Set up before class. + * + * @return void + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + // Create a product. + static::$product_id = static::create_simple_product(); + $_SERVER['HTTP_USER_AGENT'] = 'Test User Agent'; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9'; + } + + /** + * Tear down after class. + * + * @return void + */ + public static function tear_down_after_class() { + // Delete the product. + wc_get_product( static::$product_id )->delete( true ); + parent::tear_down_after_class(); + } + + /** + * Create a simple product. + * + * @return integer + */ + private static function create_simple_product() { + $product = new \WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_regular_price( 10 ); + $product->set_description( 'This is a test product.' ); + $product->set_short_description( 'Short description of the test product.' ); + $product->set_sku( 'test-product' ); + $product->set_manage_stock( false ); + $product->set_stock_status( 'instock' ); + $product->save(); + + return $product->get_id(); + } + /** * Set up the test case. * @@ -34,14 +79,13 @@ public function tear_down() { parent::tear_down(); // TODO: Change the autogenerated stub } + /** + * Filter events by event type. + * + * @param array $event_types Event types to filter by. + * @return array + */ public static function filter_events( $event_types = [] ) { - //$return = []; - //foreach ( Events::$to_send as $event ) { - // if ( in_array( $event['event'], $events, true ) ) { - // $return[] = $event; - // } - //} - //return $return; return array_filter( Events::$to_send, fn ( $event ) => in_array( $event['event'], $event_types, true ) ); } @@ -52,7 +96,8 @@ public static function filter_events( $event_types = [] ) { * @return void */ public function test_add_item_to_cart() { - do_action( 'woocommerce_add_to_cart', 1 ); - $this->assertContains( 'add_item_to_cart', WPCOMSpecialProjects\SiftDecisions\WooCommerce_Actions\Events::$to_send ); + $cart = \WC()->cart; + $cart->add_to_cart( static::$product_id ); + $this->assertEquals( 1, count( static::filter_events( [ '$add_item_to_cart' ] ) ) ); } } From 18bb981a693413546bc9a6311b7c5242a330e7e2 Mon Sep 17 00:00:00 2001 From: brent Date: Wed, 16 Oct 2024 13:01:15 -0700 Subject: [PATCH 04/12] Refactor tests --- src/inc/woocommerce-actions.php | 7 ++-- tests/AddsItemToCartEventTest.php | 46 +++++++++++++++++++++++++ tests/{EventsTest.php => EventTest.php} | 23 ++++--------- 3 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 tests/AddsItemToCartEventTest.php rename tests/{EventsTest.php => EventTest.php} (78%) diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index 6ee91e1..170af89 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -276,12 +276,9 @@ public static function add_to_cart( string $cart_item_key ) { // Generate the category from the product category ids. // - SIFT wants a single string so we'll sort them and implode them. - $categories = array(); - foreach ( $product->get_category_ids() as $category_id ) { - $categories[] = get_term( $category_id )->name; - } + $categories = wp_list_pluck( get_the_terms( $product->get_id(), 'product_cat' ), 'name' ); sort( $categories, SORT_STRING ); - $category = implode( ', ', $categories ); + $category = implode( ', ', $categories ); self::add( '$add_item_to_cart', diff --git a/tests/AddsItemToCartEventTest.php b/tests/AddsItemToCartEventTest.php new file mode 100644 index 0000000..22853ee --- /dev/null +++ b/tests/AddsItemToCartEventTest.php @@ -0,0 +1,46 @@ +cart->add_to_cart( static::$product_id ); + $this->assertAddsItemToCart(); + } + + /** + * Test that the $add_item_to_cart event is triggered. + * + * @param integer $product_id Product ID. + * + * @return void + */ + public static function assertAddsItemToCart( $product_id = null ) { + $product = wc_get_product( $product_id ?? static::$product_id ); + $events = static::filter_events( [ '$add_item_to_cart' ] ); + static::assertGreaterThanOrEqual( 1, count( $events ) ); + foreach ( $events as $event ) { + if ( $event['properties']['$item']['$sku'] === $product->get_sku() ) { + return; + } + } + static::fail( 'No $add_item_to_cart event found.' ); + } +} diff --git a/tests/EventsTest.php b/tests/EventTest.php similarity index 78% rename from tests/EventsTest.php rename to tests/EventTest.php index 4e8665a..4429a0f 100644 --- a/tests/EventsTest.php +++ b/tests/EventTest.php @@ -1,6 +1,6 @@ in_array( $event['event'], $event_types, true ) ); } - - - /** - * Test that the add_to_cart event is triggered. - * - * @return void - */ - public function test_add_item_to_cart() { - $cart = \WC()->cart; - $cart->add_to_cart( static::$product_id ); - $this->assertEquals( 1, count( static::filter_events( [ '$add_item_to_cart' ] ) ) ); - } } From 4158ef4f03620d43d89d648e94a91c224bd8e2d8 Mon Sep 17 00:00:00 2001 From: brent Date: Wed, 16 Oct 2024 15:32:52 -0700 Subject: [PATCH 05/12] $create_account event --- tests/AddsItemToCartEventTest.php | 4 ++-- tests/CreateAccountEventTest.php | 40 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/CreateAccountEventTest.php diff --git a/tests/AddsItemToCartEventTest.php b/tests/AddsItemToCartEventTest.php index 22853ee..80b6783 100644 --- a/tests/AddsItemToCartEventTest.php +++ b/tests/AddsItemToCartEventTest.php @@ -22,7 +22,7 @@ class AddsItemToCartEventTest extends EventTest { */ public function test_adds_item_to_cart() { \WC()->cart->add_to_cart( static::$product_id ); - $this->assertAddsItemToCart(); + $this->assertAddsItemToCartEventTriggered(); } /** @@ -32,7 +32,7 @@ public function test_adds_item_to_cart() { * * @return void */ - public static function assertAddsItemToCart( $product_id = null ) { + public static function assertAddsItemToCartEventTriggered( $product_id = null ) { $product = wc_get_product( $product_id ?? static::$product_id ); $events = static::filter_events( [ '$add_item_to_cart' ] ); static::assertGreaterThanOrEqual( 1, count( $events ) ); diff --git a/tests/CreateAccountEventTest.php b/tests/CreateAccountEventTest.php new file mode 100644 index 0000000..50f3ff3 --- /dev/null +++ b/tests/CreateAccountEventTest.php @@ -0,0 +1,40 @@ +assertCreateAccountEventTriggered(); + + wp_delete_user( $user ); + } + + /** + * Test that the $add_item_to_cart event is triggered. + * + * @return void + */ + public static function assertCreateAccountEventTriggered() { + $events = static::filter_events( [ '$create_account' ] ); + static::assertGreaterThanOrEqual( 1, count( $events ) ); + } +} From 94d065a4cac0e2416ff204f585ef7db9414d66a0 Mon Sep 17 00:00:00 2001 From: brent Date: Wed, 16 Oct 2024 19:48:12 -0700 Subject: [PATCH 06/12] Order created event. --- src/inc/woocommerce-actions.php | 21 +++++++-------- tests/CreateOrderEventTest.php | 47 +++++++++++++++++++++++++++++++++ tests/EventTest.php | 1 + 3 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 tests/CreateOrderEventTest.php diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index 170af89..a7b1a15 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -267,19 +267,13 @@ public static function link_session_to_user( string $session_id, string $user_id public static function add_to_cart( string $cart_item_key ) { $cart_item = \WC()->cart->get_cart_item( $cart_item_key ); /** @var \WC_Abstract_Legacy_Product $product */ - $product = $cart_item['data'] ?? null; - $user = wp_get_current_user(); + $product = $cart_item['data'] ?? null; + $user = wp_get_current_user(); if ( ! $product ) { return; } - // Generate the category from the product category ids. - // - SIFT wants a single string so we'll sort them and implode them. - $categories = wp_list_pluck( get_the_terms( $product->get_id(), 'product_cat' ), 'name' ); - sort( $categories, SORT_STRING ); - $category = implode( ', ', $categories ); - self::add( '$add_item_to_cart', array( @@ -293,7 +287,7 @@ public static function add_to_cart( string $cart_item_key ) { '$price' => $product->get_price() * 1000000, // $39.99 '$currency_code' => get_woocommerce_currency(), '$quantity' => $cart_item['quantity'], - '$category' => $category, + '$category' => wc_get_product_category_list( $product->get_id() ), '$tags' => wp_list_pluck( get_the_terms( $product->get_id(), 'product_tag' ), 'name' ), ), '$browser' => self::get_client_browser(), @@ -365,11 +359,16 @@ public static function create_order( string $order_id, array $posted_data, \WC_O foreach ( $order->get_items( 'line_item' ) as $item ) { if ( ! $item instanceof WC_Order_Item_Product ) { // log an error... - wc_get_logger()->error( 'Item not Product Item.' ); + wc_get_logger()->error( sprintf( 'Item not Item Product (order: %d).', $order->get_id() ) ); continue; } // Most of this we're basing off return value from `WC_Order_Item_Product::get_product()` as it will return the correct variation. $product = $item->get_product(); + if ( empty( $product ) ) { + // log an error... + wc_get_logger()->error( sprintf( 'Product not found for order %d.', $order->get_id() ) ); + continue; + } $items[] = array( '$item_id' => $product->get_id(), @@ -378,7 +377,7 @@ public static function create_order( string $order_id, array $posted_data, \WC_O '$price' => $product->get_price() * 1000000, // $39.99 '$currency_code' => $order->get_currency(), // For the order specifically, not the whole store. '$quantity' => $item->get_quantity(), - '$category' => $product->get_categories(), + '$category' => wc_get_product_category_list( $product->get_id() ), '$tags' => wp_list_pluck( get_the_terms( $product->get_id(), 'product_tag' ), 'name' ), ); diff --git a/tests/CreateOrderEventTest.php b/tests/CreateOrderEventTest.php new file mode 100644 index 0000000..d268839 --- /dev/null +++ b/tests/CreateOrderEventTest.php @@ -0,0 +1,47 @@ + [], 10, 0 ); + add_filter( 'woocommerce_cart_needs_payment', '__return_false' ); + + // Act + WC()->cart->add_to_cart( static::$product_id ); + $co = WC_Checkout::instance(); + $co->process_checkout(); + + // Assert + static::assertCreateOrderEventTriggered(); + } + + /** + * Test that the $add_item_to_cart event is triggered. + * + * @return void + */ + public static function assertCreateOrderEventTriggered() { + $events = static::filter_events( [ '$create_order' ] ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $create_order event found' ); + } +} diff --git a/tests/EventTest.php b/tests/EventTest.php index 4429a0f..a6d10a3 100644 --- a/tests/EventTest.php +++ b/tests/EventTest.php @@ -76,6 +76,7 @@ public function set_up() { */ public function tear_down() { Events::$to_send = []; + WC()->cart->empty_cart(); parent::tear_down(); } From 2e0f73a327b99ef2f1a4cff795631d52c629f0b2 Mon Sep 17 00:00:00 2001 From: brent Date: Thu, 17 Oct 2024 11:10:50 -0700 Subject: [PATCH 07/12] add $link_session_to_user event test --- .wp-env.json | 2 +- tests/CreateAccountEventTest.php | 2 +- tests/CreateOrderEventTest.php | 2 +- tests/LinkSessionToUserEventTest.php | 60 ++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/LinkSessionToUserEventTest.php diff --git a/.wp-env.json b/.wp-env.json index 078c422..b8a2ab1 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -2,7 +2,7 @@ "plugins": [ "https://downloads.wordpress.org/plugin/woocommerce.9.3.3.zip", ".", - "./bin/test-payment-gateway" + "./bin/test-payment-gateway" ], "lifecycleScripts": { "afterStart": "./bin/bootstrap-woocommerce.sh" diff --git a/tests/CreateAccountEventTest.php b/tests/CreateAccountEventTest.php index 50f3ff3..b558e2d 100644 --- a/tests/CreateAccountEventTest.php +++ b/tests/CreateAccountEventTest.php @@ -29,7 +29,7 @@ public function test_create_account() { } /** - * Test that the $add_item_to_cart event is triggered. + * Test that the $create_account event is triggered. * * @return void */ diff --git a/tests/CreateOrderEventTest.php b/tests/CreateOrderEventTest.php index d268839..c5511a8 100644 --- a/tests/CreateOrderEventTest.php +++ b/tests/CreateOrderEventTest.php @@ -36,7 +36,7 @@ public function test_create_account() { } /** - * Test that the $add_item_to_cart event is triggered. + * Test that the $create_order event is triggered. * * @return void */ diff --git a/tests/LinkSessionToUserEventTest.php b/tests/LinkSessionToUserEventTest.php new file mode 100644 index 0000000..795e396 --- /dev/null +++ b/tests/LinkSessionToUserEventTest.php @@ -0,0 +1,60 @@ +factory()->user->create(); + wp_set_current_user( $user_id ); + // We'll trap the cookie and set it manually + $f = function ( $enabled, $name, $value) { + $_COOKIE[ $name ] = $value; + return false; + }; + add_filter( 'woocommerce_set_cookie_enabled', $f, 10, 3 ); + WC()->session->set_customer_session_cookie( true ); + + // Act + // - (re)init the session cookie (checks for user and links session to user) + WC()->session->init_session_cookie(); + + // Assert + static::assertLinkSessionToUserEvent( $user_id ); + + // Clean up + remove_filter( 'woocommerce_set_cookie_enabled', $f ); + wp_logout(); + wp_delete_user( $user_id ); + } + + /** + * Test that the $link_session_to_user event is triggered. + * + * @param integer $user_id User ID. + * + * @return void + */ + public static function assertLinkSessionToUserEvent( $user_id ) { + $events = static::filter_events( [ '$link_session_to_user' ] ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $link_session_to_user event found' ); + } +} From 308a292dced8429f8427885a85e03059c2c9a8f1 Mon Sep 17 00:00:00 2001 From: brent Date: Thu, 17 Oct 2024 15:14:00 -0700 Subject: [PATCH 08/12] add $login event test --- src/inc/woocommerce-actions.php | 30 ++++----- tests/AddsItemToCartEventTest.php | 19 +++--- tests/CreateAccountEventTest.php | 6 +- tests/CreateOrderEventTest.php | 6 +- tests/EventTest.php | 52 ++++++++++++++- tests/LinkSessionToUserEventTest.php | 8 +-- tests/LoginEventTest.php | 94 ++++++++++++++++++++++++++++ 7 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 tests/LoginEventTest.php diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index a7b1a15..96d6547 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -116,22 +116,24 @@ public static function login_failure( string $username, \WP_Error $error ) { $failure_reason = '$account_disabled'; break; default: - $failure_reason = '$' . $error->get_error_code(); + // Only other accepted failure reason is $account_suspended... We shouldn't set the failure reason. + $failure_reason = null; } - - self::add( - '$login', - array( - '$user_id' => $attempted_user->ID ? $attempted_user->ID : null, - '$login_status' => '$failure', - '$session_id' => WC()->session->get_customer_unique_id(), - '$browser' => self::get_client_browser(), // alternately, `$app` for details of the app if not a browser. - '$username' => $username, - '$failure_reason' => $failure_reason, - '$ip' => self::get_client_ip(), - '$time' => intval( 1000 * microtime( true ) ), - ) + $properties = array( + '$user_id' => $attempted_user->ID ? $attempted_user->ID : null, + '$login_status' => '$failure', + '$session_id' => WC()->session->get_customer_unique_id(), + '$browser' => self::get_client_browser(), // alternately, `$app` for details of the app if not a browser. + '$username' => $username, + '$ip' => self::get_client_ip(), + '$time' => intval( 1000 * microtime( true ) ), ); + + if ( ! empty( $failure_reason ) ) { + $properties['$failure_reason'] = $failure_reason; + } + + self::add( '$login', $properties ); } /** diff --git a/tests/AddsItemToCartEventTest.php b/tests/AddsItemToCartEventTest.php index 80b6783..1b06f2e 100644 --- a/tests/AddsItemToCartEventTest.php +++ b/tests/AddsItemToCartEventTest.php @@ -16,7 +16,7 @@ */ class AddsItemToCartEventTest extends EventTest { /** - * Test that the add_to_cart event is triggered. + * Test that the $add_item_to_cart event is triggered. * * @return void */ @@ -26,7 +26,7 @@ public function test_adds_item_to_cart() { } /** - * Test that the $add_item_to_cart event is triggered. + * Assert $add_item_to_cart event is triggered. * * @param integer $product_id Product ID. * @@ -34,13 +34,12 @@ public function test_adds_item_to_cart() { */ public static function assertAddsItemToCartEventTriggered( $product_id = null ) { $product = wc_get_product( $product_id ?? static::$product_id ); - $events = static::filter_events( [ '$add_item_to_cart' ] ); - static::assertGreaterThanOrEqual( 1, count( $events ) ); - foreach ( $events as $event ) { - if ( $event['properties']['$item']['$sku'] === $product->get_sku() ) { - return; - } - } - static::fail( 'No $add_item_to_cart event found.' ); + $events = static::filter_events( + [ + 'event' => '$add_item_to_cart', + 'properties.$item.$sku' => $product->get_sku(), + ] + ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $add_item_to_cart event found.' ); } } diff --git a/tests/CreateAccountEventTest.php b/tests/CreateAccountEventTest.php index b558e2d..03a7682 100644 --- a/tests/CreateAccountEventTest.php +++ b/tests/CreateAccountEventTest.php @@ -16,7 +16,7 @@ */ class CreateAccountEventTest extends EventTest { /** - * Test that the create account event is triggered. + * Test that the $create_account event is triggered. * * @return void */ @@ -29,12 +29,12 @@ public function test_create_account() { } /** - * Test that the $create_account event is triggered. + * Assert $create_account event is triggered. * * @return void */ public static function assertCreateAccountEventTriggered() { - $events = static::filter_events( [ '$create_account' ] ); + $events = static::filter_events( [ 'event' => '$create_account' ] ); static::assertGreaterThanOrEqual( 1, count( $events ) ); } } diff --git a/tests/CreateOrderEventTest.php b/tests/CreateOrderEventTest.php index c5511a8..3a15c00 100644 --- a/tests/CreateOrderEventTest.php +++ b/tests/CreateOrderEventTest.php @@ -16,7 +16,7 @@ */ class CreateOrderEventTest extends EventTest { /** - * Test that the create account event is triggered. + * Test that the $create_order event is triggered. * * @return void */ @@ -36,12 +36,12 @@ public function test_create_account() { } /** - * Test that the $create_order event is triggered. + * Assert $create_order event is triggered. * * @return void */ public static function assertCreateOrderEventTriggered() { - $events = static::filter_events( [ '$create_order' ] ); + $events = static::filter_events( [ 'event' => '$create_order' ] ); static::assertGreaterThanOrEqual( 1, count( $events ), 'No $create_order event found' ); } } diff --git a/tests/EventTest.php b/tests/EventTest.php index a6d10a3..879487d 100644 --- a/tests/EventTest.php +++ b/tests/EventTest.php @@ -59,6 +59,28 @@ private static function create_simple_product() { return $product->get_id(); } + /** + * Flatten an array to dot notation. + * + * E.g. ['key' => ['subkey' => 'value']] => ['key.subkey' => 'value'] + * + * @param mixed $multidimensional_array Arbitrary array (most likely a SIFT event). + * + * @return array + */ + private static function array_dot( mixed $multidimensional_array ) { + $flat = []; + $it = new RecursiveIteratorIterator( new RecursiveArrayIterator( $multidimensional_array ) ); + foreach ( $it as $leaf ) { + $keys = []; + foreach ( range( 0, $it->getDepth() ) as $depth ) { + $keys[] = $it->getSubIterator( $depth )->key(); + } + $flat[ implode( '.', $keys ) ] = $leaf; + } + return $flat; + } + /** * Set up the test case. * @@ -83,11 +105,35 @@ public function tear_down() { /** * Filter events by event type. * - * @param array $event_types Event types to filter by. + * @param array $filters Associative array for filtering. + * + * @return generator + */ + public static function filter_events_gen( $filters = [] ) { + foreach ( Events::$to_send as $event ) { + $match = true; + // flatten the keys to dot notation (e.g. 'key.subkey.subsubkey' => 'value') + $event = self::array_dot( $event ); + foreach ( $filters as $key => $value ) { + if ( ! isset( $event[ $key ] ) || $event[ $key ] !== $value ) { + $match = false; + break; + } + } + if ( $match ) { + yield $event; + } + } + } + + /** + * Filter events by event type. + * + * @param array $filters Associative array for filtering. * * @return array */ - public static function filter_events( $event_types = [] ) { - return array_filter( Events::$to_send, fn ( $event ) => in_array( $event['event'], $event_types, true ) ); + public static function filter_events( $filters = [] ) { + return iterator_to_array( static::filter_events_gen( $filters ) ); } } diff --git a/tests/LinkSessionToUserEventTest.php b/tests/LinkSessionToUserEventTest.php index 795e396..899daf6 100644 --- a/tests/LinkSessionToUserEventTest.php +++ b/tests/LinkSessionToUserEventTest.php @@ -16,7 +16,7 @@ */ class LinkSessionToUserEventTest extends EventTest { /** - * Test that the create account event is triggered. + * Test that the $link_session_to_user event is triggered. * * @return void */ @@ -26,7 +26,7 @@ public function test_link_session_to_user_event() { $user_id = $this->factory()->user->create(); wp_set_current_user( $user_id ); // We'll trap the cookie and set it manually - $f = function ( $enabled, $name, $value) { + $f = function ( $enabled, $name, $value ) { $_COOKIE[ $name ] = $value; return false; }; @@ -47,14 +47,14 @@ public function test_link_session_to_user_event() { } /** - * Test that the $link_session_to_user event is triggered. + * Assert $link_session_to_user event is triggered. * * @param integer $user_id User ID. * * @return void */ public static function assertLinkSessionToUserEvent( $user_id ) { - $events = static::filter_events( [ '$link_session_to_user' ] ); + $events = static::filter_events( [ 'event' => '$link_session_to_user' ] ); static::assertGreaterThanOrEqual( 1, count( $events ), 'No $link_session_to_user event found' ); } } diff --git a/tests/LoginEventTest.php b/tests/LoginEventTest.php new file mode 100644 index 0000000..39cd0e7 --- /dev/null +++ b/tests/LoginEventTest.php @@ -0,0 +1,94 @@ +factory()->user->create(); + // phpcs:ignore + $auth_func = fn() => get_user_by( 'ID', $user_id ); + add_filter( 'authenticate', $auth_func, 10, 0 ); + + // Act + wp_signon( + [ + 'user_login' => get_userdata( $user_id )->user_login, + 'user_password' => 'password', + ] + ); + + // Assert + static::assertLoginEvent( '$success', $user_id ); + + // Clean up + remove_filter( 'authenticate', $auth_func ); + wp_delete_user( $user_id ); + } + + /** + * Test that the $login event is triggered (with failure). + * + * @return void + */ + public function test_login_failure() { + // Arrange + // - create a user and log them in + $user_id = $this->factory()->user->create(); + // phpcs:ignore + $auth_func = fn() => false; + add_filter( 'authenticate', $auth_func, 99, 0 ); + + // Act + wp_signon( + [ + 'user_login' => get_userdata( $user_id )->user_login, + 'user_password' => 'password', + ] + ); + + // Assert + static::assertLoginEvent( '$failure', $user_id ); + + // Clean up + remove_filter( 'authenticate', $auth_func, 99 ); + wp_delete_user( $user_id ); + } + + /** + * Assert $login event is triggered. + * + * @param string $login_status Event type ($success|$failure). + * @param integer $user_id User ID. + * + * @return void + */ + public static function assertLoginEvent( $login_status, $user_id ) { + $events = static::filter_events( + [ + 'event' => '$login', + 'properties.$login_status' => $login_status, + 'properties.$user_id' => $user_id, + ] + ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $login event found' ); + } +} From cfd2ea483a0c257ee4e1acf8d34296909ae95231 Mon Sep 17 00:00:00 2001 From: brent Date: Thu, 17 Oct 2024 16:08:39 -0700 Subject: [PATCH 09/12] add $remove_item_from_cart event test --- src/inc/woocommerce-actions.php | 4 +- tests/RemoveItemFromCartEventTest.php | 59 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/RemoveItemFromCartEventTest.php diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index 96d6547..6c2a9d7 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -329,8 +329,8 @@ public static function remove_from_cart( string $cart_item_key, \WC_Cart $cart ) '$price' => $product->get_price() * 1000000, // $39.99 '$currency_code' => get_woocommerce_currency(), '$quantity' => $cart_item['quantity'], - '$category' => $product->get_categories(), - '$tags' => wp_list_pluck( get_the_terms( $product->ID, 'product_tag' ), 'name' ), + '$category' => wc_get_product_category_list( $product->get_id() ), + '$tags' => wp_list_pluck( get_the_terms( $product->get_id(), 'product_tag' ), 'name' ), ), '$browser' => self::get_client_browser(), '$site_domain' => wp_parse_url( site_url(), PHP_URL_HOST ), diff --git a/tests/RemoveItemFromCartEventTest.php b/tests/RemoveItemFromCartEventTest.php new file mode 100644 index 0000000..3bb1d9b --- /dev/null +++ b/tests/RemoveItemFromCartEventTest.php @@ -0,0 +1,59 @@ +cart->add_to_cart( static::$product_id ); + \WC()->cart->remove_cart_item( $cart_item_key ); + + // Assert + $this->assertAddsItemToCartEventTriggered(); + + // Clean up + remove_action( 'woocommerce_add_to_cart', $grab_cart_key_func ); + } + + /** + * Assert $add_item_to_cart event is triggered. + * + * @param integer $product_id Product ID. + * + * @return void + */ + public static function assertAddsItemToCartEventTriggered( $product_id = null ) { + $product = wc_get_product( $product_id ?? static::$product_id ); + $events = static::filter_events( + [ + 'event' => '$remove_item_from_cart', + 'properties.$item.$sku' => $product->get_sku(), + ] + ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $remove_item_from_cart event found.' ); + } +} From 1a77c46cce08482112ea93bc40d60b88be222a1d Mon Sep 17 00:00:00 2001 From: brent Date: Thu, 17 Oct 2024 16:17:15 -0700 Subject: [PATCH 10/12] Pin the SIFT API version. --- src/sift-decisions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sift-decisions.php b/src/sift-decisions.php index 51ec410..207c7bc 100644 --- a/src/sift-decisions.php +++ b/src/sift-decisions.php @@ -121,6 +121,7 @@ public static function get_api_client() { array( 'api_key' => $api_key, 'account_id' => $account_id, + 'version' => '205', // Hardcode this so new sift versions don't break us. ) ); } From ef120b28b52f6f57274cf511ac336b7c98810de2 Mon Sep 17 00:00:00 2001 From: brent Date: Thu, 17 Oct 2024 16:43:16 -0700 Subject: [PATCH 11/12] add $update_account event test --- tests/UpdateAccountEventTest.php | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/UpdateAccountEventTest.php diff --git a/tests/UpdateAccountEventTest.php b/tests/UpdateAccountEventTest.php new file mode 100644 index 0000000..b60cc85 --- /dev/null +++ b/tests/UpdateAccountEventTest.php @@ -0,0 +1,62 @@ +factory()->user->create(); + $user = get_user_by( 'ID', $user_id ); + + // Act + // - update the user + wp_insert_user( + [ + 'ID' => $user->ID, + 'user_login' => $user->user_login, + 'display_name' => 'John Doe', + ] + ); + + // Assert + static::assertUpdateAccountEvent( $user_id ); + + // Clean up + wp_delete_user( $user_id ); + } + + /** + * Assert $update_account event is triggered. + * + * @param integer $user_id User ID. + * + * @return void + */ + public static function assertUpdateAccountEvent( $user_id ) { + $events = static::filter_events( + [ + 'event' => '$update_account', + 'properties.$user_id' => $user_id, + ] + ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $update_account event found' ); + } +} From bfda5c69e2600034ffd862f0cc928ad49e2273dd Mon Sep 17 00:00:00 2001 From: brent Date: Fri, 18 Oct 2024 11:35:08 -0700 Subject: [PATCH 12/12] add $update_password event test --- src/inc/woocommerce-actions.php | 13 ++- tests/UpdateAccountEventTest.php | 18 ++++ tests/UpdatePasswordEventTest.php | 138 ++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 tests/UpdatePasswordEventTest.php diff --git a/src/inc/woocommerce-actions.php b/src/inc/woocommerce-actions.php index 6c2a9d7..75edf19 100644 --- a/src/inc/woocommerce-actions.php +++ b/src/inc/woocommerce-actions.php @@ -22,7 +22,7 @@ public static function hooks() { add_action( 'wp_login', array( static::class, 'login_success' ), 100, 2 ); add_action( 'wp_login_failed', array( static::class, 'login_failure' ), 100, 2 ); add_action( 'user_register', array( static::class, 'create_account' ), 100 ); - add_action( 'profile_update', array( static::class, 'update_account' ), 100 ); + add_action( 'profile_update', array( static::class, 'update_account' ), 100, 3 ); add_action( 'wp_set_password', array( static::class, 'update_password' ), 100, 2 ); add_action( 'woocommerce_add_to_cart', array( static::class, 'add_to_cart' ), 100 ); add_action( 'woocommerce_remove_cart_item', array( static::class, 'remove_from_cart' ), 100, 2 ); @@ -175,13 +175,20 @@ public static function create_account( string $user_id ) { * * @link https://developers.sift.com/docs/curl/events-api/reserved-events/update-account * - * @param string $user_id User's ID. + * @param string $user_id User's ID. + * @param \WP_User $old_user_data The old user data. + * @param array $new_user_data The new user data. * * @return void */ - public static function update_account( string $user_id ) { + public static function update_account( string $user_id, ?\WP_User $old_user_data = null, ?array $new_user_data = null ) { $user = get_user_by( 'id', $user_id ); + // check if the password changed + if ( ! empty( $new_user_data['user_pass'] ) && $old_user_data->user_pass !== $new_user_data['user_pass'] ) { + self::update_password( '', $user_id ); + } + self::add( '$update_account', array( diff --git a/tests/UpdateAccountEventTest.php b/tests/UpdateAccountEventTest.php index b60cc85..d5b6d4a 100644 --- a/tests/UpdateAccountEventTest.php +++ b/tests/UpdateAccountEventTest.php @@ -59,4 +59,22 @@ public static function assertUpdateAccountEvent( $user_id ) { ); static::assertGreaterThanOrEqual( 1, count( $events ), 'No $update_account event found' ); } + + + /** + * Assert $update_account event is not triggered. + * + * @param integer $user_id User ID. + * + * @return void + */ + public static function assertNoUpdateAccountEvent( $user_id ) { + $events = static::filter_events( + [ + 'event' => '$update_account', + 'properties.$user_id' => $user_id, + ] + ); + static::assertEquals( 0, count( $events ), '$update_account event found' ); + } } diff --git a/tests/UpdatePasswordEventTest.php b/tests/UpdatePasswordEventTest.php new file mode 100644 index 0000000..1298b4c --- /dev/null +++ b/tests/UpdatePasswordEventTest.php @@ -0,0 +1,138 @@ +factory()->user->create(); + $user = get_user_by( 'ID', $user_id ); + + // Act + // - update the user + $password = wp_generate_password(); + wp_set_password( $password, $user_id ); + + // Assert + UpdateAccountEventTest::assertNoUpdateAccountEvent( $user_id ); + static::assertUpdatePasswordEvent( $user_id ); + + // Clean up + wp_delete_user( $user_id ); + } + + /** + * Test that the $update_password event is triggered. + * + * @return void + */ + public function test_update_password_event() { + // Arrange + // - create a user + $user_id = $this->factory()->user->create(); + $user = get_user_by( 'ID', $user_id ); + + // Act + // - update the user + $password = wp_generate_password(); + // wp_insert_user() will not hash the password on an update, so we'll use wp_update_user(). + wp_update_user( + [ + 'ID' => $user->ID, + 'user_login' => $user->user_login, + 'user_pass' => $password, + ] + ); + + // Assert + // Might as well 😆 (currently only testing wp_insert_user() so this adds another check). + UpdateAccountEventTest::assertUpdateAccountEvent( $user_id ); + static::assertUpdatePasswordEvent( $user_id ); + + // Clean up + wp_delete_user( $user_id ); + } + + /** + * Test that the $update_password event is NOT triggered. + * + * @return void + */ + public function test_no_update_password_event() { + // Arrange + // - create a user + $user_id = $this->factory()->user->create(); + $user = get_user_by( 'ID', $user_id ); + + // Act + // - update the user + $password = wp_generate_password(); + // wp_insert_user() will not hash the password on an update, so we'll use wp_update_user(). + wp_update_user( + [ + 'ID' => $user->ID, + 'user_login' => $user->user_login, + ] + ); + + // Assert + // Might as well 😆 (currently only testing wp_insert_user() so this adds another check). + UpdateAccountEventTest::assertUpdateAccountEvent( $user_id ); + static::assertNoUpdatePasswordEvent( $user_id ); + + // Clean up + wp_delete_user( $user_id ); + } + + /** + * Assert $update_password event is triggered. + * + * @param integer $user_id User ID. + * + * @return void + */ + public static function assertUpdatePasswordEvent( $user_id ) { + $events = static::filter_events( + [ + 'event' => '$update_password', + 'properties.$user_id' => $user_id, + ] + ); + static::assertGreaterThanOrEqual( 1, count( $events ), 'No $update_password event found' ); + } + + /** + * Assert $update_password event is triggered. + * + * @param integer $user_id User ID. + * + * @return void + */ + public static function assertNoUpdatePasswordEvent( $user_id ) { + $events = static::filter_events( + [ + 'event' => '$update_password', + 'properties.$user_id' => $user_id, + ] + ); + static::assertEquals( 0, count( $events ), '$update_password event found' ); + } +}