From e97eda12aeb5065a059b141352edc013debd3e8d Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Wed, 19 Jul 2023 11:57:03 -0700 Subject: [PATCH] Introduce grace period before asking guests to verify their email address (#39191) * Add a grace period during which email verification is not required (order pay/conf page). When the order confirmation (or payment) page is requested, we often want to ensure the visitor is associated with the order. However, this relies heavily on information stored in the user session and, depending on the payment gateway in use, this may not be dependable. Therefore, we've introduced a grace period during which no such verification will take place. * Provide a mechanism for establishing server-side filters from our E2E tests. * Make our utilities for setting up filters from E2E available in the test env. * Update guest shopper workflow to account for order conf/payment access grace period. * Tidy verbiage. * Add changefile(s) from automation for the following project(s): woocommerce * Only activate the Filter Setter (e2e utility) during e2e tests. * Coding standard fixes for E2E utility plugin. * e2e: Update locators for headings to use getByRole, add await to clearFilters * e2e: Abstract the cookie domain to work on non-localhost test sites --------- Co-authored-by: github-actions Co-authored-by: Corey McKrill <916023+coreymckrill@users.noreply.github.com> --- plugins/woocommerce/.wp-env.json | 3 +- .../changelog/fix-order-confirmation-access | 4 + .../class-wc-shortcode-checkout.php | 25 ++++++ .../tests/e2e-pw/bin/filter-setter.php | 87 +++++++++++++++++++ .../tests/e2e-pw/bin/test-env-setup.sh | 3 + .../e2e-pw/tests/shopper/checkout.spec.js | 32 ++++--- .../woocommerce/tests/e2e-pw/utils/filters.js | 56 ++++++++++++ .../tests/e2e/docker/initialize.sh | 3 + 8 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-order-confirmation-access create mode 100644 plugins/woocommerce/tests/e2e-pw/bin/filter-setter.php create mode 100644 plugins/woocommerce/tests/e2e-pw/utils/filters.js diff --git a/plugins/woocommerce/.wp-env.json b/plugins/woocommerce/.wp-env.json index a9b1dea4818..58ecc03c740 100644 --- a/plugins/woocommerce/.wp-env.json +++ b/plugins/woocommerce/.wp-env.json @@ -10,7 +10,8 @@ "ALTERNATE_WP_CRON": true }, "mappings": { - "wp-cli.yml": "./tests/wp-cli.yml" + "wp-cli.yml": "./tests/wp-cli.yml", + "wp-content/plugins/filter-setter.php": "./tests/e2e-pw/bin/filter-setter.php" }, "lifecycleScripts": { "afterStart": "./tests/e2e-pw/bin/test-env-setup.sh", diff --git a/plugins/woocommerce/changelog/fix-order-confirmation-access b/plugins/woocommerce/changelog/fix-order-confirmation-access new file mode 100644 index 00000000000..a1170f00ebe --- /dev/null +++ b/plugins/woocommerce/changelog/fix-order-confirmation-access @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Adds a grace period during which email verification will not be needed before the order confirmation (or payment) page is rendered. diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php index 28e53766b98..96903d1f747 100644 --- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php +++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php @@ -390,6 +390,31 @@ class WC_Shortcode_Checkout { return true; } + /** + * Controls the grace period within which we do not require any sort of email verification step before rendering + * the 'order received' or 'order pay' pages. + * + * To eliminate the grace period, set to zero (or to a negative value). Note that this filter is not invoked + * at all if email verification is deemed to be unnecessary (in other words, it cannot be used to force + * verification in *all* cases). + * + * @since 8.0.0 + * + * @param int $grace_period Time in seconds after an order is placed before email verification may be required. + * @param WC_Order $order The order for which this grace period is being assessed. + * @param string $context Indicates the context in which we might verify the email address. Typically 'order-pay' or 'order-received'. + */ + $verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order, $context ); + $date_created = $order->get_date_created(); + + // We do not need to verify the email address if we are within the grace period immediately following order creation. + if ( + is_a( $date_created, WC_DateTime::class ) + && time() - $date_created->getTimestamp() <= $verification_grace_period + ) { + return false; + } + $session = wc()->session; $session_email = ''; diff --git a/plugins/woocommerce/tests/e2e-pw/bin/filter-setter.php b/plugins/woocommerce/tests/e2e-pw/bin/filter-setter.php new file mode 100644 index 00000000000..515cfb5b3f4 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/bin/filter-setter.php @@ -0,0 +1,87 @@ + $spec ) { + // A priority may be specified as part of the spec, else use the default priority (10). + $priority = isset( $spec['priority'] ) && is_int( $spec['priority'] ) + ? $spec['priority'] + : 10; + + // If the spec is not an array, then it is probably intended as the literal value. + if ( ! is_array( $spec ) ) { + $value = $spec; + } elseif ( isset( $spec['value'] ) ) { + $value = $spec['value']; + } + + // If we know the value, we can establish our filter callback. + if ( isset( $value ) ) { + $callback = function () use ( $value ) { + return $value; + }; + } + + // We also support specifying a callback function. + if ( is_array( $spec ) && isset( $spec['callback'] ) && is_string( $spec['callback'] ) ) { + $callback = $spec['callback']; + } + + // Ensure we have a callback, then setup the filter. + if ( isset( $callback ) ) { + add_filter( $hook, $callback, $priority ); + } +} + diff --git a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh index 79a52b1d8ac..9fba2ee9cb2 100755 --- a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh +++ b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh @@ -10,6 +10,9 @@ wp-env run tests-cli wp theme activate twentynineteen echo -e 'Update URL structure \n' wp-env run tests-cli wp rewrite structure '/%postname%/' --hard +echo -e 'Activate Filter Setter utility plugin \n' +wp-env run tests-cli wp plugin activate filter-setter + echo -e 'Add Customer user \n' wp-env run tests-cli wp user create customer customer@woocommercecoree2etestsuite.com \ --user_pass=password \ diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js index 0bb9759cb60..6aabb57cb29 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require( '@playwright/test' ); const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; const { admin, customer } = require( '../../test-data/data' ); +const { setFilterValue, clearFilters } = require( '../../utils/filters' ); const guestEmail = 'checkout-guest@example.com'; @@ -235,9 +236,9 @@ test.describe( 'Checkout page', () => { await page.locator( 'text=Place order' ).click(); - await expect( page.locator( 'h1.entry-title' ) ).toContainText( - 'Order received' - ); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); // get order ID from the page const orderReceivedText = await page @@ -245,10 +246,20 @@ test.describe( 'Checkout page', () => { .textContent(); guestOrderId = await orderReceivedText.split( /(\s+)/ )[ 6 ].toString(); - // If we simulate a new browser context by dropping all cookies, and reload the page, the shopper should be - // prompted to complete an email validation step before they can proceed. + + // Let's simulate a new browser context (by dropping all cookies), and reload the page. This approximates a + // scenario where the server can no longer identify the shopper. However, so long as we are within the 10 minute + // grace period following initial order placement, the 'order received' page should still be rendered. await page.context().clearCookies(); await page.reload(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // Let's simulate a scenario where the 10 minute grace period has expired. This time, we expect the shopper to + // be presented with a request to verify their email address. + await setFilterValue( page, 'woocommerce_order_email_verification_grace_period', 0 ); + await page.reload(); await expect( page.locator( 'form.woocommerce-verify-email p:nth-child(3)' ) ).toContainText( /verify the email address associated with the order/ ); @@ -267,9 +278,9 @@ test.describe( 'Checkout page', () => { // However if they supply the *correct* billing email address, they should see the order received page again. await page.fill( '#email', guestEmail ); await page.locator( 'form.woocommerce-verify-email button' ).click(); - await expect( page.locator( 'h1.entry-title' ) ).toContainText( - 'Order received' - ); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); await page.goto( 'wp-login.php' ); await page.locator( 'input[name="log"]' ).fill( admin.username ); @@ -282,8 +293,8 @@ test.describe( 'Checkout page', () => { ); await expect( - page.locator( 'h2.woocommerce-order-data__heading' ) - ).toContainText( `Order #${ guestOrderId } details` ); + page.getByRole( 'heading', { name: `Order #${ guestOrderId } details` } ) + ).toBeVisible(); await expect( page.locator( '.wc-order-item-name' ) ).toContainText( simpleProductName ); @@ -296,6 +307,7 @@ test.describe( 'Checkout page', () => { await expect( page.locator( 'td.line_cost >> nth=0' ) ).toContainText( twoProductPrice ); + await clearFilters( page ); } ); test( 'allows existing customer to place order', async ( { page } ) => { diff --git a/plugins/woocommerce/tests/e2e-pw/utils/filters.js b/plugins/woocommerce/tests/e2e-pw/utils/filters.js new file mode 100644 index 00000000000..04c90a76b86 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/filters.js @@ -0,0 +1,56 @@ +const defaultConfig = require( '../playwright.config' ); +const testURL = new URL( defaultConfig.use.baseURL ); + +/** + * Request that a WordPress filter be established for the specified hook and returning the specified value. + * + * 'Under the hood', this is done by communicating to a server-side plugin via cookies. Therefore, for the server-side + * code to observe the requested filter, you may need to `page.reload()` prior to writing assertions that rely on your + * filter. + * + * @param page + * @param hook + * @param value + * @param priority + */ +export async function setFilterValue( page, hook, value, priority = 10 ) { + const context = page.context(); + const existingCookies = await context.cookies(); + let filterSpecs = {}; + + for ( const cookie of existingCookies ) { + if ( cookie.name === 'e2e-filters' ) { + filterSpecs = JSON.parse( cookie.value ); + break; + } + } + + filterSpecs[hook] = { + value: value, + priority: priority + }; + + await context.addCookies( [ { + name: 'e2e-filters', + value: JSON.stringify( filterSpecs ), + path: '/', + domain: testURL.hostname + } ] ); +} + +/** + * Clears any server-side filters setup via setFilterValue(). + * + * As with its sister function, this mechanism relies on cookies and therefore a call to `page.reload()` may be required + * before performing further assertions. + * + * @param page + */ +export async function clearFilters( page ) { + await page.context().addCookies( [ { + name: 'e2e-filters', + value: '', + path: '/', + domain: testURL.hostname + } ] ); +} diff --git a/plugins/woocommerce/tests/e2e/docker/initialize.sh b/plugins/woocommerce/tests/e2e/docker/initialize.sh index f86c50f218e..a624331ffb3 100755 --- a/plugins/woocommerce/tests/e2e/docker/initialize.sh +++ b/plugins/woocommerce/tests/e2e/docker/initialize.sh @@ -24,5 +24,8 @@ wp plugin install https://github.com/woocommerce/woocommerce-reset/zipball/trunk # install the WP Mail Logging plugin to test emails wp plugin install wp-mail-logging --activate +# Activate our Filter Setter utility. +wp plugin activate filter-setter + # initialize pretty permalinks wp rewrite structure /%postname%/