Skip to content

Commit

Permalink
Introduce grace period before asking guests to verify their email add…
Browse files Browse the repository at this point in the history
…ress (#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 <github-actions@github.com>
Co-authored-by: Corey McKrill <916023+coreymckrill@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 19, 2023
1 parent 032beaa commit e97eda1
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 11 deletions.
3 changes: 2 additions & 1 deletion plugins/woocommerce/.wp-env.json
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions 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.
Expand Up @@ -390,6 +390,31 @@ private static function guest_should_verify_email( WC_Order $order, string $cont
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 = '';

Expand Down
87 changes: 87 additions & 0 deletions plugins/woocommerce/tests/e2e-pw/bin/filter-setter.php
@@ -0,0 +1,87 @@
<?php
/**
* Plugin name: Filter Setter
* Description: Utility intended to be used during E2E testing, to make it easy to setup WordPress filters.
*
* Intended to function as a (mu-)plugin while tests are running, this code works by inspecting the current cookie
* for an entry called 'e2e-filters', which is expected to be a JSON description of filter hooks and the values we want
* to set via those filters. For example, given the JSON (pretty printed here for clarity):
*
* {
* "wooocommerce_system_timeout": 10
* }
*
* Then a filter will be added that returns 10 when 'woocommerce_system_timeout' is invoked. Or, given:
*
* {
* "woocommerce_enable_deathray": {
* "callback": "__return_false"
* }
* }
*
* Then the `__return_false()` convenience function will be set up in relation to filter hook
* 'woocommerce_enable_deathray'. Additionally, priorities can be specified. Example:
*
* {
* "woocommerce_enable_deathray": {
* "callback": "__return_false",
* "priority": 20
* }
* }
*
* Priorities can also be used in combination with literal values. For example:
*
* {
* "woocommerce_default_username": {
* "value": "Geoffrey",
* "priority": 20
* }
* }
*
* It hopefully goes without saying, this should not be used in a production environment.
*
* @package Automattic\WooCommerce\E2EPlaywright
*/

if ( ! isset( $_COOKIE ) || ! isset( $_COOKIE['e2e-filters'] ) ) {
return;
}

// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$filters = json_decode( $_COOKIE['e2e-filters'], true );

if ( ! is_array( $filters ) ) {
return;
}

foreach ( $filters as $hook => $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 );
}
}

3 changes: 3 additions & 0 deletions plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh
Expand Up @@ -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 \
Expand Down
32 changes: 22 additions & 10 deletions 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';

Expand Down Expand Up @@ -235,20 +236,30 @@ 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
.locator( '.woocommerce-order-overview__order.order' )
.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/
);
Expand All @@ -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 );
Expand All @@ -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
);
Expand All @@ -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 } ) => {
Expand Down
56 changes: 56 additions & 0 deletions 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
} ] );
}
3 changes: 3 additions & 0 deletions plugins/woocommerce/tests/e2e/docker/initialize.sh
Expand Up @@ -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%/

0 comments on commit e97eda1

Please sign in to comment.