Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce grace period before asking guests to verify their email address #39191

Merged
merged 11 commits into from Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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/mu-plugins/filter-setter.php": "./tests/e2e-pw/bin/filter-setter.php"
barryhughes marked this conversation as resolved.
Show resolved Hide resolved
},
"lifecycleScripts": {
"afterStart": "./tests/e2e-pw/bin/test-env-setup.sh",
Expand Down
@@ -1,4 +1,4 @@
<?php

Check notice on line 1 in plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php

View workflow job for this annotation

GitHub Actions / Check pull request changes to highlight

new filter found - woocommerce_order_email_verification_grace_period

\'woocommerce_order_email_verification_grace_period\' introduced in 8.0.0
/**
* Checkout Shortcode
*
Expand Down Expand Up @@ -390,6 +390,31 @@
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 );
barryhughes marked this conversation as resolved.
Show resolved Hide resolved
$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
82 changes: 82 additions & 0 deletions plugins/woocommerce/tests/e2e-pw/bin/filter-setter.php
@@ -0,0 +1,82 @@
<?php
/**
* Makes it possible to set filters from within E2E tests.
*
* 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
* }
* }
*/


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

$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 );
}
}

16 changes: 14 additions & 2 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 @@ -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.locator( 'h1.entry-title' ) ).toContainText(
'Order received'
);
coreymckrill marked this conversation as resolved.
Show resolved Hide resolved

// 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 Down Expand Up @@ -296,6 +307,7 @@ test.describe( 'Checkout page', () => {
await expect( page.locator( 'td.line_cost >> nth=0' ) ).toContainText(
twoProductPrice
);
clearFilters( page );
coreymckrill marked this conversation as resolved.
Show resolved Hide resolved
} );

test( 'allows existing customer to place order', async ( { page } ) => {
Expand Down
54 changes: 54 additions & 0 deletions plugins/woocommerce/tests/e2e-pw/utils/filters.js
@@ -0,0 +1,54 @@

/**
* 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: 'localhost'
coreymckrill marked this conversation as resolved.
Show resolved Hide resolved
} ] );
}

/**
* 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: 'localhost'
coreymckrill marked this conversation as resolved.
Show resolved Hide resolved
} ] );
}