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

MC-30171: Add to Cart Form wrong Form Key in FPC #30961

Merged
merged 3 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
<element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/>
<element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/>
<element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/>
<element name="inputFormKey" type="text" selector="input[name='form_key']"/>
</section>
</sections>
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@
<waitForPageLoad stepKey="waitForPageLoad"/>
<see userInput="Your coupon was successfully applied." stepKey="seeSuccessMessage"/>
<click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/>
<!-- Cannot use waitForPageLoad as the below error message will disappear after a few seconds & waitForPageLoad will cause this test to be flaky -->
<comment userInput="BIC workaround" stepKey="waitForError"/>
<waitForText stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/>
<waitForPageLoad stepKey="waitForError"/>
<seeElementInDOM selector="{{CheckoutHeaderSection.errorMessageContainsText('The shipping method is missing. Select the shipping method and try again.')}}" stepKey="seeShippingMethodError"/>
<amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/>
<waitForPageLoad stepKey="waitForShippingPageLoad"/>
<click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
<actionGroup name="AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup">
<annotations>
<description>Assert that product page add to cart form key is different from cached value.</description>
</annotations>
<arguments>
<argument name="cachedValue" type="string"/>
</arguments>

<grabValueFrom selector="{{StorefrontProductActionSection.inputFormKey}}" stepKey="grabUpdatedValue"/>
<assertRegExp stepKey="validateCachedFormKey">
<expectedResult type="string">/\w{16}/</expectedResult>
<actualResult type="string">{{cachedValue}}</actualResult>
</assertRegExp>
<assertRegExp stepKey="validateUpdatedFormKey">
<expectedResult type="string">/\w{16}/</expectedResult>
<actualResult type="variable">grabUpdatedValue</actualResult>
</assertRegExp>
<assertNotEquals stepKey="assertFormKeyUpdated">
<expectedResult type="string">{{cachedValue}}</expectedResult>
<actualResult type="variable">grabUpdatedValue</actualResult>
</assertNotEquals>
</actionGroup>
</actionGroups>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->

<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
<test name="StorefrontCachedInputFormKeyValueUpdatedTest">
<annotations>
<features value="PageCache"/>
<stories value="FormKey"/>
<title value="Form Key value should be updated by js script"/>
<description value="Form Key value should be updated by js script"/>
<testCaseId value="MC-39300"/>
<useCaseId value="MC-30171"/>
<severity value="AVERAGE"/>
<group value="pageCache"/>
</annotations>
<before>
<!-- Create Data -->
<createData entity="SimpleProduct2" stepKey="createProduct"/>
<actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache">
<argument name="tags" value="full_page"/>
</actionGroup>
</before>
<after>
<!-- Delete data -->
<deleteData createDataKey="createProduct" stepKey="deleteProduct"/>
</after>
<actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage">
<argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/>
</actionGroup>
<grabValueFrom selector="{{StorefrontProductActionSection.inputFormKey}}" stepKey="grabCachedValue"/>
<resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/>
<resetCookie userInput="form_key" stepKey="resetFormKeyCookie"/>
<actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="reopenProductPage">
<argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/>
</actionGroup>
<actionGroup ref="AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup" stepKey="assertValueIsUpdatedByScript">
<argument name="cachedValue" value="{$grabCachedValue}"/>
</actionGroup>
</test>
</tests>
41 changes: 41 additions & 0 deletions app/code/Magento/PageCache/ViewModel/FormKeyProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\PageCache\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\PageCache\Model\Config;

/**
* Adds script to update form key from cookie after script rendering
*/
class FormKeyProvider implements ArgumentInterface
{
/**
* @var Config
*/
private $config;

/**
* @param Config $config
*/
public function __construct(
Config $config
) {
$this->config = $config;
}

/**
* Is full page cache enabled
*
* @return bool
*/
public function isFullPageCacheEnabled(): bool
{
return $this->config->isEnabled();
}
}
7 changes: 7 additions & 0 deletions app/code/Magento/PageCache/view/frontend/layout/default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
<referenceBlock name="head.components">
<block class="Magento\Framework\View\Element\Js\Components" name="pagecache_page_head_components" template="Magento_PageCache::js/components.phtml"/>
</referenceBlock>
<referenceBlock name="head.additional">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this block could be removed. Could you do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing. accidentally missed it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved logic from head to head.additional block to load asynchronously, won't remove

<block name="form_key_provider" template="Magento_PageCache::form_key_provider.phtml">
<arguments>
<argument name="form_key_provider" xsi:type="object">Magento\PageCache\ViewModel\FormKeyProvider</argument>
</arguments>
</block>
</referenceBlock>
<referenceContainer name="content">
<block class="Magento\PageCache\Block\Javascript" template="Magento_PageCache::javascript.phtml" name="pageCache" as="pageCache"/>
</referenceContainer>
Expand Down
3 changes: 2 additions & 1 deletion app/code/Magento/PageCache/view/frontend/requirejs-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ var config = {
'*': {
pageCache: 'Magento_PageCache/js/page-cache'
}
}
},
deps: ['Magento_PageCache/js/form-key-provider']
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
if ($block->getFormKeyProvider()->isFullPageCacheEnabled()): ?>
<script type="text/x-magento-init">
{
"*": {
"Magento_PageCache/js/form-key-provider": {}
}
}
</script>
<?php endif; ?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
define(function () {
'use strict';

return function () {
var formKey,
inputElements,
inputSelector = 'input[name="form_key"]';

/**
* Set form_key cookie
* @private
*/
function setFormKeyCookie(value) {
var expires,
secure,
date = new Date(),
isSecure = !!window.cookiesConfig && window.cookiesConfig.secure;

date.setTime(date.getTime() + 86400000);
expires = '; expires=' + date.toUTCString();
secure = isSecure ? '; secure' : '';

document.cookie = 'form_key=' + (value || '') + expires + secure + '; path=/';
}

/**
* Retrieves form key from cookie
* @private
*/
function getFormKeyCookie() {
var cookie,
i,
nameEQ = 'form_key=',
cookieArr = document.cookie.split(';');

for (i = 0; i < cookieArr.length; i++) {
cookie = cookieArr[i];

while (cookie.charAt(0) === ' ') {
cookie = cookie.substring(1, cookie.length);
}

if (cookie.indexOf(nameEQ) === 0) {
return cookie.substring(nameEQ.length, cookie.length);
}
}

return null;
}

/**
* Generate form key string
* @private
*/
function generateFormKeyString() {
var result = '',
length = 16,
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

while (length--) {
result += chars[Math.round(Math.random() * (chars.length - 1))];
}

return result;
}

/**
* Init form_key inputs with value
* @private
*/
function initFormKey() {
formKey = getFormKeyCookie();

if (!formKey) {
formKey = generateFormKeyString();
setFormKeyCookie(formKey);
}
inputElements = document.querySelectorAll(inputSelector);

if (inputElements.length) {
Array.prototype.forEach.call(inputElements, function (element) {
element.setAttribute('value', formKey);
});
}
}

initFormKey();
};
});
7 changes: 4 additions & 3 deletions app/code/Magento/PageCache/view/frontend/web/js/page-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ define([
'jquery',
'domReady',
'consoleLogger',
'Magento_PageCache/js/form-key-provider',
'jquery-ui-modules/widget',
'mage/cookies'
], function ($, domReady, consoleLogger) {
], function ($, domReady, consoleLogger, formKeyInit) {
'use strict';

/**
Expand Down Expand Up @@ -99,6 +100,7 @@ define([

/**
* FormKey Widget - this widget is generating from key, saves it to cookie and
* @deprecated see Magento/PageCache/view/frontend/web/js/form-key-provider.js
*/
$.widget('mage.formKey', {
options: {
Expand Down Expand Up @@ -298,8 +300,7 @@ define([
});

domReady(function () {
$('body')
.formKey();
formKeyInit();
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/

/* eslint-disable max-nested-callbacks */
define([
'jquery',
'Magento_PageCache/js/form-key-provider'
], function ($, formKeyInit) {
'use strict';

describe('Testing FormKey Provider', function () {
var inputContainer;

beforeEach(function () {
inputContainer = document.createElement('input');
inputContainer.setAttribute('value', '');
inputContainer.setAttribute('name', 'form_key');
document.querySelector('body').appendChild(inputContainer);
});

afterEach(function () {
$(inputContainer).remove();
document.cookie = 'form_key= ; expires = Thu, 01 Jan 1970 00:00:00 GMT';
});

it('sets value of input[form_key]', function () {
var expires,
date = new Date();

date.setTime(date.getTime() + 86400000);
expires = '; expires=' + date.toUTCString();
document.cookie = 'form_key=FAKE_COOKIE' + expires + '; path=/';
formKeyInit();
expect($(inputContainer).val()).toEqual('FAKE_COOKIE');
});

it('widget sets value to input[form_key] in case it empty', function () {
document.cookie = 'form_key= ; expires = Thu, 01 Jan 1970 00:00:00 GMT';
formKeyInit();
expect($(inputContainer).val()).toEqual(jasmine.any(String));
expect($(inputContainer).val().length).toEqual(16);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,6 @@ define([
expect($.mage.cookies.set).toHaveBeenCalled();
expect(inputContainer.val()).toEqual(jasmine.any(String));
});

it('widget exists on load on body', function (done) {
$(function () {
expect($('body').data('mageFormKey')).toBeDefined();
done();
});
});
});

describe('Testing PageCache Widget', function () {
Expand Down