@@ -38,8 +39,10 @@ To suggest documentation improvements, click [here][4].
|  | The pull request has been rejected and will not be merged into mainline code. Possible reasons can include but are not limited to: issue has already been fixed in another code contribution, or there is an issue with the code contribution. |
|  | The Magento Team has confirmed that this issue contains the minimum required information to reproduce. |
|  | The Magento Team has validated the issue and an internal ticket has been created. |
-|  | The internal ticket is currently in progress, fix is scheduled to be delivered. |
-|  | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. |
+|  | The internal ticket is currently in progress, fix is scheduled to be delivered. |
+|  | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. |
+
+To learn more about issue gate labels click [here](https://github.com/magento/magento2/wiki/Magento-Issue-Gates)
Reporting security issues
diff --git a/app/.htaccess b/app/.htaccess
index 93169e4eb44ff..707c26b075e16 100644
--- a/app/.htaccess
+++ b/app/.htaccess
@@ -1,2 +1,8 @@
-Order deny,allow
-Deny from all
+
+ order allow,deny
+ deny from all
+
+= 2.4>
+ Require all denied
+
+
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 6701a9f4dd51e..e77c6d432c816 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -14,12 +14,12 @@
if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) {
if (PHP_SAPI == 'cli') {
echo 'Magento supports 7.0.2, 7.0.4, and 7.0.6 or later. ' .
- 'Please read http://devdocs.magento.com/guides/v1.0/install-gde/system-requirements.html';
+ 'Please read http://devdocs.magento.com/guides/v2.2/install-gde/system-requirements.html';
} else {
echo <<
Magento supports PHP 7.0.2, 7.0.4, and 7.0.6 or later. Please read
-
+
Magento System Requirements .
HTML;
@@ -49,12 +49,17 @@
unset($_SERVER['ORIG_PATH_INFO']);
}
-if (!empty($_SERVER['MAGE_PROFILER'])
+if (
+ (!empty($_SERVER['MAGE_PROFILER']) || file_exists(BP . '/var/profiler.flag'))
&& isset($_SERVER['HTTP_ACCEPT'])
&& strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false
) {
+ $profilerFlag = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER'])
+ ? $_SERVER['MAGE_PROFILER']
+ : trim(file_get_contents(BP . '/var/profiler.flag'));
+
\Magento\Framework\Profiler::applyConfig(
- $_SERVER['MAGE_PROFILER'],
+ $profilerFlag,
BP,
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
);
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
index 6c0dfd1db7d16..94c7d955f592b 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
@@ -39,6 +39,6 @@ public function execute()
$this->messageManager->addException($e, __("We couldn't remove the messages because of an error."));
}
}
- $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*')));
+ $this->_redirect('adminhtml/*/');
}
}
diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json
index afb820a2e6c93..59a3845cbd4b7 100644
--- a/app/code/Magento/AdminNotification/composer.json
+++ b/app/code/Magento/AdminNotification/composer.json
@@ -6,12 +6,12 @@
"magento/module-store": "100.2.*",
"magento/module-backend": "100.2.*",
"magento/module-media-storage": "100.2.*",
- "magento/framework": "100.2.*",
- "magento/module-ui": "100.2.*",
+ "magento/framework": "101.0.*",
+ "magento/module-ui": "101.0.*",
"lib-libxml": "*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml
index 7778c1dd5ca98..c68313211c2e6 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml
@@ -20,14 +20,14 @@
0
-
+
Severity
severity
Magento\AdminNotification\Block\Grid\Renderer\Severity
-
+
Date Added
date_added
@@ -37,14 +37,14 @@
col-date
-
+
Message
title
Magento\AdminNotification\Block\Grid\Renderer\Notice
-
+
Actions
0
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php
index 7ddd5e3bb2a36..62a7aefa77550 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php
@@ -79,6 +79,11 @@ class AdvancedPricing extends \Magento\CatalogImportExport\Model\Export\Product
ImportAdvancedPricing::COL_TIER_PRICE_TYPE => ''
];
+ /**
+ * @var string[]
+ */
+ private $websiteCodesMap = [];
+
/**
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
* @param \Magento\Eav\Model\Config $config
@@ -213,6 +218,7 @@ public function export()
break;
}
}
+
return $writer->getContents();
}
@@ -255,70 +261,111 @@ public function filterAttributeCollection(\Magento\Eav\Model\ResourceModel\Entit
*/
protected function getExportData()
{
+ if ($this->_passTierPrice) {
+ return [];
+ }
+
$exportData = [];
try {
- $rawData = $this->collectRawData();
- $productIds = array_keys($rawData);
- if (isset($productIds)) {
- if (!$this->_passTierPrice) {
- $exportData = array_merge(
- $exportData,
- $this->getTierPrices($productIds, ImportAdvancedPricing::TABLE_TIER_PRICE)
- );
+ $productsByStores = $this->loadCollection();
+ if (!empty($productsByStores)) {
+ $productLinkIds = array_map(
+ function (array $productData) {
+ return $productData[Store::DEFAULT_STORE_ID][$this->getProductEntityLinkField()];
+ },
+ $productsByStores
+ );
+ $tierPricesData = $this->getTierPrices(
+ $productLinkIds,
+ ImportAdvancedPricing::TABLE_TIER_PRICE
+ );
+
+ $exportData = $this->correctExportData(
+ $productsByStores,
+ $tierPricesData
+ );
+ if (!empty($exportData)) {
+ asort($exportData);
}
}
- if ($exportData) {
- $exportData = $this->correctExportData($exportData);
- }
- if (isset($exportData)) {
- asort($exportData);
- }
} catch (\Exception $e) {
$this->_logger->critical($e);
}
+
return $exportData;
}
/**
- * Correct export data.
+ * @param array $tierPriceData Tier price information.
*
- * @param array $exportData
- * @return array
- * @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ * @return array Formatted for export tier price information.
*/
- protected function correctExportData($exportData)
+ private function createExportRow(array $tierPriceData): array
{
- $customExportData = [];
- foreach ($exportData as $key => $row) {
- $exportRow = $this->templateExportData;
- foreach ($exportRow as $keyTemplate => $valueTemplate) {
- if (isset($row[$keyTemplate])) {
- if (in_array($keyTemplate, $this->_priceWebsite)) {
- $exportRow[$keyTemplate] = $this->_getWebsiteCode(
- $row[$keyTemplate]
- );
- } elseif (in_array($keyTemplate, $this->_priceCustomerGroup)) {
- $exportRow[$keyTemplate] = $this->_getCustomerGroupById(
- $row[$keyTemplate],
- isset($row[ImportAdvancedPricing::VALUE_ALL_GROUPS])
- ? $row[ImportAdvancedPricing::VALUE_ALL_GROUPS]
- : null
- );
- unset($exportRow[ImportAdvancedPricing::VALUE_ALL_GROUPS]);
- } elseif ($keyTemplate === ImportAdvancedPricing::COL_TIER_PRICE) {
- $exportRow[$keyTemplate] = $row[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
- ? $row[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
- : $row[ImportAdvancedPricing::COL_TIER_PRICE];
- $exportRow[ImportAdvancedPricing::COL_TIER_PRICE_TYPE]
- = $this->tierPriceTypeValue($row[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]);
- } else {
- $exportRow[$keyTemplate] = $row[$keyTemplate];
- }
+ $exportRow = $this->templateExportData;
+ foreach (array_keys($exportRow) as $keyTemplate) {
+ if (array_key_exists($keyTemplate, $tierPriceData)) {
+ if (in_array($keyTemplate, $this->_priceWebsite)) {
+ $exportRow[$keyTemplate] = $this->_getWebsiteCode(
+ $tierPriceData[$keyTemplate]
+ );
+ } elseif (in_array($keyTemplate, $this->_priceCustomerGroup)) {
+ $exportRow[$keyTemplate] = $this->_getCustomerGroupById(
+ $tierPriceData[$keyTemplate],
+ $tierPriceData[ImportAdvancedPricing::VALUE_ALL_GROUPS]
+ );
+ unset($exportRow[ImportAdvancedPricing::VALUE_ALL_GROUPS]);
+ } elseif ($keyTemplate
+ === ImportAdvancedPricing::COL_TIER_PRICE
+ ) {
+ $exportRow[$keyTemplate]
+ = $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
+ ? $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
+ : $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE];
+ $exportRow[ImportAdvancedPricing::COL_TIER_PRICE_TYPE]
+ = $this->tierPriceTypeValue($tierPriceData);
+ } else {
+ $exportRow[$keyTemplate] = $tierPriceData[$keyTemplate];
}
}
+ }
+
+ return $exportRow;
+ }
- $customExportData[$key] = $exportRow;
- unset($exportRow);
+ /**
+ * Correct export data.
+ *
+ * @param array $productsData
+ * @param array $tierPricesData
+ *
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ */
+ protected function correctExportData(
+ array $productsData,
+ array $tierPricesData
+ ): array {
+ //Assigning SKUs to tier prices data.
+ $productLinkIdToSkuMap = [];
+ foreach ($productsData as $productData) {
+ $productLinkIdToSkuMap[$productData[Store::DEFAULT_STORE_ID][$this->getProductEntityLinkField()]]
+ = $productData[Store::DEFAULT_STORE_ID]['sku'];
+ }
+ unset($productData);
+ $linkedTierPricesData = [];
+ foreach ($tierPricesData as $tierPriceData) {
+ $sku = $productLinkIdToSkuMap[$tierPriceData['product_link_id']];
+ $linkedTierPricesData[] = array_merge(
+ $tierPriceData,
+ [ImportAdvancedPricing::COL_SKU => $sku]
+ );
+ }
+ unset($sku, $tierPriceData);
+
+ $customExportData = [];
+ foreach ($linkedTierPricesData as $row) {
+ $customExportData[] = $this->createExportRow($row);
}
return $customExportData;
@@ -327,12 +374,13 @@ protected function correctExportData($exportData)
/**
* Check type for tier price.
*
- * @param string $tierPricePercentage
+ * @param array $tierPriceData
+ *
* @return string
*/
- private function tierPriceTypeValue($tierPricePercentage)
+ private function tierPriceTypeValue(array $tierPriceData): string
{
- return $tierPricePercentage
+ return $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
? ImportAdvancedPricing::TIER_PRICE_TYPE_PERCENT
: ImportAdvancedPricing::TIER_PRICE_TYPE_FIXED;
}
@@ -340,54 +388,52 @@ private function tierPriceTypeValue($tierPricePercentage)
/**
* Get tier prices.
*
- * @param array $listSku
+ * @param string[] $productLinksIds
* @param string $table
* @return array|bool
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function getTierPrices(array $listSku, $table)
+ protected function getTierPrices(array $productLinksIds, $table)
{
+ $exportFilter = null;
+ $price = null;
if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP])) {
$exportFilter = $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP];
}
+ $productEntityLinkField = $this->getProductEntityLinkField();
+
if ($table == ImportAdvancedPricing::TABLE_TIER_PRICE) {
$selectFields = [
- ImportAdvancedPricing::COL_SKU => 'cpe.sku',
- ImportAdvancedPricing::COL_TIER_PRICE_WEBSITE => 'ap.website_id',
- ImportAdvancedPricing::VALUE_ALL_GROUPS => 'ap.all_groups',
- ImportAdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'ap.customer_group_id',
- ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty',
- ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value',
+ ImportAdvancedPricing::COL_TIER_PRICE_WEBSITE => 'ap.website_id',
+ ImportAdvancedPricing::VALUE_ALL_GROUPS => 'ap.all_groups',
+ ImportAdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'ap.customer_group_id',
+ ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty',
+ ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value',
ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE => 'ap.percentage_value',
+ 'product_link_id' => 'ap.'
+ .$productEntityLinkField,
];
- if (isset($exportFilter) && !empty($exportFilter)) {
- $price = $exportFilter['tier_price'];
- }
- }
- if ($listSku) {
- if (isset($exportFilter) && !empty($exportFilter)) {
- $date = $exportFilter[\Magento\Catalog\Model\Category::KEY_UPDATED_AT];
- if (isset($date[0]) && !empty($date[0])) {
- $updatedAtFrom = $this->_localeDate->date($date[0], null, false)->format('Y-m-d H:i:s');
- }
- if (isset($date[1]) && !empty($date[1])) {
- $updatedAtTo = $this->_localeDate->date($date[1], null, false)->format('Y-m-d H:i:s');
+ if ($exportFilter) {
+ if (array_key_exists('tier_price', $exportFilter)) {
+ $price = $exportFilter['tier_price'];
}
}
+ } else {
+ throw new \InvalidArgumentException('Proper table name needed');
+ }
+
+ if ($productLinksIds) {
try {
- $productEntityLinkField = $this->getProductEntityLinkField();
$select = $this->_connection->select()
->from(
- ['cpe' => $this->_resource->getTableName('catalog_product_entity')],
- $selectFields
- )
- ->joinInner(
['ap' => $this->_resource->getTableName($table)],
- 'ap.' . $productEntityLinkField . ' = cpe.' . $productEntityLinkField,
- []
+ $selectFields
)
- ->where('cpe.entity_id IN (?)', $listSku);
+ ->where(
+ 'ap.'.$productEntityLinkField.' IN (?)',
+ $productLinksIds
+ );
if (isset($price[0]) && !empty($price[0])) {
$select->where('ap.value >= ?', $price[0]);
@@ -398,18 +444,16 @@ protected function getTierPrices(array $listSku, $table)
if (isset($price[0]) && !empty($price[0]) || isset($price[1]) && !empty($price[1])) {
$select->orWhere('ap.percentage_value IS NOT NULL');
}
- if (isset($updatedAtFrom) && !empty($updatedAtFrom)) {
- $select->where('cpe.updated_at >= ?', $updatedAtFrom);
- }
- if (isset($updatedAtTo) && !empty($updatedAtTo)) {
- $select->where('cpe.updated_at <= ?', $updatedAtTo);
- }
+
$exportData = $this->_connection->fetchAll($select);
} catch (\Exception $e) {
return false;
}
+
+ return $exportData;
+ } else {
+ return false;
}
- return $exportData;
}
/**
@@ -418,35 +462,46 @@ protected function getTierPrices(array $listSku, $table)
* @param int $websiteId
* @return string
*/
- protected function _getWebsiteCode($websiteId)
+ protected function _getWebsiteCode(int $websiteId): string
{
- $storeName = ($websiteId == 0)
- ? ImportAdvancedPricing::VALUE_ALL_WEBSITES
- : $this->_storeManager->getWebsite($websiteId)->getCode();
- $currencyCode = '';
- if ($websiteId == 0) {
- $currencyCode = $this->_storeManager->getWebsite($websiteId)->getBaseCurrencyCode();
- }
- if ($storeName && $currencyCode) {
- return $storeName . ' [' . $currencyCode . ']';
- } else {
- return $storeName;
+ if (!array_key_exists($websiteId, $this->websiteCodesMap)) {
+ $storeName = ($websiteId == 0)
+ ? ImportAdvancedPricing::VALUE_ALL_WEBSITES
+ : $this->_storeManager->getWebsite($websiteId)->getCode();
+ $currencyCode = '';
+ if ($websiteId == 0) {
+ $currencyCode = $this->_storeManager->getWebsite($websiteId)
+ ->getBaseCurrencyCode();
+ }
+
+ if ($storeName && $currencyCode) {
+ $code = $storeName.' ['.$currencyCode.']';
+ } else {
+ $code = $storeName;
+ }
+ $this->websiteCodesMap[$websiteId] = $code;
}
+
+ return $this->websiteCodesMap[$websiteId];
}
/**
* Get Customer Group By Id
*
* @param int $customerGroupId
- * @param null $allGroups
+ * @param int $allGroups
* @return string
*/
- protected function _getCustomerGroupById($customerGroupId, $allGroups = null)
- {
- if ($allGroups) {
+ protected function _getCustomerGroupById(
+ int $customerGroupId,
+ int $allGroups = 0
+ ): string {
+ if ($allGroups !== 0) {
return ImportAdvancedPricing::VALUE_ALL_GROUPS;
} else {
- return $this->_groupRepository->getById($customerGroupId)->getCode();
+ return $this->_groupRepository
+ ->getById($customerGroupId)
+ ->getCode();
}
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
index 23829d3725119..0e8acb37104e6 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
@@ -394,7 +394,7 @@ protected function saveAndReplaceAdvancedPrices()
? $rowData[self::COL_TIER_PRICE] : 0,
'percentage_value' => $rowData[self::COL_TIER_PRICE_TYPE] === self::TIER_PRICE_TYPE_PERCENT
? $rowData[self::COL_TIER_PRICE] : null,
- 'website_id' => $this->getWebsiteId($rowData[self::COL_TIER_PRICE_WEBSITE])
+ 'website_id' => $this->getWebSiteId($rowData[self::COL_TIER_PRICE_WEBSITE])
];
}
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
index 5111b4932d7a8..9a380ff75da24 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
@@ -27,7 +27,7 @@ class WebsiteTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\WebSite::class)
+ $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\Website::class)
->setMethods(['getBaseCurrency'])
->disableOriginalConstructor()
->getMock();
diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json
index 228464ecd6304..79e6e2d368736 100644
--- a/app/code/Magento/AdvancedPricingImportExport/composer.json
+++ b/app/code/Magento/AdvancedPricingImportExport/composer.json
@@ -3,17 +3,17 @@
"description": "N/A",
"require": {
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
- "magento/module-catalog": "101.1.*",
+ "magento/module-catalog": "102.0.*",
"magento/module-catalog-inventory": "100.2.*",
- "magento/module-eav": "100.2.*",
+ "magento/module-eav": "101.0.*",
"magento/module-import-export": "100.2.*",
"magento/module-catalog-import-export": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-store": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Analytics/Api/Data/LinkInterface.php b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
new file mode 100644
index 0000000000000..6597dff868b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
@@ -0,0 +1,24 @@
+' . $element->getLabel() . '';
+ $html .= '';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ return sprintf(
+ ' ',
+ $element->getHtmlId(),
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
new file mode 100644
index 0000000000000..34f2b7d53d9be
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
@@ -0,0 +1,53 @@
+localeResolver = $localeResolver ?:
+ ObjectManager::getInstance()->get(\Magento\Framework\Locale\ResolverInterface::class);
+ parent::__construct($context, $data);
+ }
+
+ /**
+ * Add current time zone to comment, properly translated according to locale
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $timeZoneCode = $this->_localeDate->getConfigTimezone();
+ $locale = $this->localeResolver->getLocale();
+ $getLongTimeZoneName = \IntlTimeZone::createTimeZone($timeZoneCode)
+ ->getDisplayName(false, \IntlTimeZone::DISPLAY_LONG, $locale);
+ $element->setData(
+ 'comment',
+ sprintf("%s (%s)", $getLongTimeZoneName, $timeZoneCode)
+ );
+ return parent::render($element);
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
new file mode 100644
index 0000000000000..c09213c7f009d
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
@@ -0,0 +1,64 @@
+subscriptionStatusProvider = $labelStatusProvider;
+ }
+
+ /**
+ * Add Subscription status to comment
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $element->setData(
+ 'comment',
+ $this->prepareLabelValue()
+ );
+ return parent::render($element);
+ }
+
+ /**
+ * Prepare label for subscription status
+ *
+ * @return string
+ */
+ private function prepareLabelValue()
+ {
+ return __('Subscription status') . ': ' . __($this->subscriptionStatusProvider->getStatus());
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
new file mode 100644
index 0000000000000..99606e10f99d9
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
@@ -0,0 +1,41 @@
+' . $element->getHint() . '';
+ $html .= '';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * Decorates row HTML for custom element style
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ $rowHtml = sprintf('%s ', $html);
+ $rowHtml .= sprintf(
+ '%s %s ',
+ $element->getHtmlId(),
+ $element->getLabelHtml($element->getHtmlId(), "[WEBSITE]"),
+ $element->getElementHtml()
+ );
+ return $rowHtml;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
new file mode 100644
index 0000000000000..a90a971cf41b4
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
@@ -0,0 +1,64 @@
+config = $config;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::bi_essentials');
+ }
+
+ /**
+ * Provides link to BI Essentials signup
+ *
+ * @return \Magento\Framework\Controller\AbstractResult
+ */
+ public function execute()
+ {
+ return $this->resultRedirectFactory->create()->setUrl(
+ $this->config->getValue($this->urlBIEssentialsConfigPath)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
new file mode 100644
index 0000000000000..1b0e5c92420de
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
@@ -0,0 +1,75 @@
+reportUrlProvider = $reportUrlProvider;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller.
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Redirect to resource with reports.
+ *
+ * @return Redirect $resultRedirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setUrl($this->reportUrlProvider->getUrl());
+ } catch (SubscriptionUpdateException $e) {
+ $this->getMessageManager()->addNoticeMessage($e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ $resultRedirect->setPath('adminhtml');
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
new file mode 100644
index 0000000000000..122cf74123cc9
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
@@ -0,0 +1,73 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Retry process of subscription.
+ *
+ * @return Redirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setPath('adminhtml');
+ $this->subscriptionHandler->processEnabled();
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/CollectData.php b/app/code/Magento/Analytics/Cron/CollectData.php
new file mode 100644
index 0000000000000..ff0b3e4f67638
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/CollectData.php
@@ -0,0 +1,53 @@
+exportDataHandler = $exportDataHandler;
+ $this->subscriptionStatus = $subscriptionStatus;
+ }
+
+ /**
+ * @return bool
+ */
+ public function execute()
+ {
+ if ($this->subscriptionStatus->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->exportDataHandler->prepareExportData();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/SignUp.php b/app/code/Magento/Analytics/Cron/SignUp.php
new file mode 100644
index 0000000000000..c17b9b8c381c3
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/SignUp.php
@@ -0,0 +1,101 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Execute scheduled subscription operation
+ * In case of failure writes message to notifications inbox
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $attemptsCount = $this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+
+ if (($attemptsCount === null) || ($attemptsCount <= 0)) {
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return false;
+ }
+
+ $attemptsCount -= 1;
+ $this->flagManager->saveFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $signUpResult = $this->connector->execute('signUp');
+ if ($signUpResult === false) {
+ return false;
+ }
+
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return true;
+ }
+
+ /**
+ * Delete cron schedule setting into config.
+ *
+ * Delete cron schedule setting for subscription handler into config and
+ * re-initialize config cache to avoid auto-generate new schedule items.
+ *
+ * @return bool
+ */
+ private function deleteAnalyticsCronExpr()
+ {
+ $this->configWriter->delete(SubscriptionHandler::CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/Update.php b/app/code/Magento/Analytics/Cron/Update.php
new file mode 100644
index 0000000000000..9062a7bac7551
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/Update.php
@@ -0,0 +1,92 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ }
+
+ /**
+ * Execute scheduled update operation
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $attemptsCount = $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+
+ if ($attemptsCount) {
+ $attemptsCount -= 1;
+ $result = $this->connector->execute('update');
+ }
+
+ if ($result || ($attemptsCount <= 0) || (!$this->analyticsToken->isTokenExist())) {
+ $this->flagManager
+ ->deleteFlag(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+ $this->flagManager->deleteFlag(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE);
+ $this->configWriter->delete(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/LICENSE.txt b/app/code/Magento/Analytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Analytics/LICENSE.txt
@@ -0,0 +1,48 @@
+
+Open Software License ("OSL") v. 3.0
+
+This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Open Software License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
\ No newline at end of file
diff --git a/app/code/Magento/Analytics/LICENSE_AFL.txt b/app/code/Magento/Analytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Analytics/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+Academic Free License ("AFL") v. 3.0
+
+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Academic Free License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/Analytics/Model/AnalyticsToken.php b/app/code/Magento/Analytics/Model/AnalyticsToken.php
new file mode 100644
index 0000000000000..ccec4d1bbe958
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/AnalyticsToken.php
@@ -0,0 +1,92 @@
+reinitableConfig = $reinitableConfig;
+ $this->config = $config;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Get Magento BI token value.
+ *
+ * @return string|null
+ */
+ public function getToken()
+ {
+ return $this->config->getValue($this->tokenPath);
+ }
+
+ /**
+ * Stores Magento BI token value.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ public function storeToken($value)
+ {
+ $this->configWriter->save($this->tokenPath, $value);
+ $this->reinitableConfig->reinit();
+
+ return true;
+ }
+
+ /**
+ * Check Magento BI token value exist.
+ *
+ * @return bool
+ */
+ public function isTokenExist()
+ {
+ return (bool)$this->getToken();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config.php b/app/code/Magento/Analytics/Model/Config.php
new file mode 100644
index 0000000000000..ba508187b4b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config.php
@@ -0,0 +1,40 @@
+data = $data;
+ }
+
+ /**
+ * Get config value by key.
+ *
+ * @param string|null $key
+ * @param string|null $default
+ * @return array
+ */
+ public function get($key = null, $default = null)
+ {
+ return $this->data->get($key, $default);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
new file mode 100644
index 0000000000000..6e6f008d49f7e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
@@ -0,0 +1,107 @@
+analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Activate process of subscription update handling.
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function processUrlUpdate(string $url)
+ {
+ if ($this->analyticsToken->isTokenExist()) {
+ if (!$this->flagManager->getFlagData(self::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $this->flagManager->saveFlag(self::PREVIOUS_BASE_URL_FLAG_CODE, $url);
+ }
+
+ $this->flagManager
+ ->saveFlag(self::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ $this->configWriter->save(self::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
new file mode 100644
index 0000000000000..e26ad01fc74bf
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
@@ -0,0 +1,91 @@
+configWriter = $configWriter;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * {@inheritdoc}. Set schedule setting for cron.
+ *
+ * @return Value
+ */
+ public function afterSave()
+ {
+ $result = preg_match('#(?\d{2}),(?\d{2}),(?\d{2})#', $this->getValue(), $time);
+
+ if (!$result) {
+ throw new LocalizedException(__('Time value has an unsupported format'));
+ }
+
+ $cronExprArray = [
+ $time['min'], # Minute
+ $time['hour'], # Hour
+ '*', # Day of the Month
+ '*', # Month of the Year
+ '*', # Day of the Week
+ ];
+
+ $cronExprString = join(' ', $cronExprArray);
+
+ try {
+ $this->configWriter->save(self::CRON_SCHEDULE_PATH, $cronExprString);
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('Cron settings can\'t be saved'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
new file mode 100644
index 0000000000000..ac97f2a843e61
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
@@ -0,0 +1,84 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @return Value
+ * @throws LocalizedException
+ */
+ public function afterSave()
+ {
+ try {
+ if ($this->isValueChanged()) {
+ $enabled = $this->getData('value');
+
+ if ($enabled) {
+ $this->subscriptionHandler->processEnabled();
+ } else {
+ $this->subscriptionHandler->processDisabled();
+ }
+ }
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('There was an error save new configuration value.'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
new file mode 100644
index 0000000000000..4b125949948c6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
@@ -0,0 +1,172 @@
+configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Processing of activation MBI subscription.
+ *
+ * Activate process of subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processEnabled()
+ {
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->setCronSchedule();
+ $this->setAttemptsFlag();
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+
+ /**
+ * Set cron schedule setting into config for activation of subscription process.
+ *
+ * @return bool
+ */
+ private function setCronSchedule()
+ {
+ $this->configWriter->save(self::CRON_STRING_PATH, join(' ', self::CRON_EXPR_ARRAY));
+ return true;
+ }
+
+ /**
+ * Set flag as reserve counter of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function setAttemptsFlag()
+ {
+ return $this->flagManager
+ ->saveFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ }
+
+ /**
+ * Processing of deactivation MBI subscription.
+ *
+ * Disable data collection
+ * and interrupt subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processDisabled()
+ {
+ $this->disableCollectionData();
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->unsetAttemptsFlag();
+ }
+
+ return true;
+ }
+
+ /**
+ * Unset flag of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function unsetAttemptsFlag()
+ {
+ return $this->flagManager
+ ->deleteFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ }
+
+ /**
+ * Unset schedule of collection data cron.
+ *
+ * @return bool
+ */
+ private function disableCollectionData()
+ {
+ $this->configWriter->delete(CollectionTime::CRON_SCHEDULE_PATH);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
new file mode 100644
index 0000000000000..1aabbb91ddf87
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
@@ -0,0 +1,32 @@
+getValue())) {
+ throw new LocalizedException(__('Please select a vertical.'));
+ }
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Mapper.php b/app/code/Magento/Analytics/Model/Config/Mapper.php
new file mode 100644
index 0000000000000..504690b8e4763
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Mapper.php
@@ -0,0 +1,66 @@
+ [
+ * 'name' => 'file_name',
+ * 'providers' => [
+ * 'reportProvider' => [
+ * 'name' => 'report_provider_name',
+ * 'class' => 'Magento\Analytics\ReportXml\ReportProvider',
+ * 'parameters' =>[
+ * 'name' => 'report_name',
+ * ],
+ * ],
+ * 'customProvider' => [
+ * 'name' => 'custom_provider_name',
+ * 'class' => 'Magento\Analytics\Model\CustomProvider',
+ * ],
+ * ],
+ * ]
+ * ];
+ */
+ public function execute($configData)
+ {
+ if (!isset($configData['config'][0]['file'])) {
+ return [];
+ }
+
+ $files = [];
+ foreach ($configData['config'][0]['file'] as $fileData) {
+ /** just one set of providers is allowed by xsd */
+ $providers = reset($fileData['providers']);
+ foreach ($providers as $providerType => $providerDataSet) {
+ /** just one set of provider data is allowed by xsd */
+ $providerData = reset($providerDataSet);
+ /** just one set of parameters is allowed by xsd */
+ $providerData['parameters'] = !empty($providerData['parameters'])
+ ? reset($providerData['parameters'])
+ : [];
+ $providerData['parameters'] = array_map(
+ 'reset',
+ $providerData['parameters']
+ );
+ $providers[$providerType] = $providerData;
+ }
+ $files[$fileData['name']] = $fileData;
+ $files[$fileData['name']]['providers'] = $providers;
+ }
+ return $files;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Reader.php b/app/code/Magento/Analytics/Model/Config/Reader.php
new file mode 100644
index 0000000000000..8980e31627717
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Reader.php
@@ -0,0 +1,52 @@
+mapper = $mapper;
+ $this->readers = $readers;
+ }
+
+ /**
+ * Read configuration scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Source/Vertical.php b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
new file mode 100644
index 0000000000000..c9d9582ea7c7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
@@ -0,0 +1,51 @@
+verticals = $verticals;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toOptionArray()
+ {
+ $result = [
+ ['value' => '', 'label' => __('--Please Select--')]
+ ];
+
+ foreach ($this->verticals as $vertical) {
+ $result[] = ['value' => $vertical, 'label' => __($vertical)];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ConfigInterface.php b/app/code/Magento/Analytics/Model/ConfigInterface.php
new file mode 100644
index 0000000000000..caaa2e100c1c7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ConfigInterface.php
@@ -0,0 +1,22 @@
+ 'command_class_name'.
+ *
+ * The list may be configured in each module via '/etc/di.xml'.
+ *
+ * @var string[]
+ */
+ private $commands;
+
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @param array $commands
+ * @param ObjectManagerInterface $objectManager
+ */
+ public function __construct(
+ array $commands,
+ ObjectManagerInterface $objectManager
+ ) {
+ $this->commands = $commands;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Executes a command in accordance with the given name.
+ *
+ * @param string $commandName
+ * @return bool
+ * @throws NotFoundException if the command is not found.
+ */
+ public function execute($commandName)
+ {
+ if (!array_key_exists($commandName, $this->commands)) {
+ throw new NotFoundException(__('Command was not found.'));
+ }
+
+ /** @var \Magento\Analytics\Model\Connector\CommandInterface $command */
+ $command = $this->objectManager->create($this->commands[$commandName]);
+
+ return $command->execute();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/CommandInterface.php b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
new file mode 100644
index 0000000000000..7a8774fe3dba9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
@@ -0,0 +1,21 @@
+curlFactory = $curlFactory;
+ $this->responseFactory = $responseFactory;
+ $this->converter = $converter;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request($method, $url, array $body = [], array $headers = [], $version = '1.1')
+ {
+ $response = new \Zend_Http_Response(0, []);
+
+ try {
+ $curl = $this->curlFactory->create();
+ $headers = $this->applyContentTypeHeaderFromConverter($headers);
+
+ $curl->write($method, $url, $version, $headers, $this->converter->toBody($body));
+
+ $result = $curl->read();
+
+ if ($curl->getErrno()) {
+ $this->logger->critical(
+ new \Exception(
+ sprintf(
+ 'MBI service CURL connection error #%s: %s',
+ $curl->getErrno(),
+ $curl->getError()
+ )
+ )
+ );
+
+ return $response;
+ }
+
+ $response = $this->responseFactory->create($result);
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+
+ return $response;
+ }
+
+ /**
+ * @param array $headers
+ *
+ * @return array
+ */
+ private function applyContentTypeHeaderFromConverter(array $headers)
+ {
+ $contentTypeHeaderKey = array_search($this->converter->getContentTypeHeader(), $headers);
+ if ($contentTypeHeaderKey === false) {
+ $headers[] = $this->converter->getContentTypeHeader();
+ }
+
+ return $headers;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
new file mode 100644
index 0000000000000..a1e1f057684f6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
@@ -0,0 +1,29 @@
+converter = $converter;
+ $this->responseHandlers = $responseHandlers;
+ }
+
+ /**
+ * @param \Zend_Http_Response $response
+ *
+ * @return bool|string
+ */
+ public function getResult(\Zend_Http_Response $response)
+ {
+ $result = false;
+ $responseBody = $this->converter->fromBody($response->getBody());
+ if (array_key_exists($response->getStatus(), $this->responseHandlers)) {
+ $result = $this->responseHandlers[$response->getStatus()]->handleResponse($responseBody);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
new file mode 100644
index 0000000000000..f1a8ea6460f9d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
@@ -0,0 +1,93 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Notify MBI about that data collection was finished
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->notifyDataChangedUrlPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ }
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/OTPRequest.php b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
new file mode 100644
index 0000000000000..dfa283e10d070
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
@@ -0,0 +1,115 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Performs obtaining of an OTP from the MBI service.
+ *
+ * Returns received OTP or FALSE in case of failure.
+ *
+ * @return string|false
+ */
+ public function call()
+ {
+ $result = false;
+
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->otpUrlConfigPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Obtaining of an OTP from the MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
new file mode 100644
index 0000000000000..d9a672e81f43d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->subscriptionHandler = $subscriptionHandler;
+ $this->subscriptionStatusProvider = $subscriptionStatusProvider;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $responseBody)
+ {
+ if ($this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->analyticsToken->storeToken(null);
+ $this->subscriptionHandler->processEnabled();
+ }
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
new file mode 100644
index 0000000000000..b2261e418abc7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
@@ -0,0 +1,51 @@
+analyticsToken = $analyticsToken;
+ $this->converter = $converter;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $body)
+ {
+ if (isset($body['access-token']) && !empty($body['access-token'])) {
+ $this->analyticsToken->storeToken($body['access-token']);
+ return $body['access-token'];
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
new file mode 100644
index 0000000000000..73fc575ae2821
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->integrationManager = $integrationManager;
+ $this->config = $config;
+ $this->httpClient = $httpClient;
+ $this->logger = $logger;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes signUp command
+ *
+ * During this call Magento generates or retrieves access token for the integration user
+ * In case successful generation Magento activates user and sends access token to MA
+ * As the response, Magento receives a token to MA
+ * Magento stores this token in System Configuration
+ *
+ * This method returns true in case of success
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $integrationToken = $this->integrationManager->generateToken();
+ if ($integrationToken) {
+ $this->integrationManager->activateIntegration();
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->signUpUrlPath),
+ [
+ "token" => $integrationToken->getData('token'),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Subscription for MBI service has been failed. An error occurred during token exchange: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
new file mode 100644
index 0000000000000..8f05f1107e87e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
@@ -0,0 +1,114 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->flagManager = $flagManager;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes update request to MBI api in case store url was changed
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::PUT,
+ $this->config->getValue($this->updateUrlPath),
+ [
+ "url" => $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE),
+ "new-url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ "access-token" => $this->analyticsToken->getToken(),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Update of the subscription for MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php
new file mode 100644
index 0000000000000..6905eee372ae2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Cryptographer.php
@@ -0,0 +1,130 @@
+analyticsToken = $analyticsToken;
+ $this->encodedContextFactory = $encodedContextFactory;
+ }
+
+ /**
+ * Encrypt input data.
+ *
+ * @param string $source
+ * @return EncodedContext
+ * @throws LocalizedException
+ */
+ public function encode($source)
+ {
+ if (!is_string($source)) {
+ try {
+ $source = (string)$source;
+ } catch (\Exception $e) {
+ throw new LocalizedException(__('Input data must be string or convertible into string.'));
+ }
+ } elseif (!$source) {
+ throw new LocalizedException(__('Input data must be non-empty string.'));
+ }
+ if (!$this->validateCipherMethod($this->cipherMethod)) {
+ throw new LocalizedException(__('Not valid cipher method.'));
+ }
+ $initializationVector = $this->getInitializationVector();
+
+ $encodedContext = $this->encodedContextFactory->create([
+ 'content' => openssl_encrypt(
+ $source,
+ $this->cipherMethod,
+ $this->getKey(),
+ OPENSSL_RAW_DATA,
+ $initializationVector
+ ),
+ 'initializationVector' => $initializationVector,
+ ]);
+
+ return $encodedContext;
+ }
+
+ /**
+ * Return key for encryption.
+ *
+ * @return string
+ * @throws LocalizedException
+ */
+ private function getKey()
+ {
+ $token = $this->analyticsToken->getToken();
+ if (!$token) {
+ throw new LocalizedException(__('Encryption key can\'t be empty.'));
+ }
+ return hash('sha256', $token);
+ }
+
+ /**
+ * Return established cipher method.
+ *
+ * @return string
+ */
+ private function getCipherMethod()
+ {
+ return $this->cipherMethod;
+ }
+
+ /**
+ * Return each time generated random initialization vector which depends on the cipher method.
+ *
+ * @return string
+ */
+ private function getInitializationVector()
+ {
+ $ivSize = openssl_cipher_iv_length($this->getCipherMethod());
+ return openssl_random_pseudo_bytes($ivSize);
+ }
+
+ /**
+ * Check that cipher method is allowed for encryption.
+ *
+ * @param string $cipherMethod
+ * @return bool
+ */
+ private function validateCipherMethod($cipherMethod)
+ {
+ $methods = openssl_get_cipher_methods();
+ return (false !== array_search($cipherMethod, $methods));
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/EncodedContext.php b/app/code/Magento/Analytics/Model/EncodedContext.php
new file mode 100644
index 0000000000000..5fb2d0c15aef7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/EncodedContext.php
@@ -0,0 +1,52 @@
+content = $content;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
new file mode 100644
index 0000000000000..5d127037afea9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
@@ -0,0 +1,17 @@
+filesystem = $filesystem;
+ $this->archive = $archive;
+ $this->reportWriter = $reportWriter;
+ $this->cryptographer = $cryptographer;
+ $this->fileRecorder = $fileRecorder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function prepareExportData()
+ {
+ try {
+ $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
+
+ $this->prepareDirectory($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $this->reportWriter->write($tmpDirectory, $this->getTmpFilesDirRelativePath());
+
+ $tmpFilesDirectoryAbsolutePath = $this->validateSource($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $archiveAbsolutePath = $this->prepareFileDirectory($tmpDirectory, $this->getArchiveRelativePath());
+ $this->pack(
+ $tmpFilesDirectoryAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->validateSource($tmpDirectory, $this->getArchiveRelativePath());
+ $this->fileRecorder->recordNewFile(
+ $this->cryptographer->encode($tmpDirectory->readFile($this->getArchiveRelativePath()))
+ );
+ } finally {
+ $tmpDirectory->delete($this->getTmpFilesDirRelativePath());
+ $tmpDirectory->delete($this->getArchiveRelativePath());
+ }
+
+ return true;
+ }
+
+ /**
+ * Return relative path to a directory for temporary files with reports data.
+ *
+ * @return string
+ */
+ private function getTmpFilesDirRelativePath()
+ {
+ return $this->subdirectoryPath . 'tmp/';
+ }
+
+ /**
+ * Return relative path to a directory for an archive.
+ *
+ * @return string
+ */
+ private function getArchiveRelativePath()
+ {
+ return $this->subdirectoryPath . $this->archiveName;
+ }
+
+ /**
+ * Clean up a directory.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Remove a file and a create parent directory a file.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareFileDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+ if (dirname($path) !== '.') {
+ $directory->create(dirname($path));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Packing data into an archive.
+ *
+ * @param string $source
+ * @param string $destination
+ * @return bool
+ */
+ private function pack($source, $destination)
+ {
+ $this->archive->pack(
+ $source,
+ $destination,
+ is_dir($source) ?: false
+ );
+
+ return true;
+ }
+
+ /**
+ * Validate that data source exist.
+ *
+ * Return absolute path in a validated data source.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ * @throws LocalizedException If source is not exist.
+ */
+ private function validateSource(WriteInterface $directory, $path)
+ {
+ if (!$directory->isExist($path)) {
+ throw new LocalizedException(__('Source "%1" is not exist', $directory->getAbsolutePath($path)));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
new file mode 100644
index 0000000000000..65efb33659c89
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
@@ -0,0 +1,19 @@
+exportDataHandler = $exportDataHandler;
+ $this->analyticsConnector = $connector;
+ }
+
+ /**
+ * {@inheritdoc}
+ * Execute notification command.
+ *
+ * @return bool
+ */
+ public function prepareExportData()
+ {
+ $result = $this->exportDataHandler->prepareExportData();
+ $this->analyticsConnector->execute('notifyDataChanged');
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfo.php b/app/code/Magento/Analytics/Model/FileInfo.php
new file mode 100644
index 0000000000000..19bdaf21b2a20
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfo.php
@@ -0,0 +1,52 @@
+path = $path;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfoManager.php b/app/code/Magento/Analytics/Model/FileInfoManager.php
new file mode 100644
index 0000000000000..e37700e665420
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfoManager.php
@@ -0,0 +1,123 @@
+flagManager = $flagManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ }
+
+ /**
+ * Save FileInfo object.
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ * @throws LocalizedException
+ */
+ public function save(FileInfo $fileInfo)
+ {
+ $parameters = [];
+ $parameters['initializationVector'] = $fileInfo->getInitializationVector();
+ $parameters['path'] = $fileInfo->getPath();
+
+ $emptyParameters = array_diff($parameters, array_filter($parameters));
+ if ($emptyParameters) {
+ throw new LocalizedException(
+ __('These arguments can\'t be empty "%1"', implode(', ', array_keys($emptyParameters)))
+ );
+ }
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->encodeValue($parameters[$encodedParameter]);
+ }
+
+ $this->flagManager->saveFlag($this->flagCode, $parameters);
+
+ return true;
+ }
+
+ /**
+ * Load FileInfo object.
+ *
+ * @return FileInfo
+ */
+ public function load()
+ {
+ $parameters = $this->flagManager->getFlagData($this->flagCode) ?: [];
+
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($parameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->decodeValue($parameters[$encodedParameter]);
+ }
+
+ $fileInfo = $this->fileInfoFactory->create($parameters);
+
+ return $fileInfo;
+ }
+
+ /**
+ * Encode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function encodeValue($value)
+ {
+ return base64_encode($value);
+ }
+
+ /**
+ * Decode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function decodeValue($value)
+ {
+ return base64_decode($value);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileRecorder.php b/app/code/Magento/Analytics/Model/FileRecorder.php
new file mode 100644
index 0000000000000..70438a98d56f1
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileRecorder.php
@@ -0,0 +1,136 @@
+fileInfoManager = $fileInfoManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * Save new encrypted file, register it and remove old registered file.
+ *
+ * @param EncodedContext $encodedContext
+ * @return bool
+ */
+ public function recordNewFile(EncodedContext $encodedContext)
+ {
+ $directory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+
+ $fileRelativePath = $this->getFileRelativePath();
+ $directory->writeFile($fileRelativePath, $encodedContext->getContent());
+
+ $fileInfo = $this->fileInfoManager->load();
+ $this->registerFile($encodedContext, $fileRelativePath);
+ $this->removeOldFile($fileInfo, $directory);
+
+ return true;
+ }
+
+ /**
+ * Return relative path to encoded file.
+ *
+ * @return string
+ */
+ private function getFileRelativePath()
+ {
+ return $this->fileSubdirectoryPath . hash('sha256', time())
+ . '/' . $this->encodedFileName;
+ }
+
+ /**
+ * Register encoded file.
+ *
+ * @param EncodedContext $encodedContext
+ * @param string $fileRelativePath
+ * @return bool
+ */
+ private function registerFile(EncodedContext $encodedContext, $fileRelativePath)
+ {
+ $newFileInfo = $this->fileInfoFactory->create(
+ [
+ 'path' => $fileRelativePath,
+ 'initializationVector' => $encodedContext->getInitializationVector(),
+ ]
+ );
+ $this->fileInfoManager->save($newFileInfo);
+
+ return true;
+ }
+
+ /**
+ * Remove previously registered file.
+ *
+ * @param FileInfo $fileInfo
+ * @param WriteInterface $directory
+ * @return bool
+ */
+ private function removeOldFile(FileInfo $fileInfo, WriteInterface $directory)
+ {
+ if (!$fileInfo->getPath()) {
+ return true;
+ }
+
+ $directory->delete($fileInfo->getPath());
+
+ $directoryName = dirname($fileInfo->getPath());
+ if ($directoryName !== '.') {
+ $directory->delete($directoryName);
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/IntegrationManager.php b/app/code/Magento/Analytics/Model/IntegrationManager.php
new file mode 100644
index 0000000000000..61a40a955e026
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/IntegrationManager.php
@@ -0,0 +1,126 @@
+integrationService = $integrationService;
+ $this->config = $config;
+ $this->oauthService = $oauthService;
+ }
+
+ /**
+ * Activate predefined integration user
+ *
+ * @return bool
+ * @throws NoSuchEntityException
+ */
+ public function activateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ throw new NoSuchEntityException(__('Cannot find predefined integration user!'));
+ }
+ $integrationData = $this->getIntegrationData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = $integration->getId();
+ $this->integrationService->update($integrationData);
+ return true;
+ }
+
+ /**
+ * This method execute Generate Token command and enable integration
+ *
+ * @return bool|\Magento\Integration\Model\Oauth\Token
+ */
+ public function generateToken()
+ {
+ $consumerId = $this->generateIntegration()->getConsumerId();
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ if (!$accessToken && $this->oauthService->createAccessToken($consumerId, true)) {
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ }
+ return $accessToken;
+ }
+
+ /**
+ * Returns consumer Id for MA integration user
+ *
+ * @return \Magento\Integration\Model\Integration
+ */
+ private function generateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ $integration = $this->integrationService->create($this->getIntegrationData());
+ }
+ return $integration;
+ }
+
+ /**
+ * Returns default attributes for MA integration user
+ *
+ * @param int $status
+ * @return array
+ */
+ private function getIntegrationData($status = Integration::STATUS_INACTIVE)
+ {
+ $integrationData = [
+ 'name' => $this->config->getConfigDataValue('analytics/integration_name'),
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ return $integrationData;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Link.php b/app/code/Magento/Analytics/Model/Link.php
new file mode 100644
index 0000000000000..4a40796df4fd0
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Link.php
@@ -0,0 +1,54 @@
+url = $url;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/LinkProvider.php b/app/code/Magento/Analytics/Model/LinkProvider.php
new file mode 100644
index 0000000000000..2474653f4916c
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/LinkProvider.php
@@ -0,0 +1,87 @@
+linkFactory = $linkFactory;
+ $this->fileInfoManager = $fileInfoManager;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Returns base url to file according to store configuration
+ *
+ * @param FileInfo $fileInfo
+ * @return string
+ */
+ private function getBaseUrl(FileInfo $fileInfo)
+ {
+ return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $fileInfo->getPath();
+ }
+
+ /**
+ * Verify is requested file ready
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ */
+ private function isFileReady(FileInfo $fileInfo)
+ {
+ return $fileInfo->getPath() && $fileInfo->getInitializationVector();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get()
+ {
+ $fileInfo = $this->fileInfoManager->load();
+ if (!$this->isFileReady($fileInfo)) {
+ throw new NoSuchEntityException(__('File is not ready yet.'));
+ }
+ return $this->linkFactory->create(
+ [
+ 'url' => $this->getBaseUrl($fileInfo),
+ 'initializationVector' => base64_encode($fileInfo->getInitializationVector())
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
new file mode 100644
index 0000000000000..174272614fb19
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
@@ -0,0 +1,61 @@
+subscriptionUpdateHandler = $subscriptionUpdateHandler;
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @param Value $subject
+ * @param Value $result
+ * @return Value
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterAfterSave(
+ Value $subject,
+ Value $result
+ ) {
+ if ($this->isPluginApplicable($result)) {
+ $this->subscriptionUpdateHandler->processUrlUpdate($result->getOldValue());
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Value $result
+ * @return bool
+ */
+ private function isPluginApplicable(Value $result)
+ {
+ return $result->isValueChanged()
+ && ($result->getPath() === Store::XML_PATH_SECURE_BASE_URL)
+ && ($result->getScope() === ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ProviderFactory.php b/app/code/Magento/Analytics/Model/ProviderFactory.php
new file mode 100644
index 0000000000000..3a23430fca077
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ProviderFactory.php
@@ -0,0 +1,39 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * @param string $providerName
+ * @return object
+ */
+ public function create($providerName)
+ {
+ return $this->objectManager->get($providerName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
new file mode 100644
index 0000000000000..e7fdf6f9e8132
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
@@ -0,0 +1,94 @@
+analyticsToken = $analyticsToken;
+ $this->otpRequest = $otpRequest;
+ $this->config = $config;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Provide URL on resource with reports.
+ *
+ * @return string
+ * @throws SubscriptionUpdateException
+ */
+ public function getUrl()
+ {
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ throw new SubscriptionUpdateException(__(
+ 'Your Base URL has been changed and your reports are being updated. '
+ . 'Advanced Reporting will be available once this change has been processed. Please try again later.'
+ ));
+ }
+
+ $url = $this->config->getValue($this->urlReportConfigPath);
+ if ($this->analyticsToken->isTokenExist()) {
+ $otp = $this->otpRequest->call();
+ if ($otp) {
+ $query = http_build_query(['otp' => $otp], '', '&');
+ $url .= '?' . $query;
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php
new file mode 100644
index 0000000000000..7128658947908
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriter.php
@@ -0,0 +1,101 @@
+config = $config;
+ $this->reportValidator = $reportValidator;
+ $this->providerFactory = $providerFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write(WriteInterface $directory, $path)
+ {
+ $errorsList = [];
+ foreach ($this->config->get() as $file) {
+ $provider = reset($file['providers']);
+ if (isset($provider['parameters']['name'])) {
+ $error = $this->reportValidator->validate($provider['parameters']['name']);
+ if ($error) {
+ $errorsList[] = $error;
+ continue;
+ }
+ }
+ /** @var $providerObject */
+ $providerObject = $this->providerFactory->create($provider['class']);
+ $fileName = $provider['parameters'] ? $provider['parameters']['name'] : $provider['name'];
+ $fileFullPath = $path . $fileName . '.csv';
+ $fileData = $providerObject->getReport(...array_values($provider['parameters']));
+ $stream = $directory->openFile($fileFullPath, 'w+');
+ $stream->lock();
+ $headers = [];
+ foreach ($fileData as $row) {
+ if (!$headers) {
+ $headers = array_keys($row);
+ $stream->writeCsv($headers);
+ }
+ $stream->writeCsv($row);
+ }
+ $stream->unlock();
+ $stream->close();
+ }
+ if ($errorsList) {
+ $errorStream = $directory->openFile($path . $this->errorsFileName, 'w+');
+ foreach ($errorsList as $error) {
+ $errorStream->lock();
+ $errorStream->writeCsv($error);
+ $errorStream->unlock();
+ }
+ $errorStream->close();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriterInterface.php b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
new file mode 100644
index 0000000000000..a611095a47ae4
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
@@ -0,0 +1,28 @@
+moduleManager = $moduleManager;
+ }
+
+ /**
+ * Returns module with module status
+ *
+ * @return array
+ */
+ public function current()
+ {
+ $current = parent::current();
+ if (is_array($current) && isset($current['module_name'])) {
+ $current['status'] =
+ $this->moduleManager->isEnabled($current['module_name']) == 1 ? 'Enabled' : "Disabled";
+ }
+ return $current;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
new file mode 100644
index 0000000000000..0d226a9de7dc2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
@@ -0,0 +1,102 @@
+scopeConfig = $scopeConfig;
+ $this->configPaths = $configPaths;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Generates report using config paths from di.xml
+ * For each website and store
+ * @return \IteratorIterator
+ */
+ public function getReport()
+ {
+ $configReport = $this->generateReportForScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0);
+
+ /** @var WebsiteInterface $website */
+ foreach ($this->storeManager->getWebsites() as $website) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_WEBSITES, $website->getId()),
+ $configReport
+ );
+ }
+
+ /** @var StoreInterface $store */
+ foreach ($this->storeManager->getStores() as $store) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_STORES, $store->getId()),
+ $configReport
+ );
+ }
+ return new \IteratorIterator(new \ArrayIterator($configReport));
+ }
+
+ /**
+ * Creates report from config for scope type and scope id.
+ *
+ * @param string $scope
+ * @param int $scopeId
+ * @return array
+ */
+ private function generateReportForScope($scope, $scopeId)
+ {
+ $report = [];
+ foreach ($this->configPaths as $configPath) {
+ $report[] = [
+ "config_path" => $configPath,
+ "scope" => $scope,
+ "scope_id" => $scopeId,
+ "value" => $this->scopeConfig->getValue(
+ $configPath,
+ $scope,
+ $scopeId
+ )
+ ];
+ }
+ return $report;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
new file mode 100644
index 0000000000000..1dd831a672faa
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
@@ -0,0 +1,120 @@
+scopeConfig = $scopeConfig;
+ $this->analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Retrieve subscription status to Magento BI Advanced Reporting.
+ *
+ * Statuses:
+ * Enabled - if subscription is enabled and MA token was received;
+ * Pending - if subscription is enabled and MA token was not received;
+ * Disabled - if subscription is not enabled.
+ * Failed - if subscription is enabled and token was not received after attempts ended.
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ $isSubscriptionEnabledInConfig = $this->scopeConfig->getValue('analytics/subscription/enabled');
+ if ($isSubscriptionEnabledInConfig) {
+ return $this->getStatusForEnabledSubscription();
+ }
+
+ return $this->getStatusForDisabledSubscription();
+ }
+
+ /**
+ * Retrieve status for subscription that enabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForEnabledSubscription()
+ {
+ $status = static::ENABLED;
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $status = self::PENDING;
+ }
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $status = static::PENDING;
+ if ($this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) === null) {
+ $status = static::FAILED;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Retrieve status for subscription that disabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForDisabledSubscription()
+ {
+ return static::DISABLED;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
new file mode 100644
index 0000000000000..9aaa2ebb3b56f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
@@ -0,0 +1,80 @@
+subscriptionStatusProvider = $subscriptionStatusProvider;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getIdentity()
+ {
+ return hash('sha256', 'ANALYTICS_NOTIFICATION');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDisplayed()
+ {
+ return $this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::FAILED;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getText()
+ {
+ $messageDetails = '';
+
+ $messageDetails .= __('Failed to synchronize data to the Magento Business Intelligence service. ');
+ $messageDetails .= __(
+ 'Retry Synchronization ',
+ $this->urlBuilder->getUrl('analytics/subscription/retry')
+ );
+
+ return $messageDetails;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getSeverity()
+ {
+ return self::SEVERITY_MAJOR;
+ }
+}
diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md
new file mode 100644
index 0000000000000..7ec64abcd9b86
--- /dev/null
+++ b/app/code/Magento/Analytics/README.md
@@ -0,0 +1,41 @@
+# Magento_Analytics Module
+
+The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality.
+
+The module implements the following functionality:
+
+* enabling subscription to the MBI and automatic re-subscription
+* changing the base URL with the same MBI account remained
+* declaring the configuration schemas for report data collection
+* collecting the Magento instance data as reports for the MBI
+* introducing API that provides the collected data
+* extending Magento configuration with the module parameters:
+ * subscription status (enabled/disabled)
+ * industry (a business area in which the instance website works)
+ * time of data collection (time of the day when the module collects data)
+
+## Structure
+
+Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`.
+[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting.
+The language declares SQL queries using XML declaration.
+
+## Subscription Process
+
+The subscription to the MBI service is enabled during the installation process of the Analytics module. Each administrator will be notified of these new features upon their initial login to the Admin Panel.
+
+## Analytics Settings
+
+Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab.
+
+The following options can be adjusted:
+* Advanced Reporting Service (Enabled/Disabled)
+ * Alters the status of the Advanced Reporting subscription
+* Time of day to send data (Hour/Minute/Second in the store's time zone)
+ * Defines when the data collection process for the Advanced Reporting service occurs
+* Industry
+ * Defines the industry of the store in order to create a personalized Advanced Reporting experience
+
+## Extensibility
+
+We do not recommend to extend the Magento_Analytics module. It introduces an API that is purposed to transfer the collected data. Note that the API cannot be used for other needs.
diff --git a/app/code/Magento/Analytics/ReportXml/Config.php b/app/code/Magento/Analytics/ReportXml/Config.php
new file mode 100644
index 0000000000000..f50dcf941bf50
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config.php
@@ -0,0 +1,43 @@
+data = $data;
+ }
+
+ /**
+ * Returns config value by name
+ *
+ * @param string $queryName
+ * @return array
+ */
+ public function get($queryName)
+ {
+ return $this->data->get($queryName);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
new file mode 100644
index 0000000000000..9e0b20a6ad414
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
@@ -0,0 +1,61 @@
+hasAttributes()) {
+ $attrs = $source->attributes;
+ foreach ($attrs as $attr) {
+ $result[$attr->name] = $attr->value;
+ }
+ }
+ if ($source->hasChildNodes()) {
+ $children = $source->childNodes;
+ if ($children->length == 1) {
+ $child = $children->item(0);
+ if ($child->nodeType == XML_TEXT_NODE) {
+ $result['_value'] = $child->nodeValue;
+ return count($result) == 1 ? $result['_value'] : $result;
+ }
+ }
+ foreach ($children as $child) {
+ if ($child instanceof \DOMCharacterData) {
+ continue;
+ }
+ $result[$child->nodeName][] = $this->convertNode($child);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Converts XML document into corresponding array.
+ *
+ * @param \DOMDocument $source
+ * @return array
+ */
+ public function convert($source)
+ {
+ return $this->convertNode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Mapper.php b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
new file mode 100644
index 0000000000000..4dda8f3c733a6
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
@@ -0,0 +1,37 @@
+readers = $readers;
+ $this->mapper = $mapper;
+ }
+
+ /**
+ * Reads configuration according to the given scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ConfigInterface.php b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
new file mode 100644
index 0000000000000..ec03ddf429c06
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
@@ -0,0 +1,23 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Creates one-time connection for export
+ *
+ * @param string $connectionName
+ * @return AdapterInterface
+ */
+ public function getConnection($connectionName)
+ {
+ $connection = $this->resourceConnection->getConnection($connectionName);
+ $connectionClassName = get_class($connection);
+ $configData = $connection->getConfig();
+ $configData['use_buffered_query'] = false;
+ unset($configData['persistent']);
+ return $this->objectManager->create(
+ $connectionClassName,
+ [
+ 'config' => $configData
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
new file mode 100644
index 0000000000000..083b4843c185a
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
@@ -0,0 +1,27 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ }
+
+ /**
+ * Assembles WHERE conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['filter'])) {
+ return $selectBuilder;
+ }
+ $filters = $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $queryConfig['source']['filter'],
+ $this->nameResolver->getAlias($queryConfig['source'])
+ );
+ $selectBuilder->setFilters(array_merge_recursive($selectBuilder->getFilters(), [$filters]));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
new file mode 100644
index 0000000000000..811119ace221b
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
@@ -0,0 +1,69 @@
+nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles FROM condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ $selectBuilder->setFrom(
+ [
+ $this->nameResolver->getAlias($queryConfig['source']) =>
+ $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($queryConfig['source'])),
+ ]
+ );
+ $columns = $this->columnsResolver->getColumns($selectBuilder, $queryConfig['source']);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
new file mode 100644
index 0000000000000..f3c6540a25171
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
@@ -0,0 +1,113 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles JOIN conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['link-source'])) {
+ return $selectBuilder;
+ }
+ $joins = [];
+ $filters = $selectBuilder->getFilters();
+
+ $sourceAlias = $this->nameResolver->getAlias($queryConfig['source']);
+
+ foreach ($queryConfig['source']['link-source'] as $join) {
+ $joinAlias = $this->nameResolver->getAlias($join);
+
+ $joins[$joinAlias] = [
+ 'link-type' => isset($join['link-type']) ? $join['link-type'] : 'left',
+ 'table' => [
+ $joinAlias => $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($join)),
+ ],
+ 'condition' => $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['using'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ];
+ if (isset($join['filter'])) {
+ $filters = array_merge(
+ $filters,
+ [
+ $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['filter'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ]
+ );
+ }
+ $columns = $this->columnsResolver->getColumns($selectBuilder, isset($join['attribute']) ? $join : []);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ }
+ $selectBuilder->setFilters($filters);
+ $selectBuilder->setJoins(array_merge($selectBuilder->getJoins(), $joins));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
new file mode 100644
index 0000000000000..e6474d4c5dc6d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
@@ -0,0 +1,100 @@
+nameResolver = $nameResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Set columns list to SelectBuilder
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $entityConfig
+ * @return array
+ */
+ public function getColumns(SelectBuilder $selectBuilder, $entityConfig)
+ {
+ if (!isset($entityConfig['attribute'])) {
+ return [];
+ }
+ $group = [];
+ $columns = $selectBuilder->getColumns();
+ foreach ($entityConfig['attribute'] as $attributeData) {
+ $columnAlias = $this->nameResolver->getAlias($attributeData);
+ $tableAlias = $this->nameResolver->getAlias($entityConfig);
+ $columnName = $this->nameResolver->getName($attributeData);
+ if (isset($attributeData['function'])) {
+ $prefix = '';
+ if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) {
+ $prefix = ' DISTINCT ';
+ }
+ $expression = new ColumnValueExpression(
+ strtoupper($attributeData['function']) . '(' . $prefix
+ . $this->getConnection()->quoteIdentifier($tableAlias . '.' . $columnName)
+ . ')'
+ );
+ } else {
+ $expression = $tableAlias . '.' . $columnName;
+ }
+ $columns[$columnAlias] = $expression;
+ if (isset($attributeData['group'])) {
+ $group[$columnAlias] = $expression;
+ }
+ }
+ $selectBuilder->setGroup(array_merge($selectBuilder->getGroup(), $group));
+ return $columns;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
new file mode 100644
index 0000000000000..773b96959e794
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
@@ -0,0 +1,166 @@
+ '%1$s = %2$s',
+ 'neq' => '%1$s != %2$s',
+ 'like' => '%1$s LIKE %2$s',
+ 'nlike' => '%1$s NOT LIKE %2$s',
+ 'in' => '%1$s IN(%2$s)',
+ 'nin' => '%1$s NOT IN(%2$s)',
+ 'notnull' => '%1$s IS NOT NULL',
+ 'null' => '%1$s IS NULL',
+ 'gt' => '%1$s > %2$s',
+ 'lt' => '%1$s < %2$s',
+ 'gteq' => '%1$s >= %2$s',
+ 'lteq' => '%1$s <= %2$s',
+ 'finset' => 'FIND_IN_SET(%2$s, %1$s)'
+ ];
+
+ /**
+ * @var \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private $connection;
+
+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
+ /**
+ * ConditionResolver constructor.
+ * @param ResourceConnection $resourceConnection
+ */
+ public function __construct(
+ ResourceConnection $resourceConnection
+ ) {
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Returns value for condition
+ *
+ * @param string $condition
+ * @param string $referencedEntity
+ * @return mixed|null|string|\Zend_Db_Expr
+ */
+ private function getValue($condition, $referencedEntity)
+ {
+ $value = null;
+ $argument = isset($condition['_value']) ? $condition['_value'] : null;
+ if (!isset($condition['type'])) {
+ $condition['type'] = 'value';
+ }
+
+ switch ($condition['type']) {
+ case "value":
+ $value = $this->getConnection()->quote($argument);
+ break;
+ case "variable":
+ $value = new Expression($argument);
+ break;
+ case "identifier":
+ $value = $this->getConnection()->quoteIdentifier(
+ $referencedEntity ? $referencedEntity . '.' . $argument : $argument
+ );
+ break;
+ }
+ return $value;
+ }
+
+ /**
+ * Returns condition for WHERE
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param string $tableName
+ * @param array $condition
+ * @param null|string $referencedEntity
+ * @return string
+ */
+ private function getCondition(SelectBuilder $selectBuilder, $tableName, $condition, $referencedEntity = null)
+ {
+ $columns = $selectBuilder->getColumns();
+ if (isset($columns[$condition['attribute']])
+ && $columns[$condition['attribute']] instanceof Expression
+ ) {
+ $expression = $columns[$condition['attribute']];
+ } else {
+ $expression = $this->getConnection()->quoteIdentifier($tableName . '.' . $condition['attribute']);
+ }
+ return sprintf(
+ $this->conditionMap[$condition['operator']],
+ $expression,
+ $this->getValue($condition, $referencedEntity)
+ );
+ }
+
+ /**
+ * Build WHERE condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $filterConfig
+ * @param string $aliasName
+ * @param null|string $referencedAlias
+ * @return array
+ */
+ public function getFilter(SelectBuilder $selectBuilder, $filterConfig, $aliasName, $referencedAlias = null)
+ {
+ $filtersParts = [];
+ foreach ($filterConfig as $filter) {
+ $glue = $filter['glue'];
+ $parts = [];
+ foreach ($filter['condition'] as $condition) {
+ if (isset($condition['type']) && $condition['type'] == 'variable') {
+ $selectBuilder->setParams(array_merge($selectBuilder->getParams(), [$condition['_value']]));
+ }
+ $parts[] = $this->getCondition(
+ $selectBuilder,
+ $aliasName,
+ $condition,
+ $referencedAlias
+ );
+ }
+ if (isset($filter['filter'])) {
+ $parts[] = '(' . $this->getFilter(
+ $selectBuilder,
+ $filter['filter'],
+ $aliasName,
+ $referencedAlias
+ ) . ')';
+ }
+ $filtersParts[] = '(' . implode(' ' . strtoupper($glue) . ' ', $parts) . ')';
+ }
+ return implode(' OR ', $filtersParts);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
new file mode 100644
index 0000000000000..c9543002eb272
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
@@ -0,0 +1,40 @@
+getName($elementConfig);
+ if (isset($elementConfig['alias'])) {
+ $alias = $elementConfig['alias'];
+ }
+ return $alias;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
new file mode 100644
index 0000000000000..21a641f0a71c7
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
@@ -0,0 +1,64 @@
+connectionFactory = $connectionFactory;
+ $this->queryFactory = $queryFactory;
+ }
+
+ /**
+ * Tries to do query for provided report with limit 0 and return error information if it failed
+ *
+ * @param string $name
+ * @param SearchCriteriaInterface $criteria
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function validate($name, SearchCriteriaInterface $criteria = null)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $query->getSelect()->limit(0);
+ try {
+ $connection->query($query->getSelect());
+ } catch (\Zend_Db_Statement_Exception $e) {
+ return [$name, $e->getMessage()];
+ }
+
+ return [];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
new file mode 100644
index 0000000000000..4e5a1940773b1
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
@@ -0,0 +1,289 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Get join condition
+ *
+ * @return array
+ */
+ public function getJoins()
+ {
+ return $this->joins;
+ }
+
+ /**
+ * Set joins conditions
+ *
+ * @param array $joins
+ * @return void
+ */
+ public function setJoins($joins)
+ {
+ $this->joins = $joins;
+ }
+
+ /**
+ * Get connection name
+ *
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * Set connection name
+ *
+ * @param string $connectionName
+ * @return void
+ */
+ public function setConnectionName($connectionName)
+ {
+ $this->connectionName = $connectionName;
+ }
+
+ /**
+ * Get columns
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Set columns
+ *
+ * @param array $columns
+ * @return void
+ */
+ public function setColumns($columns)
+ {
+ $this->columns = $columns;
+ }
+
+ /**
+ * Get filters
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+
+ /**
+ * Set filters
+ *
+ * @param array $filters
+ * @return void
+ */
+ public function setFilters($filters)
+ {
+ $this->filters = $filters;
+ }
+
+ /**
+ * Get from condition
+ *
+ * @return array
+ */
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ /**
+ * Set from condition
+ *
+ * @param array $from
+ * @return void
+ */
+ public function setFrom($from)
+ {
+ $this->from = $from;
+ }
+
+ /**
+ * Process JOIN conditions
+ *
+ * @param Select $select
+ * @param array $joinConfig
+ * @return Select
+ */
+ private function processJoin(Select $select, $joinConfig)
+ {
+ switch ($joinConfig['link-type']) {
+ case 'left':
+ $select->joinLeft($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'inner':
+ $select->joinInner($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'right':
+ $select->joinRight($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ }
+ return $select;
+ }
+
+ /**
+ * Creates Select object
+ *
+ * @return Select
+ */
+ public function create()
+ {
+ $connection = $this->resourceConnection->getConnection($this->getConnectionName());
+ $select = $connection->select();
+ $select->from($this->getFrom(), []);
+ $select->columns($this->getColumns());
+ foreach ($this->getFilters() as $filter) {
+ $select->where($filter);
+ }
+ foreach ($this->getJoins() as $joinConfig) {
+ $select = $this->processJoin($select, $joinConfig);
+ }
+ if (!empty($this->getGroup())) {
+ $select->group(implode(', ', $this->getGroup()));
+ }
+ return $select;
+ }
+
+ /**
+ * Returns group
+ *
+ * @return array
+ */
+ public function getGroup()
+ {
+ return $this->group;
+ }
+
+ /**
+ * Set group
+ *
+ * @param array $group
+ * @return void
+ */
+ public function setGroup($group)
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * Get parameters
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Set parameters
+ *
+ * @param array $params
+ * @return void
+ */
+ public function setParams($params)
+ {
+ $this->params = $params;
+ }
+
+ /**
+ * Get having condition
+ *
+ * @return array
+ */
+ public function getHaving()
+ {
+ return $this->having;
+ }
+
+ /**
+ * Set having condition
+ *
+ * @param array $having
+ * @return void
+ */
+ public function setHaving($having)
+ {
+ $this->having = $having;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
new file mode 100644
index 0000000000000..1d88d4618efc5
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
@@ -0,0 +1,43 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Create class instance with specified parameters
+ *
+ * @param array $data
+ * @return SelectBuilder
+ */
+ public function create(array $data = [])
+ {
+ return $this->objectManager->create(SelectBuilder::class, $data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/IteratorFactory.php b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
new file mode 100644
index 0000000000000..a196cef8b66dc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
@@ -0,0 +1,61 @@
+objectManager = $objectManager;
+ $this->defaultIteratorName = $defaultIteratorName;
+ }
+
+ /**
+ * Creates instance of the result iterator with the query result as an input
+ * Result iterator can be changed through report configuration
+ *
+ * < ...
+ *
+ * Uses IteratorIterator by default
+ *
+ * @param \Traversable $result
+ * @param string|null $iteratorName
+ * @return \IteratorIterator
+ */
+ public function create(\Traversable $result, $iteratorName = null)
+ {
+ return $this->objectManager->create(
+ $iteratorName ?: $this->defaultIteratorName,
+ [
+ 'iterator' => $result
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Query.php b/app/code/Magento/Analytics/ReportXml/Query.php
new file mode 100644
index 0000000000000..46ec2fb494183
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Query.php
@@ -0,0 +1,96 @@
+select = $select;
+ $this->connectionName = $connectionName;
+ $this->selectHydrator = $selectHydrator;
+ $this->config = $config;
+ }
+
+ /**
+ * @return Select
+ */
+ public function getSelect()
+ {
+ return $this->select;
+ }
+
+ /**
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Specify data which should be serialized to JSON
+ * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
+ * @return mixed data which can be serialized by json_encode ,
+ * which is a value of any type other than a resource.
+ * @since 5.4.0
+ */
+ public function jsonSerialize()
+ {
+ return [
+ 'connectionName' => $this->getConnectionName(),
+ 'select_parts' => $this->selectHydrator->extract($this->getSelect()),
+ 'config' => $this->getConfig()
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/QueryFactory.php b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
new file mode 100644
index 0000000000000..8ed7e767b28b3
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
@@ -0,0 +1,142 @@
+config = $config;
+ $this->selectBuilderFactory = $selectBuilderFactory;
+ $this->assemblers = $assemblers;
+ $this->queryCache = $queryCache;
+ $this->objectManager = $objectManager;
+ $this->selectHydrator = $selectHydrator;
+ }
+
+ /**
+ * Returns query connection name according to configuration
+ *
+ * @param string $queryConfig
+ * @return string
+ */
+ private function getQueryConnectionName($queryConfig)
+ {
+ $connectionName = 'default';
+ if (isset($queryConfig['connection'])) {
+ $connectionName = $queryConfig['connection'];
+ }
+ return $connectionName;
+ }
+
+ /**
+ * Create query according to configuration settings
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ private function constructQuery($queryName)
+ {
+ $queryConfig = $this->config->get($queryName);
+ $selectBuilder = $this->selectBuilderFactory->create();
+ $selectBuilder->setConnectionName($this->getQueryConnectionName($queryConfig));
+ foreach ($this->assemblers as $assembler) {
+ $selectBuilder = $assembler->assemble($selectBuilder, $queryConfig);
+ }
+ $select = $selectBuilder->create();
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $select,
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $selectBuilder->getConnectionName(),
+ 'config' => $queryConfig
+ ]
+ );
+ }
+
+ /**
+ * Creates query by name
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ public function create($queryName)
+ {
+ $cached = $this->queryCache->load($queryName);
+ if ($cached) {
+ $queryData = json_decode($cached, true);
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $this->selectHydrator->recreate($queryData['select_parts']),
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $queryData['connectionName'],
+ 'config' => $queryData['config']
+ ]
+ );
+ }
+ $query = $this->constructQuery($queryName);
+ $this->queryCache->save(json_encode($query), $queryName);
+ return $query;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ReportProvider.php b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
new file mode 100644
index 0000000000000..3ebe5941108bc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
@@ -0,0 +1,76 @@
+queryFactory = $queryFactory;
+ $this->connectionFactory = $connectionFactory;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * Returns custom iterator name for report
+ * Null for default
+ *
+ * @param Query $query
+ * @return string|null
+ */
+ private function getIteratorName(Query $query)
+ {
+ $config = $query->getConfig();
+ return isset($config['iterator']) ? $config['iterator'] : null;
+ }
+
+ /**
+ * Returns report data by name and criteria
+ *
+ * @param string $name
+ * @return \IteratorIterator
+ */
+ public function getReport($name)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $statement = $connection->query($query->getSelect());
+ return $this->iteratorFactory->create($statement, $this->getIteratorName($query));
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/SelectHydrator.php b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
new file mode 100644
index 0000000000000..02cde2fd0598d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
@@ -0,0 +1,143 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ $this->selectParts = $selectParts;
+ }
+
+ /**
+ * @return array
+ */
+ private function getSelectParts()
+ {
+ return array_merge($this->predefinedSelectParts, $this->selectParts);
+ }
+
+ /**
+ * Extracts Select metadata parts
+ *
+ * @param Select $select
+ * @return array
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function extract(Select $select)
+ {
+ $parts = [];
+ foreach ($this->getSelectParts() as $partName) {
+ $parts[$partName] = $select->getPart($partName);
+ }
+ return $parts;
+ }
+
+ /**
+ * @param array $selectParts
+ * @return Select
+ */
+ public function recreate(array $selectParts)
+ {
+ $select = $this->resourceConnection->getConnection()->select();
+
+ $select = $this->processColumns($select, $selectParts);
+
+ foreach ($selectParts as $partName => $partValue) {
+ $select->setPart($partName, $partValue);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Process COLUMNS part values and add this part into select.
+ *
+ * If each column contains information about select expression
+ * an object with the type of this expression going to be created and assigned to this column.
+ *
+ * @param Select $select
+ * @param array $selectParts
+ * @return Select
+ */
+ private function processColumns(Select $select, array &$selectParts)
+ {
+ if (!empty($selectParts[Select::COLUMNS]) && is_array($selectParts[Select::COLUMNS])) {
+ $part = [];
+
+ foreach ($selectParts[Select::COLUMNS] as $columnEntry) {
+ list($correlationName, $column, $alias) = $columnEntry;
+ if (is_array($column) && !empty($column['class'])) {
+ $expression = $this->objectManager->create(
+ $column['class'],
+ isset($column['arguments']) ? $column['arguments'] : []
+ );
+ $part[] = [$correlationName, $expression, $alias];
+ } else {
+ $part[] = $columnEntry;
+ }
+ }
+
+ $select->setPart(Select::COLUMNS, $part);
+ unset($selectParts[Select::COLUMNS]);
+ }
+
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Analytics/Setup/InstallData.php b/app/code/Magento/Analytics/Setup/InstallData.php
new file mode 100644
index 0000000000000..aaa619bbb0caa
--- /dev/null
+++ b/app/code/Magento/Analytics/Setup/InstallData.php
@@ -0,0 +1,53 @@
+getConnection()->insertMultiple(
+ $setup->getTable('core_config_data'),
+ [
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => 'analytics/subscription/enabled',
+ 'value' => 1
+ ],
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => SubscriptionHandler::CRON_STRING_PATH,
+ 'value' => join(' ', SubscriptionHandler::CRON_EXPR_ARRAY)
+ ]
+ ]
+ );
+
+ $setup->getConnection()->insert(
+ $setup->getTable('flag'),
+ [
+ 'flag_code' => SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE,
+ 'state' => 0,
+ 'flag_data' => 24,
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
new file mode 100644
index 0000000000000..cbf06264096ac
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->additionalComment = $objectManager->getObject(
+ AdditionalComment::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getLabel')
+ ->willReturn('Comment label');
+ $html = $this->additionalComment->render($this->abstractElementMock);
+ $this->assertRegexp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegexp(
+ "/Comment label/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
new file mode 100644
index 0000000000000..a652cf6b3d548
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
@@ -0,0 +1,81 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->setMethods(['getLocaleDate'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->timeZoneMock = $this->getMockBuilder(TimezoneInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock->expects($this->any())
+ ->method('getLocaleDate')
+ ->willReturn($this->timeZoneMock);
+
+ $objectManager = new ObjectManager($this);
+ $this->collectionTimeLabel = $objectManager->getObject(
+ CollectionTimeLabel::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $timeZone = "America/New_York";
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->timeZoneMock->expects($this->once())
+ ->method('getConfigTimezone')
+ ->willReturn($timeZone);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Eastern Standard Time (America/New_York)');
+ $this->assertRegexp(
+ "/Eastern Standard Time \(America\/New_York\)/",
+ $this->collectionTimeLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
new file mode 100644
index 0000000000000..09e753e4ac8aa
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
@@ -0,0 +1,85 @@
+subscriptionStatusProviderMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->subscriptionStatusLabel = $objectManager->getObject(
+ SubscriptionStatusLabel::class,
+ [
+ 'context' => $this->contextMock,
+ 'subscriptionStatusProvider' => $this->subscriptionStatusProviderMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->subscriptionStatusProviderMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn('Enabled');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Subscription status: Enabled');
+ $this->assertRegexp(
+ "/Subscription status: Enabled/",
+ $this->subscriptionStatusLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
new file mode 100644
index 0000000000000..abce48c36c86a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel', 'getHint'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->vertical = $objectManager->getObject(
+ Vertical::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getHint')
+ ->willReturn('New hint');
+ $html = $this->vertical->render($this->abstractElementMock);
+ $this->assertRegexp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegExp(
+ "/New hint/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
new file mode 100644
index 0000000000000..6f613cdc4d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
@@ -0,0 +1,84 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultRedirectFactoryMock = $this->getMockBuilder(RedirectFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->signUpController = $this->objectManagerHelper->getObject(
+ SignUp::class,
+ [
+ 'config' => $this->configMock,
+ 'resultRedirectFactory' => $this->resultRedirectFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $urlBIEssentialsConfigPath = 'analytics/url/bi_essentials';
+ $this->configMock->expects($this->once())
+ ->method('getValue')
+ ->with($urlBIEssentialsConfigPath)
+ ->willReturn('value');
+ $this->resultRedirectFactoryMock->expects($this->once())->method('create')->willReturn($this->redirectMock);
+ $this->redirectMock->expects($this->once())->method('setUrl')->with('value')->willReturnSelf();
+ $this->assertEquals($this->redirectMock, $this->signUpController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
new file mode 100644
index 0000000000000..4f54ce5059965
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
@@ -0,0 +1,185 @@
+reportUrlProviderMock = $this->getMockBuilder(ReportUrlProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->showController = $this->objectManagerHelper->getObject(
+ Show::class,
+ [
+ 'reportUrlProvider' => $this->reportUrlProviderMock,
+ 'resultFactory' => $this->resultFactoryMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $otpUrl = 'http://example.com?otp=15vbjcfdvd15645';
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willReturn($otpUrl);
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setUrl')
+ ->with($otpUrl)
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @dataProvider executeWithExceptionDataProvider
+ *
+ * @param \Exception $exception
+ */
+ public function testExecuteWithException(\Exception $exception)
+ {
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ if ($exception instanceof LocalizedException) {
+ $message = $exception->getMessage();
+ } else {
+ $message = __('Sorry, there has been an error processing your request. Please try again later.');
+ }
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message)
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeWithExceptionDataProvider()
+ {
+ return [
+ 'ExecuteWithLocalizedException' => [new LocalizedException(__('TestMessage'))],
+ 'ExecuteWithException' => [new \Exception('TestMessage')],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithSubscriptionUpdateException()
+ {
+ $exception = new SubscriptionUpdateException(__('TestMessage'));
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addNoticeMessage')
+ ->with($exception->getMessage())
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
new file mode 100644
index 0000000000000..17c485a8df230
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
@@ -0,0 +1,159 @@
+resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultRedirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->retryController = $this->objectManagerHelper->getObject(
+ Retry::class,
+ [
+ 'resultFactory' => $this->resultFactoryMock,
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @dataProvider executeExceptionsDataProvider
+ *
+ * @param \Exception $exception
+ * @param Phrase $message
+ */
+ public function testExecuteWithException(\Exception $exception, Phrase $message)
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message);
+
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function executeExceptionsDataProvider()
+ {
+ return [
+ [new LocalizedException(__('TestMessage')), __('TestMessage')],
+ [
+ new \Exception('TestMessage'),
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
new file mode 100644
index 0000000000000..81c57d79033c8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
@@ -0,0 +1,91 @@
+exportDataHandlerMock = $this->getMockBuilder(ExportDataHandlerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectData = $this->objectManagerHelper->getObject(
+ CollectData::class,
+ [
+ 'exportDataHandler' => $this->exportDataHandlerMock,
+ 'subscriptionStatus' => $this->subscriptionStatusMock,
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ * @return void
+ * @dataProvider executeDataProvider
+ */
+ public function testExecute($status)
+ {
+ $this->subscriptionStatusMock
+ ->expects($this->once())
+ ->method('getStatus')
+ ->with()
+ ->willReturn($status);
+ $this->exportDataHandlerMock
+ ->expects(($status === SubscriptionStatusProvider::ENABLED) ? $this->once() : $this->never())
+ ->method('prepareExportData')
+ ->with();
+
+ $this->assertTrue($this->collectData->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeDataProvider()
+ {
+ return [
+ 'Subscription is enabled' => [SubscriptionStatusProvider::ENABLED],
+ 'Subscription is disabled' => [SubscriptionStatusProvider::DISABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
new file mode 100644
index 0000000000000..959a11f9e1058
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
@@ -0,0 +1,133 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUp = new SignUp(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->flagManagerMock,
+ $this->reinitableConfigMock
+ );
+ }
+
+ public function testExecute()
+ {
+ $attemptsCount = 10;
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+
+ $attemptsCount -= 1;
+ $this->flagManagerMock->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $this->connectorMock->expects($this->once())
+ ->method('execute')
+ ->with('signUp')
+ ->willReturn(true);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertTrue($this->signUp->execute());
+ }
+
+ public function testExecuteFlagNotExist()
+ {
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(null);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ public function testExecuteZeroAttempts()
+ {
+ $attemptsCount = 0;
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ /**
+ * Add assertions for method deleteAnalyticsCronExpr.
+ *
+ * @return void
+ */
+ private function addDeleteAnalyticsCronExprAsserts()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(SubscriptionHandler::CRON_STRING_PATH)
+ ->willReturn(true);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
new file mode 100644
index 0000000000000..ede53d8783a7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
@@ -0,0 +1,214 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->update = new Update(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->reinitableConfigMock,
+ $this->flagManagerMock,
+ $this->analyticsTokenMock
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithoutToken()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(10);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * @param bool $isExecuted
+ */
+ private function addFinalOutputAsserts(bool $isExecuted = true)
+ {
+ $this->flagManagerMock
+ ->expects($this->exactly(2 * $isExecuted))
+ ->method('deleteFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE]
+ );
+ $this->configWriterMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('delete')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfigMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('reinit')
+ ->with();
+ }
+
+ /**
+ * @param $counterData
+ * @return void
+ * @dataProvider executeWithEmptyReverseCounterDataProvider
+ */
+ public function testExecuteWithEmptyReverseCounter($counterData)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($counterData);
+ $this->connectorMock
+ ->expects($this->never())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * Provides empty states of the reverse counter.
+ *
+ * @return array
+ */
+ public function executeWithEmptyReverseCounterDataProvider()
+ {
+ return [
+ [null],
+ [0]
+ ];
+ }
+
+ /**
+ * @param int $reverseCount
+ * @param bool $commandResult
+ * @param bool $finalConditionsIsExpected
+ * @param bool $functionResult
+ * @return void
+ * @dataProvider executeRegularScenarioDataProvider
+ */
+ public function testExecuteRegularScenario(
+ int $reverseCount,
+ bool $commandResult,
+ bool $finalConditionsIsExpected,
+ bool $functionResult
+ ) {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($reverseCount);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn($commandResult);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts($finalConditionsIsExpected);
+ $this->assertSame($functionResult, $this->update->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeRegularScenarioDataProvider()
+ {
+ return [
+ 'The last attempt with command execution result False' => [
+ 'Reverse count' => 1,
+ 'Command result' => false,
+ 'Executed final output conditions' => true,
+ 'Function result' => false,
+ ],
+ 'Not the last attempt with command execution result False' => [
+ 'Reverse count' => 10,
+ 'Command result' => false,
+ 'Executed final output conditions' => false,
+ 'Function result' => false,
+ ],
+ 'Command execution result True' => [
+ 'Reverse count' => 10,
+ 'Command result' => true,
+ 'Executed final output conditions' => true,
+ 'Function result' => true,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
new file mode 100644
index 0000000000000..57315543bc32d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
@@ -0,0 +1,129 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->tokenModel = $this->objectManagerHelper->getObject(
+ AnalyticsToken::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'config' => $this->configMock,
+ 'configWriter' => $this->configWriterMock,
+ 'tokenPath' => $this->tokenPath,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testStoreToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->tokenPath, $value);
+
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+
+ $this->assertTrue($this->tokenModel->storeToken($value));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn($value);
+
+ $this->assertSame($value, $this->tokenModel->getToken());
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsTokenExist()
+ {
+ $this->assertFalse($this->tokenModel->isTokenExist());
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn('0000');
+ $this->assertTrue($this->tokenModel->isTokenExist());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
new file mode 100644
index 0000000000000..f5f721c038c57
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
@@ -0,0 +1,178 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionUpdateHandler = $this->objectManagerHelper->getObject(
+ SubscriptionUpdateHandler::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenDoesNotExist()
+ {
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate('http://store.com'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenAndPreviousBaseUrlExist()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenExistAndWithoutPreviousBaseUrl()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->exactly(2))
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url],
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
new file mode 100644
index 0000000000000..071b96111ac8b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
@@ -0,0 +1,111 @@
+configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectionTime = $this->objectManagerHelper->getObject(
+ CollectionTime::class,
+ [
+ 'configWriter' => $this->configWriterMock,
+ '_logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSave()
+ {
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']));
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->collectionTime->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWrongValue()
+ {
+ $this->collectionTime->setData('value', '00,01');
+ $this->collectionTime->afterSave();
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWithLocalizedException()
+ {
+ $exception = new \Exception('Test message');
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']))
+ ->willThrowException($exception);
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+ $this->collectionTime->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
new file mode 100644
index 0000000000000..82aa4dc72dfe0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
@@ -0,0 +1,152 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->tokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionHandler = $this->objectManagerHelper->getObject(
+ SubscriptionHandler::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ 'attemptsInitValue' => $this->attemptsInitValue,
+ 'analyticsToken' => $this->tokenMock,
+ ]
+ );
+ }
+
+ public function testProcessEnabledTokenExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessEnabledTokenDoesNotExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionHandler::CRON_STRING_PATH, "0 * * * *");
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessDisabledTokenDoesNotExist()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+
+ public function testProcessDisabledTokenExists()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('deleteFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
new file mode 100644
index 0000000000000..eea3193258bc6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
@@ -0,0 +1,184 @@
+subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->enabledModel = $this->objectManagerHelper->getObject(
+ Enabled::class,
+ [
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ '_logger' => $this->loggerMock,
+ 'config' => $this->configMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessEnabled()
+ {
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessDisabled()
+ {
+ $this->enabledModel->setData('value', $this->valueDisabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueDisabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessValueNotChanged()
+ {
+ $this->enabledModel->setData('value', null);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(null);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testExecuteAfterSaveFailedWithLocalizedException()
+ {
+ $exception = new \Exception('Message');
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+
+ $this->enabledModel->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
new file mode 100644
index 0000000000000..6fe7d0aa93998
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
@@ -0,0 +1,59 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testBeforeSaveSuccess()
+ {
+ $this->subject->setValue('Apps and Games');
+
+ $this->assertInstanceOf(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class,
+ $this->subject->beforeSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testBeforeSaveFailedWithLocalizedException()
+ {
+ $this->subject->setValue('');
+
+ $this->subject->beforeSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
new file mode 100644
index 0000000000000..0b7f4870dbac8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
@@ -0,0 +1,142 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->mapper = $this->objectManagerHelper->getObject(Mapper::class);
+ }
+
+ /**
+ * @param array $configData
+ * @param array $resultData
+ * @return void
+ *
+ * @dataProvider executingDataProvider
+ */
+ public function testExecution($configData, $resultData)
+ {
+ $this->assertSame($resultData, $this->mapper->execute($configData));
+ }
+
+ /**
+ * @return array
+ */
+ public function executingDataProvider()
+ {
+ return [
+ 'wrongConfig' => [
+ ['config' => ['files']],
+ []
+ ],
+ 'validConfigWithFileNodes' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [[]]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => []
+ ]
+ ],
+ ],
+ 'validConfigWithProvidersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [0 => []]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => ['parameters' => []]
+ ]
+ ]
+ ],
+ ],
+ 'validConfigWithParametersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [
+ 0 => [
+ 'parameters' => [
+ 0 => ['name' => ['reportName']]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => [
+ 'parameters' => [
+ 'name' => 'reportName'
+ ]
+ ]
+ ]
+ ]
+ ],
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
new file mode 100644
index 0000000000000..6aa9c7ef3106c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
@@ -0,0 +1,108 @@
+mapperMock = $this->getMockBuilder(Mapper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerXmlMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerDbMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reader = $this->objectManagerHelper->getObject(
+ Reader::class,
+ [
+ 'mapper' => $this->mapperMock,
+ 'readers' => [
+ $this->readerXmlMock,
+ $this->readerDbMock,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testRead()
+ {
+ $scope = 'store';
+ $xmlReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node4']]
+ ];
+ $dbReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node3']]
+ ];
+ $mapperResult = ['node2' => ['node3', 'node4']];
+
+ $this->readerXmlMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($xmlReaderResult);
+
+ $this->readerDbMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($dbReaderResult);
+
+ $this->mapperMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with(array_merge_recursive($xmlReaderResult, $dbReaderResult))
+ ->willReturn($mapperResult);
+
+ $this->assertSame($mapperResult, $this->reader->read($scope));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
new file mode 100644
index 0000000000000..c13205d34f25b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
@@ -0,0 +1,60 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Source\Vertical::class,
+ [
+ 'verticals' => [
+ 'Apps and Games',
+ 'Athletic/Sporting Goods',
+ 'Art and Design'
+ ]
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testToOptionArray()
+ {
+ $expectedOptionsArray = [
+ ['value' => '', 'label' => __('--Please Select--')],
+ ['value' => 'Apps and Games', 'label' => __('Apps and Games')],
+ ['value' => 'Athletic/Sporting Goods', 'label' => __('Athletic/Sporting Goods')],
+ ['value' => 'Art and Design', 'label' => __('Art and Design')]
+ ];
+
+ $this->assertEquals(
+ $expectedOptionsArray,
+ $this->subject->toOptionArray()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
new file mode 100644
index 0000000000000..8739219ebdf09
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
@@ -0,0 +1,68 @@
+dataInterfaceMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataInterfaceMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGet()
+ {
+ $key = 'configKey';
+ $defaultValue = 'mock';
+ $configValue = 'emptyString';
+
+ $this->dataInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($key, $defaultValue)
+ ->willReturn($configValue);
+
+ $this->assertSame($configValue, $this->config->get($key, $defaultValue));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
new file mode 100644
index 0000000000000..f8f3919b2489e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
@@ -0,0 +1,218 @@
+curlMock = $this->getMockBuilder(
+ \Magento\Framework\HTTP\Adapter\Curl::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock = $this->getMockBuilder(CurlFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->curlMock);
+
+ $this->responseFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\Connector\Http\ResponseFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->converterMock = $this->createJsonConverter();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Connector\Http\Client\Curl::class,
+ [
+ 'curlFactory' => $this->curlFactoryMock,
+ 'responseFactory' => $this->responseFactoryMock,
+ 'converter' => $this->converterMock,
+ 'logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ public function getTestData()
+ {
+ return [
+ [
+ 'data' => [
+ 'version' => '1.1',
+ 'body'=> ['name' => 'value'],
+ 'url' => 'http://www.mystore.com',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestSuccess(array $data)
+ {
+ $responseString = 'This is response.';
+ $response = new \Zend_Http_Response(201, [], $responseString);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read')
+ ->willReturn($responseString);
+ $this->curlMock->expects($this->any())
+ ->method('getErrno')
+ ->willReturn(0);
+
+ $this->responseFactoryMock->expects($this->any())
+ ->method('create')
+ ->with($responseString)
+ ->willReturn($response);
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestError(array $data)
+ {
+ $response = new \Zend_Http_Response(0, []);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read');
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getErrno')
+ ->willReturn(1);
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getError')
+ ->willReturn('CURL error.');
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with(
+ new \Exception(
+ 'MBI service CURL connection error #1: CURL error.'
+ )
+ );
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createJsonConverter()
+ {
+ $converterMock = $this->getMockBuilder(ConverterInterface::class)
+ ->getMockForAbstractClass();
+ $converterMock->expects($this->any())->method('toBody')->willReturnCallback(function ($value) {
+ return json_encode($value);
+ });
+ $converterMock->expects($this->any())
+ ->method('getContentTypeHeader')
+ ->willReturn(JsonConverter::CONTENT_TYPE_HEADER);
+ return $converterMock;
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
new file mode 100644
index 0000000000000..60a19f3d5079e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
@@ -0,0 +1,34 @@
+assertEquals(JsonConverter::CONTENT_TYPE_HEADER, $converter->getContentTypeHeader());
+ }
+
+ public function testConvertBody()
+ {
+ $body = '{"token": "secret-token"}';
+ $converter = new JsonConverter();
+ $this->assertEquals(json_decode($body, 1), $converter->fromBody($body));
+ }
+
+ public function testConvertData()
+ {
+ $data = ["token" => "secret-token"];
+ $converter = new JsonConverter();
+ $this->assertEquals(json_encode($data), $converter->toBody($data));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
new file mode 100644
index 0000000000000..7c3c484843285
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
@@ -0,0 +1,39 @@
+ 'testValue'];
+ $response = new \Zend_Http_Response(201, [], json_encode($expectedBody));
+ $responseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $responseHandlerMock->expects($this->once())
+ ->method('handleResponse')
+ ->with($expectedBody)
+ ->willReturn(true);
+ $notFoundResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $notFoundResponseHandlerMock->expects($this->never())->method('handleResponse');
+ $responseResolver = new ResponseResolver(
+ new JsonConverter(),
+ [
+ 201 => $responseHandlerMock,
+ 404 => $notFoundResponseHandlerMock,
+ ]
+ );
+ $this->assertTrue($responseResolver->getResult($response));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
new file mode 100644
index 0000000000000..cee3877631c2e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
@@ -0,0 +1,107 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $successHandler = $this->getMockBuilder(\Magento\Analytics\Model\Connector\Http\ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $successHandler->method('handleResponse')
+ ->willReturn(true);
+
+ $this->notifyDataChangedCommand = new NotifyDataChangedCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ new ResponseResolver(new JsonConverter(), [201 => $successHandler]),
+ $this->loggerMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::POST,
+ $configVal,
+ ['access-token' => $token, 'url' => $configVal]
+ )->willReturn(new \Zend_Http_Response(201, []));
+ $this->assertTrue($this->notifyDataChangedCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+ $this->assertFalse($this->notifyDataChangedCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
new file mode 100644
index 0000000000000..8a3f4efb15cf4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
@@ -0,0 +1,187 @@
+loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = new OTPRequest(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->responseResolverMock,
+ $this->loggerMock
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'otp' => 'thisisotp',
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['access-token' => 'thisisaccesstoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallSuccess()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(201, []));
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn($data['otp']);
+
+ $this->assertEquals(
+ $data['otp'],
+ $this->subject->call()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoAccessToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+
+ $this->assertFalse($this->subject->call());
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoOtp()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(0, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(false);
+
+ $this->loggerMock->expects($this->once())
+ ->method('warning');
+
+ $this->assertFalse($this->subject->call());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
new file mode 100644
index 0000000000000..0ff36cca5db2d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
@@ -0,0 +1,22 @@
+assertFalse($OTPHandler->handleResponse([]));
+ $expectedOtp = 123;
+ $this->assertEquals($expectedOtp, $OTPHandler->handleResponse(['otp' => $expectedOtp]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
new file mode 100644
index 0000000000000..707003149bcfd
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
@@ -0,0 +1,36 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with(null);
+ $subscriptionHandler = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider->method('getStatus')->willReturn(SubscriptionStatusProvider::ENABLED);
+ $reSignUpHandler = new ReSignUp($analyticsToken, $subscriptionHandler, $subscriptionStatusProvider);
+ $this->assertFalse($reSignUpHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
new file mode 100644
index 0000000000000..81711cfc56950
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
@@ -0,0 +1,30 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with($accessToken);
+ $signUpHandler = new SignUp($analyticsToken, new JsonConverter());
+ $this->assertFalse($signUpHandler->handleResponse([]));
+ $this->assertEquals($accessToken, $signUpHandler->handleResponse(['access-token' => $accessToken]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
new file mode 100644
index 0000000000000..7779357e8bea7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
@@ -0,0 +1,20 @@
+assertTrue($updateHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
new file mode 100644
index 0000000000000..5593496a957b7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
@@ -0,0 +1,174 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationManagerMock = $this->getMockBuilder(IntegrationManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationToken = $this->getMockBuilder(IntegrationToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUpCommand = new SignUpCommand(
+ $this->analyticsTokenMock,
+ $this->integrationManagerMock,
+ $this->configMock,
+ $this->httpClientMock,
+ $this->loggerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+ $this->integrationToken->expects($this->any())
+ ->method('getData')
+ ->with('token')
+ ->willReturn($data['integration-token']);
+ $httpResponse = new \Zend_Http_Response(201, [], '{"access-token": "' . $data['access-token'] . '"}');
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->with($httpResponse)
+ ->willReturn(true);
+ $this->assertTrue($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureCannotGenerateToken()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn(false);
+ $this->integrationManagerMock->expects($this->never())
+ ->method('activateIntegration');
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureResponseIsEmpty()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $httpResponse = new \Zend_Http_Response(0, []);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->willReturn(false);
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'integration-token' => 'thisisintegrationtoken',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['token' => 'thisisintegrationtoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
new file mode 100644
index 0000000000000..47253a13530e5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
@@ -0,0 +1,143 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->updateCommand = new UpdateCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->loggerMock,
+ $this->flagManagerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $url = "old.localhost.com";
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn($url);
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::PUT,
+ $configVal,
+ [
+ 'url' => $url,
+ 'new-url' => $configVal,
+ 'access-token' => $token
+ ]
+ )->willReturn(new \Zend_Http_Response(200, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(true);
+
+ $this->assertTrue($this->updateCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->assertFalse($this->updateCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
new file mode 100644
index 0000000000000..4414b81cbc183
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
@@ -0,0 +1,70 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->signUpCommandMock = $this->getMockBuilder(SignUpCommand::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->commands = ['signUp' => SignUpCommand::class];
+ $this->connector = new Connector($this->commands, $this->objectManagerMock);
+ }
+
+ public function testExecute()
+ {
+ $commandName = 'signUp';
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with($this->commands[$commandName])
+ ->willReturn($this->signUpCommandMock);
+ $this->signUpCommandMock->expects($this->once())
+ ->method('execute')
+ ->willReturn(true);
+ $this->assertTrue($this->connector->execute($commandName));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NotFoundException
+ */
+ public function testExecuteCommandNotFound()
+ {
+ $commandName = 'register';
+ $this->connector->execute($commandName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
new file mode 100644
index 0000000000000..a896c309b4007
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
@@ -0,0 +1,226 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextFactoryMock = $this->getMockBuilder(EncodedContextFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->key = '';
+ $this->source = '';
+ $this->initializationVectors = [];
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'encodedContextFactory' => $this->encodedContextFactoryMock,
+ 'cipherMethod' => $this->cipherMethod,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncode()
+ {
+ $token = 'some-token-value';
+ $this->source = 'Some text';
+ $this->key = hash('sha256', $token);
+
+ $checkEncodedContext = function ($parameters) {
+ $emptyRequiredParameters =
+ array_diff(['content', 'initializationVector'], array_keys(array_filter($parameters)));
+ if ($emptyRequiredParameters) {
+ return false;
+ }
+
+ $encryptedData = openssl_encrypt(
+ $this->source,
+ $this->cipherMethod,
+ $this->key,
+ OPENSSL_RAW_DATA,
+ $parameters['initializationVector']
+ );
+
+ return ($encryptedData === $parameters['content']);
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback($checkEncodedContext))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncodeUniqueInitializationVector()
+ {
+ $this->source = 'Some text';
+ $token = 'some-token-value';
+
+ $registerInitializationVector = function ($parameters) {
+ if (empty($parameters['initializationVector'])) {
+ return false;
+ }
+
+ $this->initializationVectors[] = $parameters['initializationVector'];
+
+ return true;
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->exactly(2))
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->exactly(2))
+ ->method('create')
+ ->with($this->callback($registerInitializationVector))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertCount(2, array_unique($this->initializationVectors));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @dataProvider encodeNotValidSourceDataProvider
+ */
+ public function testEncodeNotValidSource($source)
+ {
+ $this->cryptographer->encode($source);
+ }
+
+ /**
+ * @return array
+ */
+ public function encodeNotValidSourceDataProvider()
+ {
+ return [
+ 'Array' => [[]],
+ 'Empty string' => [''],
+ ];
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeNotValidCipherMethod()
+ {
+ $source = 'Some string';
+ $cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'cipherMethod' => 'Wrong-method',
+ ]
+ );
+
+ $cryptographer->encode($source);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeTokenNotValid()
+ {
+ $source = 'Some string';
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn(null);
+
+ $this->cryptographer->encode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
new file mode 100644
index 0000000000000..a1a7c54510681
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
@@ -0,0 +1,61 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string $content
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($content, $initializationVector)
+ {
+ $constructorArguments = [
+ 'content' => $content,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var EncodedContext $encodedContext */
+ $encodedContext = $this->objectManagerHelper->getObject(
+ EncodedContext::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($content, $encodedContext->getContent());
+ $this->assertSame($initializationVector ?: '', $encodedContext->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
new file mode 100644
index 0000000000000..1582c241bf45d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
@@ -0,0 +1,74 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @return void
+ */
+ public function testThatNotifyExecuted()
+ {
+ $expectedResult = true;
+ $notifyCommandName = 'notifyDataChanged';
+ $exportDataHandlerMockObject = $this->createExportDataHandlerMock();
+ $analyticsConnectorMockObject = $this->createAnalyticsConnectorMock();
+ /**
+ * @var $exportDataHandlerNotification ExportDataHandlerNotification
+ */
+ $exportDataHandlerNotification = $this->objectManagerHelper->getObject(
+ ExportDataHandlerNotification::class,
+ [
+ 'exportDataHandler' => $exportDataHandlerMockObject,
+ 'connector' => $analyticsConnectorMockObject,
+ ]
+ );
+ $exportDataHandlerMockObject->expects($this->once())
+ ->method('prepareExportData')
+ ->willReturn($expectedResult);
+ $analyticsConnectorMockObject->expects($this->once())
+ ->method('execute')
+ ->with($notifyCommandName);
+ $this->assertEquals($expectedResult, $exportDataHandlerNotification->prepareExportData());
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createExportDataHandlerMock()
+ {
+ return $this->getMockBuilder(ExportDataHandler::class)->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createAnalyticsConnectorMock()
+ {
+ return $this->getMockBuilder(Connector::class)->disableOriginalConstructor()->getMock();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
new file mode 100644
index 0000000000000..6ffb61d088567
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
@@ -0,0 +1,270 @@
+filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->archiveMock = $this->getMockBuilder(Archive::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->reportWriterMock = $this->getMockBuilder(ReportWriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->cryptographerMock = $this->getMockBuilder(Cryptographer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileRecorderMock = $this->getMockBuilder(FileRecorder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->exportDataHandler = $this->objectManagerHelper->getObject(
+ ExportDataHandler::class,
+ [
+ 'filesystem' => $this->filesystemMock,
+ 'archive' => $this->archiveMock,
+ 'reportWriter' => $this->reportWriterMock,
+ 'cryptographer' => $this->cryptographerMock,
+ 'fileRecorder' => $this->fileRecorderMock,
+ 'subdirectoryPath' => $this->subdirectoryPath,
+ 'archiveName' => $this->archiveName,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isArchiveSourceDirectory
+ * @dataProvider prepareExportDataDataProvider
+ */
+ public function testPrepareExportData($isArchiveSourceDirectory)
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archiveRelativePath = $this->subdirectoryPath . $this->archiveName;
+
+ $archiveSource = $isArchiveSourceDirectory ? (__DIR__) : '/tmp/' . $tmpFilesDirectoryPath;
+ $archiveAbsolutePath = '/tmp/' . $archiveRelativePath;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ );
+
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('getAbsolutePath')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ $archiveSource,
+ $archiveSource,
+ $archiveAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('isExist')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ true,
+ true
+ );
+
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(dirname($archiveRelativePath));
+
+ $this->archiveMock
+ ->expects($this->once())
+ ->method('pack')
+ ->with(
+ $archiveSource,
+ $archiveAbsolutePath,
+ $isArchiveSourceDirectory ? true : false
+ );
+
+ $fileContent = 'Some text';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('readFile')
+ ->with($archiveRelativePath)
+ ->willReturn($fileContent);
+
+ $this->cryptographerMock
+ ->expects($this->once())
+ ->method('encode')
+ ->with($fileContent)
+ ->willReturn($this->encodedContextMock);
+
+ $this->fileRecorderMock
+ ->expects($this->once())
+ ->method('recordNewFile')
+ ->with($this->encodedContextMock);
+
+ $this->assertTrue($this->exportDataHandler->prepareExportData());
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareExportDataDataProvider()
+ {
+ return [
+ 'Data source for archive is directory' => [true],
+ 'Data source for archive doesn\'t directory' => [false],
+ ];
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testPrepareExportDataWithLocalizedException()
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archivePath = $this->subdirectoryPath . $this->archiveName;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->exactly(3))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archivePath]
+ );
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('getAbsolutePath')
+ ->with($tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('isExist')
+ ->with($tmpFilesDirectoryPath)
+ ->willReturn(false);
+
+ $this->assertNull($this->exportDataHandler->prepareExportData());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
new file mode 100644
index 0000000000000..da5f6af3ca4e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
@@ -0,0 +1,194 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileInfoManager = $this->objectManagerHelper->getObject(
+ FileInfoManager::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'flagCode' => $this->flagCode,
+ 'encodedParameters' => $this->encodedParameters,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testSave()
+ {
+ $path = 'path/to/file';
+ $initializationVector = openssl_random_pseudo_bytes(16);
+ $parameters = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = base64_encode($parameters[$encodedParameter]);
+ }
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with($this->flagCode, $parameters);
+
+ $this->assertTrue($this->fileInfoManager->save($this->fileInfoMock));
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @dataProvider saveWithLocalizedExceptionDataProvider
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testSaveWithLocalizedException($path, $initializationVector)
+ {
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ $this->fileInfoManager->save($this->fileInfoMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function saveWithLocalizedExceptionDataProvider()
+ {
+ return [
+ 'Empty FileInfo' => [null, null],
+ 'FileInfo without IV' => ['path/to/file', null],
+ ];
+ }
+
+ /**
+ * @dataProvider loadDataProvider
+ * @param array|null $parameters
+ */
+ public function testLoad($parameters)
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with($this->flagCode)
+ ->willReturn($parameters);
+
+ $processedParameters = $parameters ?: [];
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($processedParameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $processedParameters[$encodedParameter] = base64_decode($processedParameters[$encodedParameter]);
+ }
+
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($processedParameters)
+ ->willReturn($this->fileInfoMock);
+
+ $this->assertSame($this->fileInfoMock, $this->fileInfoManager->load());
+ }
+
+ /**
+ * @return array
+ */
+ public function loadDataProvider()
+ {
+ return [
+ 'Empty flag data' => [null],
+ 'Correct flag data' => [[
+ 'path' => 'path/to/file',
+ 'initializationVector' => 'xUJjl54MVke+FvMFSBpRSA==',
+ ]],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
new file mode 100644
index 0000000000000..43ce833f1f03f
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
@@ -0,0 +1,62 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($path, $initializationVector)
+ {
+ $constructorArguments = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var FileInfo $fileInfo */
+ $fileInfo = $this->objectManagerHelper->getObject(
+ FileInfo::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($path ?: '', $fileInfo->getPath());
+ $this->assertSame($initializationVector ?: '', $fileInfo->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Degenerate object' => [null, null],
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
new file mode 100644
index 0000000000000..3c9520bdd995b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
@@ -0,0 +1,209 @@
+fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileRecorder = $this->objectManagerHelper->getObject(
+ FileRecorder::class,
+ [
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'filesystem' => $this->filesystemMock,
+ 'fileSubdirectoryPath' => $this->fileSubdirectoryPath,
+ 'encodedFileName' => $this->encodedFileName,
+ ]
+ );
+ }
+
+ /**
+ * @param string $pathToExistingFile
+ * @dataProvider recordNewFileDataProvider
+ */
+ public function testRecordNewFile($pathToExistingFile)
+ {
+ $content = openssl_random_pseudo_bytes(200);
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::MEDIA)
+ ->willReturn($this->directoryMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getContent')
+ ->with()
+ ->willReturn($content);
+
+ $hashLength = 64;
+ $fileRelativePathPattern = '#' . preg_quote($this->fileSubdirectoryPath, '#')
+ . '.{' . $hashLength . '}/' . preg_quote($this->encodedFileName, '#') . '#';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('writeFile')
+ ->with($this->matchesRegularExpression($fileRelativePathPattern), $content)
+ ->willReturn($this->directoryMock);
+
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('load')
+ ->with()
+ ->willReturn($this->fileInfoMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn('init_vector***');
+
+ /** register file */
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback(
+ function ($parameters) {
+ return !empty($parameters['path']) && ('init_vector***' === $parameters['initializationVector']);
+ }
+ ))
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->fileInfoMock);
+
+ /** remove old file */
+ $this->fileInfoMock
+ ->expects($this->exactly($pathToExistingFile ? 3 : 1))
+ ->method('getPath')
+ ->with()
+ ->willReturn($pathToExistingFile);
+ $directoryName = dirname($pathToExistingFile);
+ if ($directoryName === '.') {
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with($pathToExistingFile);
+ } elseif ($directoryName) {
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('delete')
+ ->withConsecutive(
+ [$pathToExistingFile],
+ [$directoryName]
+ );
+ }
+
+ $this->assertTrue($this->fileRecorder->recordNewFile($this->encodedContextMock));
+ }
+
+ /**
+ * @return array
+ */
+ public function recordNewFileDataProvider()
+ {
+ return [
+ 'File doesn\'t exist' => [''],
+ 'Existing file into subdirectory' => ['dir_name/file.txt'],
+ 'Existing file doesn\'t into subdirectory' => ['file.txt'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
new file mode 100644
index 0000000000000..3076a22c85be4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
@@ -0,0 +1,228 @@
+integrationServiceMock = $this->getMockBuilder(IntegrationServiceInterface::class)
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->oauthServiceMock = $this->getMockBuilder(OauthServiceInterface::class)
+ ->getMock();
+ $this->integrationMock = $this->getMockBuilder(Integration::class)
+ ->disableOriginalConstructor()
+ ->setMethods([
+ 'getId',
+ 'getConsumerId'
+ ])
+ ->getMock();
+ $this->integrationManager = $objectManagerHelper->getObject(
+ IntegrationManager::class,
+ [
+ 'integrationService' => $this->integrationServiceMock,
+ 'oauthService' => $this->oauthServiceMock,
+ 'config' => $this->configMock
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ *
+ * @return array
+ */
+ private function getIntegrationUserData($status)
+ {
+ return [
+ 'name' => 'ma-integration-user',
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testActivateIntegrationSuccess()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->exactly(2))
+ ->method('getId')
+ ->willReturn(100500);
+ $integrationData = $this->getIntegrationUserData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = 100500;
+ $this->configMock->expects($this->exactly(2))
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('update')
+ ->with($integrationData);
+ $this->assertTrue($this->integrationManager->activateIntegration());
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function testActivateIntegrationFailureNoSuchEntity()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(null);
+ $this->configMock->expects($this->once())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->never())
+ ->method('update');
+ $this->integrationManager->activateIntegration();
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenNewIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->at(0))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn(false);
+ $this->oauthServiceMock->expects($this->at(2))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->once())
+ ->method('createAccessToken')
+ ->with(100500, true)
+ ->willReturn(true);
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenExistingIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->once())
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->never())
+ ->method('createAccessToken');
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @return array
+ */
+ public function integrationIdDataProvider()
+ {
+ return [
+ [1],
+ [null],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
new file mode 100644
index 0000000000000..c7aa2219d1eee
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
@@ -0,0 +1,166 @@
+linkInterfaceFactoryMock = $this->getMockBuilder(LinkInterfaceFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+ $this->fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeManagerInterfaceMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->getMockForAbstractClass();
+ $this->linkInterfaceMock = $this->getMockBuilder(LinkInterface::class)
+ ->getMockForAbstractClass();
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeMock = $this->getMockBuilder(Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->linkProvider = $this->objectManagerHelper->getObject(
+ LinkProvider::class,
+ [
+ 'linkFactory' => $this->linkInterfaceFactoryMock,
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'storeManager' => $this->storeManagerInterfaceMock
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $baseUrl = 'http://magento.local/pub/media/';
+ $fileInfoPath = 'analytics/data.tgz';
+ $fileInitializationVector = 'er312esq23eqq';
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->linkInterfaceFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'initializationVector' => base64_encode($fileInitializationVector),
+ 'url' => $baseUrl . $fileInfoPath
+ ]
+ )
+ ->willReturn($this->linkInterfaceMock);
+ $this->storeManagerInterfaceMock->expects($this->once())
+ ->method('getStore')->willReturn($this->storeMock);
+ $this->storeMock->expects($this->once())
+ ->method('getBaseUrl')
+ ->with(
+ UrlInterface::URL_TYPE_MEDIA
+ )
+ ->willReturn($baseUrl);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->assertEquals($this->linkInterfaceMock, $this->linkProvider->get());
+ }
+
+ /**
+ * @param string|null $fileInfoPath
+ * @param string|null $fileInitializationVector
+ *
+ * @dataProvider fileNotReadyDataProvider
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ * @expectedExceptionMessage File is not ready yet.
+ */
+ public function testFileNotReady($fileInfoPath, $fileInitializationVector)
+ {
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoMock->expects($this->once())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->any())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->linkProvider->get();
+ }
+
+ /**
+ * @return array
+ */
+ public function fileNotReadyDataProvider()
+ {
+ return [
+ [null, 'initVector'],
+ ['path', null],
+ ['', 'initVector'],
+ ['path', ''],
+ ['', ''],
+ [null, null]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
new file mode 100644
index 0000000000000..a89e06562383b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
@@ -0,0 +1,147 @@
+subscriptionUpdateHandlerMock = $this->getMockBuilder(SubscriptionUpdateHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configValueMock = $this->getMockBuilder(Value::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isValueChanged', 'getPath', 'getScope', 'getOldValue'])
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->plugin = $this->objectManagerHelper->getObject(
+ BaseUrlConfigPlugin::class,
+ [
+ 'subscriptionUpdateHandler' => $this->subscriptionUpdateHandlerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $configValueData
+ * @return void
+ * @dataProvider afterSavePluginIsNotApplicableDataProvider
+ */
+ public function testAfterSavePluginIsNotApplicable(
+ array $configValueData
+ ) {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn($configValueData['isValueChanged']);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn($configValueData['path']);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn($configValueData['scope']);
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->never())
+ ->method('processUrlUpdate');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function afterSavePluginIsNotApplicableDataProvider()
+ {
+ return [
+ 'Value has not been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => false,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Unsecure URL has been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_UNSECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Secure URL has been changed not in the Default scope' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeInterface::SCOPE_STORES
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSavePluginIsApplicable()
+ {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn(true);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn(Store::XML_PATH_SECURE_BASE_URL);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ $this->configValueMock
+ ->method('getOldValue')
+ ->willReturn('http://store.com');
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->once())
+ ->method('processUrlUpdate')
+ ->with('http://store.com');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
new file mode 100644
index 0000000000000..0607a977e5b68
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
@@ -0,0 +1,149 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->otpRequestMock = $this->getMockBuilder(OTPRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportUrlProvider = $this->objectManagerHelper->getObject(
+ ReportUrlProvider::class,
+ [
+ 'config' => $this->configMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'otpRequest' => $this->otpRequestMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'urlReportConfigPath' => $this->urlReportConfigPath,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isTokenExist
+ * @param string|null $otp If null OTP was not received.
+ *
+ * @dataProvider getUrlDataProvider
+ */
+ public function testGetUrl($isTokenExist, $otp)
+ {
+ $reportUrl = 'https://example.com/report';
+ $url = '';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->urlReportConfigPath)
+ ->willReturn($reportUrl);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn($isTokenExist);
+ $this->otpRequestMock
+ ->expects($isTokenExist ? $this->once() : $this->never())
+ ->method('call')
+ ->with()
+ ->willReturn($otp);
+ if ($isTokenExist && $otp) {
+ $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&');
+ }
+ $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl());
+ }
+
+ /**
+ * @return array
+ */
+ public function getUrlDataProvider()
+ {
+ return [
+ 'TokenDoesNotExist' => [false, null],
+ 'TokenExistAndOtpEmpty' => [true, null],
+ 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetUrlWhenSubscriptionUpdateRunning()
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn('http://store.com');
+ $this->expectException(SubscriptionUpdateException::class);
+ $this->reportUrlProvider->getUrl();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
new file mode 100644
index 0000000000000..d9b030b84d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
@@ -0,0 +1,213 @@
+configInterfaceMock = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass();
+ $this->reportValidatorMock = $this->getMockBuilder(ReportValidator::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->providerFactoryMock = $this->getMockBuilder(ProviderFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->reportProviderMock = $this->getMockBuilder(ReportProvider::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportWriter = $this->objectManagerHelper->getObject(
+ ReportWriter::class,
+ [
+ 'config' => $this->configInterfaceMock,
+ 'reportValidator' => $this->reportValidatorMock,
+ 'providerFactory' => $this->providerFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWrite(array $configData)
+ {
+ $errors = [];
+ $fileData = [
+ ['number' => 1, 'type' => 'Shoes Usual']
+ ];
+ $this->configInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with()
+ ->willReturn([$configData]);
+ $this->providerFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->providerClass)
+ ->willReturn($this->reportProviderMock);
+ $parameterName = isset(reset($configData)[0]['parameters']['name'])
+ ? reset($configData)[0]['parameters']['name']
+ : '';
+ $this->reportProviderMock->expects($this->once())
+ ->method('getReport')
+ ->with($parameterName ?: null)
+ ->willReturn($fileData);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock
+ ->expects($this->once())
+ ->method('lock')
+ ->with();
+ $errorStreamMock
+ ->expects($this->exactly(2))
+ ->method('writeCsv')
+ ->withConsecutive(
+ [array_keys($fileData[0])],
+ [$fileData[0]]
+ );
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ if ($parameterName) {
+ $this->reportValidatorMock
+ ->expects($this->once())
+ ->method('validate')
+ ->with($parameterName)
+ ->willReturn($errors);
+ }
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('openFile')
+ ->with(
+ $this->stringContains('/var/tmp' . $parameterName ?: $this->reportName),
+ 'w+'
+ )->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWriteErrorFile($configData)
+ {
+ $errors = ['orders', 'SQL Error: test'];
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock->expects($this->once())->method('lock');
+ $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors);
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ $this->reportValidatorMock->expects($this->once())->method('validate')->willReturn($errors);
+ $this->directoryMock->expects($this->once())->method('openFile')->with('/var/tmp' . 'errors.csv', 'w+')
+ ->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testWriteEmptyReports()
+ {
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]);
+ $this->reportValidatorMock->expects($this->never())->method('validate');
+ $this->directoryMock->expects($this->never())->method('openFile');
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return array
+ */
+ public function configDataProvider()
+ {
+ return [
+ 'reportProvider' => [
+ [
+ 'providers' => [
+ [
+ 'name' => $this->providerName,
+ 'class' => $this->providerClass,
+ 'parameters' => [
+ 'name' => $this->reportName
+ ],
+ ]
+ ]
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
new file mode 100644
index 0000000000000..f314d77f32b41
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
@@ -0,0 +1,50 @@
+moduleManagerMock = $this->getMockBuilder(ModuleManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->moduleIterator = $objectManagerHelper->getObject(
+ ModuleIterator::class,
+ [
+ 'moduleManager' => $this->moduleManagerMock,
+ 'iterator' => new \ArrayIterator([0 => ['module_name' => 'Coco_Module']])
+ ]
+ );
+ }
+
+ public function testCurrent()
+ {
+ $this->moduleManagerMock->expects($this->once())
+ ->method('isEnabled')
+ ->with('Coco_Module')
+ ->willReturn(true);
+ foreach ($this->moduleIterator as $item) {
+ $this->assertEquals(['module_name' => 'Coco_Module', 'status' => 'Enabled'], $item);
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
new file mode 100644
index 0000000000000..cc46d175543ad
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
@@ -0,0 +1,123 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->websiteMock = $this->getMockBuilder(WebsiteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeMock = $this->getMockBuilder(StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configPaths = [
+ 'web/unsecure/base_url',
+ 'currency/options/base',
+ 'general/locale/timezone'
+ ];
+
+ $this->storeConfigurationProvider = new StoreConfigurationProvider(
+ $this->scopeConfigMock,
+ $this->storeManagerMock,
+ $this->configPaths
+ );
+ }
+
+ public function testGetReport()
+ {
+ $map = [
+ ['web/unsecure/base_url', 'default', 0, '127.0.0.1'],
+ ['currency/options/base', 'default', 0, 'USD'],
+ ['general/locale/timezone', 'default', 0, 'America/Dawson'],
+ ['web/unsecure/base_url', 'websites', 1, '127.0.0.2'],
+ ['currency/options/base', 'websites', 1, 'USD'],
+ ['general/locale/timezone', 'websites', 1, 'America/Belem'],
+ ['web/unsecure/base_url', 'stores', 2, '127.0.0.3'],
+ ['currency/options/base', 'stores', 2, 'USD'],
+ ['general/locale/timezone', 'stores', 2, 'America/Phoenix'],
+ ];
+
+ $this->scopeConfigMock
+ ->method('getValue')
+ ->will($this->returnValueMap($map));
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getWebsites')
+ ->willReturn([$this->websiteMock]);
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getStores')
+ ->willReturn([$this->storeMock]);
+
+ $this->websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->storeMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(2);
+ $result = iterator_to_array($this->storeConfigurationProvider->getReport());
+ $resultValues = [];
+ foreach ($result as $item) {
+ $resultValues[] = array_values($item);
+ }
+ array_multisort($resultValues);
+ array_multisort($map);
+ $this->assertEquals($resultValues, $map);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
new file mode 100644
index 0000000000000..d6b041ce03178
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
@@ -0,0 +1,196 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->statusProvider = $this->objectManagerHelper->getObject(
+ SubscriptionStatusProvider::class,
+ [
+ 'scopeConfig' => $this->scopeConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @dataProvider getStatusShouldBeFailedDataProvider
+ */
+ public function testGetStatusShouldBeFailed(array $flagManagerData)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::FAILED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBeFailedDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ 'Subscription update is active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @param bool $isTokenExist
+ * @dataProvider getStatusShouldBePendingDataProvider
+ */
+ public function testGetStatusShouldBePending(array $flagManagerData, bool $isTokenExist)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn($isTokenExist);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::PENDING, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBePendingDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and token exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ 'isTokenExist' => true,
+ ],
+ ];
+ }
+
+ public function testGetStatusShouldBeEnabled()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(null);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+ $this->assertEquals(SubscriptionStatusProvider::ENABLED, $this->statusProvider->getStatus());
+ }
+
+ public function testGetStatusShouldBeDisabled()
+ {
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(false);
+ $this->assertEquals(SubscriptionStatusProvider::DISABLED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @param array $mapping
+ */
+ private function expectFlagManagerReturn(array $mapping)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->willReturnMap($mapping);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
new file mode 100644
index 0000000000000..ad1d87488d751
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
@@ -0,0 +1,106 @@
+subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->urlBuilderMock = $this->getMockBuilder(UrlInterface::class)
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->notification = $this->objectManagerHelper->getObject(
+ NotificationAboutFailedSubscription::class,
+ [
+ 'subscriptionStatusProvider' => $this->subscriptionStatusMock,
+ 'urlBuilder' => $this->urlBuilderMock
+ ]
+ );
+ }
+
+ public function testIsDisplayedWhenMessageShouldBeDisplayed()
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn(
+ SubscriptionStatusProvider::FAILED
+ );
+ $this->assertTrue($this->notification->isDisplayed());
+ }
+
+ /**
+ * @dataProvider notDisplayedNotificationStatuses
+ *
+ * @param $status
+ */
+ public function testIsDisplayedWhenMessageShouldNotBeDisplayed($status)
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn($status);
+ $this->assertFalse($this->notification->isDisplayed());
+ }
+
+ public function testGetTextShouldBuildMessage()
+ {
+ $retryUrl = 'http://magento.dev/retryUrl';
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('analytics/subscription/retry')
+ ->willReturn($retryUrl);
+ $messageDetails = 'Failed to synchronize data to the Magento Business Intelligence service. ';
+ $messageDetails .= sprintf('Retry Synchronization ', $retryUrl);
+ $this->assertEquals($messageDetails, $this->notification->getText());
+ }
+
+ /**
+ * Provide statuses according to which message should not be displayed.
+ *
+ * @return array
+ */
+ public function notDisplayedNotificationStatuses()
+ {
+ return [
+ [SubscriptionStatusProvider::PENDING],
+ [SubscriptionStatusProvider::DISABLED],
+ [SubscriptionStatusProvider::ENABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
new file mode 100644
index 0000000000000..3f1ed9a5cf4c0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
@@ -0,0 +1,121 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\Config\Converter\Xml::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvertNoElements()
+ {
+ $this->assertEmpty(
+ $this->subject->convert(new \DOMDocument())
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvert()
+ {
+ $dom = new \DOMDocument();
+
+ $expectedArray = [
+ 'config' => [
+ [
+ 'noNamespaceSchemaLocation' => 'urn:magento:module:Magento_Analytics:etc/reports.xsd',
+ 'report' => [
+ [
+ 'name' => 'test_report_1',
+ 'connection' => 'sales',
+ 'source' => [
+ [
+ 'name' => 'sales_order',
+ 'alias' => 'orders',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id',
+ 'alias' => 'identifier',
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'gt',
+ '_value' => '10'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'name' => 'test_report_2',
+ 'connection' => 'default',
+ 'source' => [
+ [
+ 'name' => 'customer_entity',
+ 'alias' => 'customers',
+ 'attribute' => [
+ [
+ 'name' => 'email'
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'dob',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $dom->loadXML(file_get_contents(__DIR__ . '/../_files/valid_reports.xml'));
+
+ $this->assertEquals($expectedArray, $this->subject->convert($dom));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
new file mode 100644
index 0000000000000..85343b6b301d6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
@@ -0,0 +1,47 @@
+mapper = new Mapper();
+ }
+
+ public function testExecute()
+ {
+ $configData['config'][0]['report'] = [
+ [
+ 'source' => ['product'],
+ 'name' => 'Product',
+ ]
+ ];
+ $expectedResult = [
+ 'Product' => [
+ 'source' => 'product',
+ 'name' => 'Product',
+ ]
+ ];
+ $this->assertEquals($this->mapper->execute($configData), $expectedResult);
+ }
+
+ public function testExecuteWithoutReports()
+ {
+ $configData = [];
+ $this->assertEquals($this->mapper->execute($configData), []);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
new file mode 100644
index 0000000000000..e04ee96163797
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
new file mode 100644
index 0000000000000..cbc9aa129d874
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
@@ -0,0 +1,64 @@
+dataMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataMock,
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $queryName = 'query string';
+ $queryResult = [ 'query' => 1 ];
+
+ $this->dataMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryResult);
+
+ $this->assertSame($queryResult, $this->config->get($queryName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
new file mode 100644
index 0000000000000..1e4ae9142c13d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
@@ -0,0 +1,106 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionNewMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->connectionFactory = $this->objectManagerHelper->getObject(
+ ConnectionFactory::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testGetConnection()
+ {
+ $connectionName = 'read';
+
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('getConfig')
+ ->with()
+ ->willReturn(['persistent' => 1]);
+
+ $this->objectManagerMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(get_class($this->connectionMock), ['config' => ['use_buffered_query' => false]])
+ ->willReturn($this->connectionNewMock);
+
+ $this->assertSame($this->connectionNewMock, $this->connectionFactory->getConnection($connectionName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
new file mode 100644
index 0000000000000..3b01105a8873b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
@@ -0,0 +1,143 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleNotEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+
+ $this->conditionResolverMock->expects($this->once())
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['filter'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(sales.entity_id IS NULL)');
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with(['(sales.entity_id IS NULL)']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
new file mode 100644
index 0000000000000..575db94a7b7e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
@@ -0,0 +1,167 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FromAssembler::class,
+ [
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider assembleDataProvider
+ * @param array $queryConfig
+ * @param string $tableName
+ * @return void
+ */
+ public function testAssemble(array $queryConfig, $tableName)
+ {
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['alias']);
+
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['name']);
+
+ $this->resourceConnection
+ ->expects($this->once())
+ ->method('getTableName')
+ ->with($queryConfig['source']['name'])
+ ->willReturn($tableName);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFrom')
+ ->with([$queryConfig['source']['alias'] => $tableName]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfig['source'])
+ ->willReturn(['entity_id' => 'sales.entity_id']);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(['entity_id' => 'sales.entity_id']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfig)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleDataProvider()
+ {
+ return [
+ 'Tables without prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'sales_order',
+ ],
+ 'Tables with prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'pref_sales_order',
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
new file mode 100644
index 0000000000000..aaafd731552a0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
@@ -0,0 +1,279 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getJoins')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setColumns');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setJoins');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @param array $queryConfigMock
+ * @param array $joinsMock
+ * @param array $tablesMapping
+ * @return void
+ * @dataProvider assembleNotEmptyDataProvider
+ */
+ public function testAssembleNotEmpty(array $queryConfigMock, array $joinsMock, array $tablesMapping)
+ {
+ $filtersMock = [];
+
+ $this->nameResolverMock->expects($this->at(0))
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+ $this->nameResolverMock->expects($this->at(1))
+ ->method('getAlias')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['alias']);
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['name']);
+
+ $this->resourceConnection
+ ->expects($this->any())
+ ->method('getTableName')
+ ->willReturnOnConsecutiveCalls(...array_values($tablesMapping));
+
+ $this->conditionResolverMock->expects($this->at(0))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['using'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(billing.parent_id = `sales`.`entity_id`)');
+
+ if (isset($queryConfigMock['source']['link-source'][0]['filter'])) {
+ $filtersMock = ['(sales.entity_id IS NULL)'];
+
+ $this->conditionResolverMock->expects($this->at(1))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['filter'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn($filtersMock[0]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfigMock['source']['link-source'][0])
+ ->willReturn(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+ }
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with($filtersMock);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setJoins')
+ ->with($joinsMock);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleNotEmptyDataProvider()
+ {
+ return [
+ [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'link-source' => [
+ [
+ 'name' => 'sales_order_address',
+ 'alias' => 'billing',
+ 'link-type' => 'left',
+ 'attribute' => [
+ [
+ 'alias' => 'billing_address_id',
+ 'name' => 'entity_id'
+ ]
+ ],
+ 'using' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'parent_id',
+ 'operator' => 'eq',
+ 'type' => 'identifier',
+ '_value' => 'entity_id'
+ ]
+ ]
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'billing' => [
+ 'link-type' => 'left',
+ 'table' => [
+ 'billing' => 'pref_sales_order_address'
+ ],
+ 'condition' => '(billing.parent_id = `sales`.`entity_id`)'
+ ]
+ ],
+ ['sales_order_address' => 'pref_sales_order_address']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
new file mode 100644
index 0000000000000..bdbe3d1d22c22
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
@@ -0,0 +1,150 @@
+selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManagerHelper($this);
+ $this->columnsResolver = $objectManager->getObject(
+ ColumnsResolver::class,
+ [
+ 'nameResolver' => new NameResolver(),
+ 'resourceConnection' => $this->resourceConnectionMock
+ ]
+ );
+ }
+
+ public function testGetColumnsWithoutAttributes()
+ {
+ $this->assertEquals($this->columnsResolver->getColumns($this->selectBuilderMock, []), []);
+ }
+
+ /**
+ * @dataProvider getColumnsDataProvider
+ */
+ public function testGetColumnsWithFunction($expectedColumns, $expectedGroup, $entityConfig)
+ {
+ $this->resourceConnectionMock->expects($this->any())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->any())
+ ->method('quoteIdentifier')
+ ->with('cpe.name')
+ ->willReturn('`cpe`.`name`');
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getGroup')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setGroup')
+ ->with($expectedGroup);
+ $this->assertEquals(
+ $expectedColumns,
+ $this->columnsResolver->getColumns(
+ $this->selectBuilderMock,
+ $entityConfig
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function getColumnsDataProvider()
+ {
+ return [
+ 'COUNT( DISTINCT `cpe`.`name`) AS name' => [
+ 'expectedColumns' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'expectedGroup' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'function' => 'COUNT',
+ 'distinct' => true,
+ 'group' => true
+ ]
+ ],
+ ],
+ ],
+ 'AVG(`cpe`.`name`) AS avg_name' => [
+ 'expectedColumns' => [
+ 'avg_name' => new ColumnValueExpression('AVG(`cpe`.`name`)')
+ ],
+ 'expectedGroup' => [],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'alias' => 'avg_name',
+ 'function' => 'AVG',
+ ]
+ ],
+ ],
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
new file mode 100644
index 0000000000000..c8182d068fba5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
@@ -0,0 +1,108 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolver = new ConditionResolver($this->resourceConnectionMock);
+ }
+
+ public function testGetFilter()
+ {
+ $condition = ["type" => "variable", "_value" => "1", "attribute" => "id", "operator" => "neq"];
+ $valueCondition = ["type" => "value", "_value" => "2", "attribute" => "first_name", "operator" => "eq"];
+ $identifierCondition = [
+ "type" => "identifier",
+ "_value" => "other_field",
+ "attribute" => "last_name",
+ "operator" => "eq"];
+ $filter = [["glue" => "AND", "condition" => [$valueCondition]]];
+ $filterConfig = [
+ ["glue" => "OR", "condition" => [$condition], 'filter' => $filter],
+ ["glue" => "OR", "condition" => [$identifierCondition]],
+ ];
+ $aliasName = 'n';
+ $this->selectBuilderMock->expects($this->any())
+ ->method('setParams')
+ ->with(array_merge([], [$condition['_value']]));
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getParams')
+ ->willReturn([]);
+
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn(['price' => new Expression("(n.price = 400)")]);
+
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock->expects($this->any())
+ ->method('quote')
+ ->willReturn("'John'");
+ $this->connectionMock->expects($this->exactly(4))
+ ->method('quoteIdentifier')
+ ->willReturnMap([
+ ['n.id', false, '`n`.`id`'],
+ ['n.first_name', false, '`n`.`first_name`'],
+ ['n.last_name', false, '`n`.`last_name`'],
+ ['other_field', false, '`other_field`'],
+ ]);
+
+ $result = "(`n`.`id` != 1 OR ((`n`.`first_name` = 'John'))) OR (`n`.`last_name` = `other_field`)";
+ $this->assertEquals(
+ $result,
+ $this->conditionResolver->getFilter($this->selectBuilderMock, $filterConfig, $aliasName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
new file mode 100644
index 0000000000000..4accd03aef3ea
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
@@ -0,0 +1,90 @@
+nameResolverMock = $this->getMockBuilder(NameResolver::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName'])
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->nameResolver = $this->objectManagerHelper->getObject(NameResolver::class);
+ }
+
+ public function testGetName()
+ {
+ $elementConfigMock = [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ ];
+
+ $this->assertSame('sales_order', $this->nameResolver->getName($elementConfigMock));
+ }
+
+ /**
+ * @param array $elementConfig
+ * @param string|null $elementAlias
+ *
+ * @dataProvider getAliasDataProvider
+ */
+ public function testGetAlias($elementConfig, $elementAlias)
+ {
+ $elementName = 'elementName';
+
+ $this->nameResolverMock
+ ->expects($this->once())
+ ->method('getName')
+ ->with($elementConfig)
+ ->willReturn($elementName);
+
+ $this->assertSame($elementAlias ?: $elementName, $this->nameResolverMock->getAlias($elementConfig));
+ }
+
+ /**
+ * @return array
+ */
+ public function getAliasDataProvider()
+ {
+ return [
+ 'ElementConfigWithAliases' => [
+ ['alias' => 'sales', 'name' => 'sales_order'],
+ 'sales',
+ ],
+ 'ElementConfigWithoutAliases' => [
+ ['name' => 'sales_order'],
+ null,
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
new file mode 100644
index 0000000000000..bbb9ca4b511b6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
@@ -0,0 +1,125 @@
+connectionFactoryMock = $this->getMockBuilder(ConnectionFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryFactoryMock = $this->getMockBuilder(QueryFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryMock = $this->getMockBuilder(Query::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass();
+ $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportValidator = $this->objectManagerHelper->getObject(
+ ReportValidator::class,
+ [
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'queryFactory' => $this->queryFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider errorDataProvider
+ * @param string $reportName
+ * @param array $result
+ * @param \PHPUnit_Framework_MockObject_Stub $queryReturnStub
+ */
+ public function testValidate($reportName, $result, \PHPUnit_Framework_MockObject_Stub $queryReturnStub)
+ {
+ $connectionName = 'testConnection';
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->queryMock);
+ $this->queryMock->expects($this->once())->method('getConnectionName')->willReturn($connectionName);
+ $this->connectionFactoryMock->expects($this->once())->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->queryMock->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())->method('limit')->with(0);
+ $this->connectionMock->expects($this->once())->method('query')->with($this->selectMock)->will($queryReturnStub);
+ $this->assertEquals($result, $this->reportValidator->validate($reportName));
+ }
+
+ /**
+ * Provide variations of the error returning
+ *
+ * @return array
+ */
+ public function errorDataProvider()
+ {
+ $reportName = 'test';
+ $errorMessage = 'SQL Error 42';
+ return [
+ [
+ $reportName,
+ 'expectedResult' => [],
+ 'queryReturnStub' => $this->returnValue(null)
+ ],
+ [
+ $reportName,
+ 'expectedResult' => [$reportName, $errorMessage],
+ 'queryReturnStub' => $this->throwException(new \Zend_Db_Statement_Exception($errorMessage))
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
new file mode 100644
index 0000000000000..a82a187cdb3f8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
@@ -0,0 +1,103 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilder = new SelectBuilder($this->resourceConnectionMock);
+ }
+
+ public function testCreate()
+ {
+ $connectionName = 'MySql';
+ $from = ['customer c'];
+ $columns = ['id', 'name', 'price'];
+ $filter = 'filter';
+ $joins = [
+ ['link-type' => 'left', 'table' => 'customer', 'condition' => 'in'],
+ ['link-type' => 'inner', 'table' => 'price', 'condition' => 'eq'],
+ ['link-type' => 'right', 'table' => 'attribute', 'condition' => 'neq'],
+ ];
+ $groups = ['id', 'name'];
+ $this->selectBuilder->setConnectionName($connectionName);
+ $this->selectBuilder->setFrom($from);
+ $this->selectBuilder->setColumns($columns);
+ $this->selectBuilder->setFilters([$filter]);
+ $this->selectBuilder->setJoins($joins);
+ $this->selectBuilder->setGroup($groups);
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())
+ ->method('from')
+ ->with($from, []);
+ $this->selectMock->expects($this->once())
+ ->method('columns')
+ ->with($columns);
+ $this->selectMock->expects($this->once())
+ ->method('where')
+ ->with($filter);
+ $this->selectMock->expects($this->once())
+ ->method('joinLeft')
+ ->with($joins[0]['table'], $joins[0]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinInner')
+ ->with($joins[1]['table'], $joins[1]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinRight')
+ ->with($joins[2]['table'], $joins[2]['condition'], []);
+ $this->selectBuilder->create();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
new file mode 100644
index 0000000000000..1d3f293ed676a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
@@ -0,0 +1,59 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorIteratorMock = $this->getMockBuilder(\IteratorIterator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactory = new IteratorFactory(
+ $this->objectManagerMock
+ );
+ }
+
+ public function testCreate()
+ {
+ $arrayObject = new \ArrayIterator([1, 2, 3, 4, 5]);
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(\IteratorIterator::class, ['iterator' => $arrayObject])
+ ->willReturn($this->iteratorIteratorMock);
+
+ $this->assertEquals($this->iteratorFactory->create($arrayObject), $this->iteratorIteratorMock);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
new file mode 100644
index 0000000000000..9a3805a50f167
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
@@ -0,0 +1,239 @@
+queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Config::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->assemblerMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\Assembler\AssemblerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryCacheMock = $this->getMockBuilder(
+ \Magento\Framework\App\CacheInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(
+ \Magento\Framework\ObjectManagerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\SelectHydrator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilderFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\QueryFactory::class,
+ [
+ 'config' => $this->configMock,
+ 'selectBuilderFactory' => $this->selectBuilderFactoryMock,
+ 'assemblers' => [$this->assemblerMock],
+ 'queryCache' => $this->queryCacheMock,
+ 'objectManager' => $this->objectManagerMock,
+ 'selectHydrator' => $this->selectHydratorMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateCached()
+ {
+ $queryName = 'test_query';
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn('{"connectionName":"sales","config":{},"select_parts":{}}');
+
+ $this->selectHydratorMock->expects($this->any())
+ ->method('recreate')
+ ->with([])
+ ->willReturn($this->selectMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => 'sales',
+ 'config' => []
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->never())
+ ->method('save');
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateNotCached()
+ {
+ $queryName = 'test_query';
+
+ $queryConfigMock = [
+ 'name' => 'test_query',
+ 'connection' => 'sales'
+ ];
+
+ $selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $selectBuilderMock->expects($this->once())
+ ->method('setConnectionName')
+ ->with($queryConfigMock['connection']);
+ $selectBuilderMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->selectMock);
+ $selectBuilderMock->expects($this->any())
+ ->method('getConnectionName')
+ ->willReturn($queryConfigMock['connection']);
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn(null);
+
+ $this->configMock->expects($this->any())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryConfigMock);
+
+ $this->selectBuilderFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($selectBuilderMock);
+
+ $this->assemblerMock->expects($this->once())
+ ->method('assemble')
+ ->with($selectBuilderMock, $queryConfigMock)
+ ->willReturn($selectBuilderMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => $queryConfigMock['connection'],
+ 'config' => $queryConfigMock
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->once())
+ ->method('save')
+ ->with(json_encode($this->queryMock), $queryName);
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
new file mode 100644
index 0000000000000..a4b08a9ce5e0a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
@@ -0,0 +1,90 @@
+selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(selectHydrator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->query = $this->objectManagerHelper->getObject(
+ Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'connectionName' => $this->connectionName,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'config' => []
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testJsonSerialize()
+ {
+ $selectParts = ['part' => 1];
+
+ $this->selectHydratorMock
+ ->expects($this->once())
+ ->method('extract')
+ ->with($this->selectMock)
+ ->willReturn($selectParts);
+
+ $expectedResult = [
+ 'connectionName' => $this->connectionName,
+ 'select_parts' => $selectParts,
+ 'config' => []
+ ];
+
+ $this->assertSame($expectedResult, $this->query->jsonSerialize());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
new file mode 100644
index 0000000000000..5f329993dd291
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
@@ -0,0 +1,180 @@
+selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->queryMock->expects($this->any())
+ ->method('getSelect')
+ ->willReturn($this->selectMock);
+
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->statementMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Statement\Pdo\Mysql::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->statementMock->expects($this->any())
+ ->method('getIterator')
+ ->willReturn($this->iteratorMock);
+
+ $this->connectionMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Adapter\AdapterInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\QueryFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\IteratorFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->connectionFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\ConnectionFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\ReportProvider::class,
+ [
+ 'queryFactory' => $this->queryFactoryMock,
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'iteratorFactory' => $this->iteratorFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetReport()
+ {
+ $reportName = 'test_report';
+ $connectionName = 'sales';
+
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($reportName)
+ ->willReturn($this->queryMock);
+
+ $this->connectionFactoryMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConnectionName')
+ ->willReturn($connectionName);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConfig')
+ ->willReturn(
+ [
+ 'connection' => $connectionName
+ ]
+ );
+
+ $this->connectionMock->expects($this->once())
+ ->method('query')
+ ->with($this->selectMock)
+ ->willReturn($this->statementMock);
+
+ $this->iteratorFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($this->statementMock, null)
+ ->willReturn($this->iteratorMock);
+ $this->assertEquals($this->iteratorMock, $this->subject->getReport($reportName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
new file mode 100644
index 0000000000000..ce57a1eca3689
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
@@ -0,0 +1,257 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->selectHydrator = $this->objectManagerHelper->getObject(
+ SelectHydrator::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testExtract()
+ {
+ $selectParts =
+ [
+ Select::DISTINCT,
+ Select::COLUMNS,
+ Select::UNION,
+ Select::FROM,
+ Select::WHERE,
+ Select::GROUP,
+ Select::HAVING,
+ Select::ORDER,
+ Select::LIMIT_COUNT,
+ Select::LIMIT_OFFSET,
+ Select::FOR_UPDATE
+ ];
+
+ $result = [];
+ foreach ($selectParts as $part) {
+ $result[$part] = "Part";
+ }
+ $this->selectMock->expects($this->any())
+ ->method('getPart')
+ ->willReturn("Part");
+ $this->assertEquals($this->selectHydrator->extract($this->selectMock), $result);
+ }
+
+ /**
+ * @dataProvider recreateWithoutExpressionDataProvider
+ * @param array $selectParts
+ * @param array $parts
+ * @param array $partValues
+ */
+ public function testRecreateWithoutExpression($selectParts, $parts, $partValues)
+ {
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ foreach ($parts as $key => $part) {
+ $this->selectMock->expects($this->at($key))
+ ->method('setPart')
+ ->with($part, $partValues[$key]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithoutExpressionDataProvider()
+ {
+ return [
+ 'Select without expressions' => [
+ [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]
+ ],
+ [Select::COLUMNS],
+ [[
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider recreateWithExpressionDataProvider
+ * @param array $selectParts
+ * @param array $expectedParts
+ * @param \PHPUnit_Framework_MockObject_MockObject[] $expressionMocks
+ */
+ public function testRecreateWithExpression(
+ array $selectParts,
+ array $expectedParts,
+ array $expressionMocks
+ ) {
+ $this->objectManagerMock
+ ->expects($this->exactly(count($expressionMocks)))
+ ->method('create')
+ ->with($this->isType('string'), $this->isType('array'))
+ ->willReturnOnConsecutiveCalls(...$expressionMocks);
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with()
+ ->willReturn($this->connectionMock);
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('select')
+ ->with()
+ ->willReturn($this->selectMock);
+ foreach (array_keys($selectParts) as $key => $partName) {
+ $this->selectMock
+ ->expects($this->at($key))
+ ->method('setPart')
+ ->with($partName, $expectedParts[$partName]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithExpressionDataProvider()
+ {
+ $expressionMock = $this->getMockBuilder(JsonSerializableExpression::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return [
+ 'Select without expressions' => [
+ 'Parts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ [
+ 'class' => 'Some_class',
+ 'arguments' => [
+ 'expression' => ['some(expression)']
+ ]
+ ],
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedParts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ $expressionMock,
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedExpressions' => [
+ $expressionMock
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json
new file mode 100644
index 0000000000000..349e5f3c08c4c
--- /dev/null
+++ b/app/code/Magento/Analytics/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "magento/module-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/module-backend": "100.2.*",
+ "magento/module-config": "101.0.*",
+ "magento/module-integration": "100.2.*",
+ "magento/module-store": "100.2.*",
+ "magento/framework": "101.0.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\Analytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/docs/images/M2_MA_signup.png b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png
new file mode 100644
index 0000000000000..78ed8fad92881
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/analytics_modules.png b/app/code/Magento/Analytics/docs/images/analytics_modules.png
new file mode 100644
index 0000000000000..0bf6048b0d9cc
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/analytics_modules.png differ
diff --git a/app/code/Magento/Analytics/docs/images/data_transition.png b/app/code/Magento/Analytics/docs/images/data_transition.png
new file mode 100644
index 0000000000000..a75e97983e15d
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/data_transition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/definition.png b/app/code/Magento/Analytics/docs/images/definition.png
new file mode 100644
index 0000000000000..16acc576320b0
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/definition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png
new file mode 100644
index 0000000000000..f39d2e4900703
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png differ
diff --git a/app/code/Magento/Analytics/docs/images/signup.png b/app/code/Magento/Analytics/docs/images/signup.png
new file mode 100644
index 0000000000000..561e18b3a351f
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update.png b/app/code/Magento/Analytics/docs/images/update.png
new file mode 100644
index 0000000000000..149f5b5d3f9bd
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update_request.png b/app/code/Magento/Analytics/docs/images/update_request.png
new file mode 100644
index 0000000000000..7181251e3634e
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update_request.png differ
diff --git a/app/code/Magento/Analytics/etc/acl.xml b/app/code/Magento/Analytics/etc/acl.xml
new file mode 100644
index 0000000000000..bf2251895f929
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/acl.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/di.xml b/app/code/Magento/Analytics/etc/adminhtml/di.xml
new file mode 100644
index 0000000000000..5e305e70e5ad3
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/di.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ - Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/menu.xml b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
new file mode 100644
index 0000000000000..915211c4bb85e
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/routes.xml b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
new file mode 100644
index 0000000000000..0ae2762dacc5f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..889517e629e04
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Advanced Reporting
+ general
+ Magento_Analytics::analytics_settings
+
+ Advanced Reporting
+ For more information, see our
+ terms and conditions .]]>
+
+ Advanced Reporting Service
+ Magento\Config\Model\Config\Source\Enabledisable
+ Magento\Analytics\Model\Config\Backend\Enabled
+ Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel
+ analytics/subscription/enabled
+
+
+ Time of day to send data
+ Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel
+ Magento\Analytics\Model\Config\Backend\CollectionTime
+
+
+ Industry Data
+ Industry
+ In order to personalize your Advanced Reporting experience, please select your industry.
+ Magento\Analytics\Model\Config\Source\Vertical
+ Magento\Analytics\Model\Config\Backend\Vertical
+ Magento\Analytics\Block\Adminhtml\System\Config\Vertical
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xml b/app/code/Magento/Analytics/etc/analytics.xml
new file mode 100644
index 0000000000000..77ebe751a31cf
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stores
+
+
+
+
+
+
+
+
+ websites
+
+
+
+
+
+
+
+
+ groups
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xsd b/app/code/Magento/Analytics/etc/analytics.xsd
new file mode 100644
index 0000000000000..2506e3d6a6a9a
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xsd
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ File name attribute can has only [a-zA-Z0-9/_].
+
+
+
+
+
+
+
+
+
+ Value is required.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml
new file mode 100644
index 0000000000000..b6194ba12993f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/config.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ https://advancedreporting.rjmetrics.com/signup
+ https://advancedreporting.rjmetrics.com/update
+ https://dashboard.rjmetrics.com/v2/magento/signup
+ https://advancedreporting.rjmetrics.com/otp
+ https://advancedreporting.rjmetrics.com/report
+ https://advancedreporting.rjmetrics.com/report
+
+ Magento Analytics user
+
+ 02,00,00
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/crontab.xml b/app/code/Magento/Analytics/etc/crontab.xml
new file mode 100644
index 0000000000000..a4beef0359540
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/crontab.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml
new file mode 100644
index 0000000000000..b9bb9cc9ff00c
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/di.xml
@@ -0,0 +1,275 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\SignUpCommand
+ - Magento\Analytics\Model\Connector\UpdateCommand
+ - Magento\Analytics\Model\Connector\NotifyDataChangedCommand
+
+
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Data
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Reader
+ Magento_Analytics_ReportXml_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/reports.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\ReportXml\Config\SchemaLocator
+ reports.xml
+
+ - name
+ -
+
- name
+ - alias
+
+ - name
+ - name
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\Config\Reader\Xml
+
+
+
+
+
+
+ Magento\Analytics\Model\Config\Data
+
+
+
+
+ Magento\Analytics\Model\Config\Reader
+ Magento_Analytics_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/analytics.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\Model\Config\SchemaLocator
+ analytics.xml
+
+ - name
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\DB\Assembler\FromAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler
+
+
+
+
+
+
+ - Magento\Analytics\Model\Config\Reader\Xml
+
+
+
+
+
+
+ - web/unsecure/base_url
+ - currency/options/base
+ - general/locale/timezone
+ - general/country/default
+ - carriers/dhl/title
+ - carriers/dhl/active
+ - carriers/fedex/title
+ - carriers/fedex/active
+ - carriers/flatrate/title
+ - carriers/flatrate/active
+ - carriers/tablerate/title
+ - carriers/tablerate/active
+ - carriers/freeshipping/title
+ - carriers/freeshipping/active
+ - carriers/ups/title
+ - carriers/ups/active
+ - carriers/usps/title
+ - carriers/usps/active
+ - payment/free/title
+ - payment/free/active
+ - payment/checkmo/title
+ - payment/checkmo/active
+ - payment/purchaseorder/title
+ - payment/purchaseorder/active
+ - payment/banktransfer/title
+ - payment/banktransfer/active
+ - payment/cashondelivery/title
+ - payment/cashondelivery/active
+ - payment/authorizenet_directpost/title
+ - payment/authorizenet_directpost/active
+ - payment/paypal_billing_agreement/title
+ - payment/paypal_billing_agreement/active
+ - payment/braintree/title
+ - payment/braintree/active
+ - payment/braintree_paypal/title
+ - payment/braintree_paypal/active
+ - analytics/general/vertical
+
+
+
+
+
+
+ - Apps and Games
+ - Athletic/Sporting Goods
+ - Art and Design
+ - Auto Parts
+ - Baby/Children’s Apparel, Gear and Toys
+ - Beauty and Cosmetics
+ - Books, Music and Magazines
+ - Crafts and Stationery
+ - Consumer Electronics
+ - Deal Site
+ - Fashion Apparel and Accessories
+ - Food, Beverage and Grocery
+ - Home Goods and Furniture
+ - Home Improvement
+ - Jewelry and Watches
+ - Mass Merchant
+ - Office Supplies
+ - Outdoor and Camping Gear
+ - Pet Goods
+ - Pharma and Medical Devices
+ - Technology B2B
+ - Other
+
+
+
+
+
+
+
+
+
+ - \Magento\Analytics\Model\Connector\ResponseHandler\SignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\Update
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\OTP
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+ SignUpResponseResolver
+
+
+
+
+ UpdateResponseResolver
+
+
+
+
+ OtpResponseResolver
+
+
+
+
+ NotifyDataChangedResponseResolver
+
+
+
+
+
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+
+
+ - 1
+ - 1
+ - 1
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/module.xml b/app/code/Magento/Analytics/etc/module.xml
new file mode 100644
index 0000000000000..32ee5d23a4d86
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/module.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/reports.xml b/app/code/Magento/Analytics/etc/reports.xml
new file mode 100644
index 0000000000000..8a43658670293
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Analytics/etc/reports.xsd b/app/code/Magento/Analytics/etc/reports.xsd
new file mode 100644
index 0000000000000..d0ba4068244fe
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xsd
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/webapi.xml b/app/code/Magento/Analytics/etc/webapi.xml
new file mode 100644
index 0000000000000..8252d039f1d03
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/webapi.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/i18n/en_US.csv b/app/code/Magento/Analytics/i18n/en_US.csv
new file mode 100644
index 0000000000000..516c388feb823
--- /dev/null
+++ b/app/code/Magento/Analytics/i18n/en_US.csv
@@ -0,0 +1,84 @@
+"Subscription status","Subscription status"
+"Sorry, there has been an error processing your request. Please try again later.","Sorry, there has been an error processing your request. Please try again later."
+"Sorry, there was an error processing your registration request to Magento Analytics. Please try again later.","Sorry, there was an error processing your registration request to Magento Analytics. Please try again later."
+"Error occurred during postponement notification","Error occurred during postponement notification"
+"Time value has an unsupported format","Time value has an unsupported format"
+"Cron settings can't be saved","Cron settings can't be saved"
+"There was an error save new configuration value.","There was an error save new configuration value."
+"Please select a vertical.","Please select a vertical."
+"--Please Select--","--Please Select--"
+"Command was not found.","Command was not found."
+"Input data must be string or convertible into string.","Input data must be string or convertible into string."
+"Input data must be non-empty string.","Input data must be non-empty string."
+"Not valid cipher method.","Not valid cipher method."
+"Encryption key can't be empty.","Encryption key can't be empty."
+"Source ""%1"" is not exist","Source ""%1"" is not exist"
+"These arguments can't be empty ""%1""","These arguments can't be empty ""%1"""
+"Cannot find predefined integration user!","Cannot find predefined integration user!"
+"File is not ready yet.","File is not ready yet."
+"Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later.","Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later."
+"Failed to synchronize data to the Magento Business Intelligence service. ","Failed to synchronize data to the Magento Business Intelligence service. "
+"Retry Synchronization ","Retry Synchronization "
+TestMessage,TestMessage
+"Error message","Error message"
+"Apps and Games","Apps and Games"
+"Athletic/Sporting Goods","Athletic/Sporting Goods"
+"Art and Design","Art and Design"
+"Advanced Reporting","Advanced Reporting"
+"Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data.","Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data."
+"View details","View details"
+"Go to Advanced Reporting","Go to Advanced Reporting"
+"An error occurred while subscription process.","An error occurred while subscription process."
+Analytics,Analytics
+API,API
+Configuration,Configuration
+"Business Intelligence","Business Intelligence"
+"BI Essentials","BI Essentials"
+"This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions .
+ ","This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions ."
+"Advanced Reporting Service","Advanced Reporting Service"
+Industry,Industry
+"Time of day to send data","Time of day to send data"
+"Get more insights from Magento Business Intelligence ","Get more insights from Magento Business Intelligence "
+"Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier.","Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier."
+"Auto Parts","Auto Parts"
+"Baby/Children’s Apparel, Gear and Toys","Baby/Children’s Apparel, Gear and Toys"
+"Beauty and Cosmetics","Beauty and Cosmetics"
+"Books, Music and Magazines","Books, Music and Magazines"
+"Crafts and Stationery","Crafts and Stationery"
+"Consumer Electronics","Consumer Electronics"
+"Deal Site","Deal Site"
+"Fashion Apparel and Accessories","Fashion Apparel and Accessories"
+"Food, Beverage and Grocery","Food, Beverage and Grocery"
+"Home Goods and Furniture","Home Goods and Furniture"
+"Home Improvement","Home Improvement"
+"Jewelry and Watches","Jewelry and Watches"
+"Mass Merchant","Mass Merchant"
+"Office Supplies","Office Supplies"
+"Outdoor and Camping Gear","Outdoor and Camping Gear"
+"Pet Goods","Pet Goods"
+"Pharma and Medical Devices","Pharma and Medical Devices"
+"Technology B2B","Technology B2B"
+"Analytics Subscription","Analytics Subscription"
+"powered by Magento Business Intelligence","powered by Magento Business Intelligence"
+"Are you sure you want to opt out?","Are you sure you want to opt out?"
+Cancel,Cancel
+"Opt out","Opt out"
+"Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
","Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
"
+"In order to personalize your Advanced Reporting experience, please select your industry.","In order to personalize your Advanced Reporting experience, please select your industry."
diff --git a/app/code/Magento/Analytics/registration.php b/app/code/Magento/Analytics/registration.php
new file mode 100644
index 0000000000000..58d3688b7491d
--- /dev/null
+++ b/app/code/Magento/Analytics/registration.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
new file mode 100644
index 0000000000000..a22c603b2a8b3
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ = $block->escapeHtml(__('Advanced Reporting')) ?>
+
+
+ = $block->escapeHtml(__('Gain new insights and take command of your business\' performance,' .
+ ' using our dynamic product, order, and customer reports tailored to your customer data.')) ?>
+
+
+
+
diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json
index a894193888100..c0b0f9c6b13df 100644
--- a/app/code/Magento/Authorization/composer.json
+++ b/app/code/Magento/Authorization/composer.json
@@ -4,10 +4,10 @@
"require": {
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
"magento/module-backend": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.0",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json
index 2abfebc3c1cb7..1022bd47a5786 100644
--- a/app/code/Magento/Authorizenet/composer.json
+++ b/app/code/Magento/Authorizenet/composer.json
@@ -3,20 +3,20 @@
"description": "N/A",
"require": {
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
- "magento/module-sales": "100.2.*",
+ "magento/module-sales": "101.0.*",
"magento/module-store": "100.2.*",
- "magento/module-quote": "100.2.*",
+ "magento/module-quote": "101.0.*",
"magento/module-checkout": "100.2.*",
"magento/module-backend": "100.2.*",
"magento/module-payment": "100.2.*",
- "magento/module-catalog": "101.1.*",
- "magento/framework": "100.2.*"
+ "magento/module-catalog": "102.0.*",
+ "magento/framework": "101.0.*"
},
"suggest": {
- "magento/module-config": "100.2.*"
+ "magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.0",
"license": [
"proprietary"
],
diff --git a/app/code/Magento/Backend/Block/Cache.php b/app/code/Magento/Backend/Block/Cache.php
index e14358396aa70..82c36bf3a1fe4 100644
--- a/app/code/Magento/Backend/Block/Cache.php
+++ b/app/code/Magento/Backend/Block/Cache.php
@@ -22,24 +22,29 @@ protected function _construct()
$this->_headerText = __('Cache Storage Management');
parent::_construct();
$this->buttonList->remove('add');
- $this->buttonList->add(
- 'flush_magento',
- [
- 'label' => __('Flush Magento Cache'),
- 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')',
- 'class' => 'primary flush-cache-magento'
- ]
- );
- $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?');
- $this->buttonList->add(
- 'flush_system',
- [
- 'label' => __('Flush Cache Storage'),
- 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')',
- 'class' => 'flush-cache-storage'
- ]
- );
+ if ($this->_authorization->isAllowed('Magento_Backend::flush_magento_cache')) {
+ $this->buttonList->add(
+ 'flush_magento',
+ [
+ 'label' => __('Flush Magento Cache'),
+ 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')',
+ 'class' => 'primary flush-cache-magento'
+ ]
+ );
+ }
+
+ if ($this->_authorization->isAllowed('Magento_Backend::flush_cache_storage')) {
+ $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?');
+ $this->buttonList->add(
+ 'flush_system',
+ [
+ 'label' => __('Flush Cache Storage'),
+ 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')',
+ 'class' => 'flush-cache-storage'
+ ]
+ );
+ }
}
/**
diff --git a/app/code/Magento/Backend/Block/Cache/Permissions.php b/app/code/Magento/Backend/Block/Cache/Permissions.php
new file mode 100644
index 0000000000000..272a603145f09
--- /dev/null
+++ b/app/code/Magento/Backend/Block/Cache/Permissions.php
@@ -0,0 +1,62 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushCatalogImages()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_catalog_images');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushJsCss()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_js_css');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushStaticFiles()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_static_files');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToAdditionalActions()
+ {
+ return ($this->hasAccessToFlushCatalogImages()
+ || $this->hasAccessToFlushJsCss()
+ || $this->hasAccessToFlushStaticFiles());
+ }
+}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php
index cecd7b8050352..301dffbdc4987 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Graph.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php
@@ -421,6 +421,8 @@ public function getChartUrl($directUrl = true)
$tmpstring = implode('|', $this->_axisLabels[$idx]);
$valueBuffer[] = $indexid . ":|" . $tmpstring;
+ } elseif ($idx == 'y') {
+ $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels);
}
$indexid++;
}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
index 9d9409fba093b..50279786c0a5b 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
@@ -92,7 +92,7 @@ protected function _prepareCollection()
protected function _afterLoadCollection()
{
foreach ($this->getCollection() as $item) {
- $item->getCustomer() ?: $item->setCustomer('Guest');
+ $item->getCustomer() ?: $item->setCustomer($item->getBillingAddress()->getName());
}
return $this;
}
diff --git a/app/code/Magento/Backend/Block/GlobalSearch.php b/app/code/Magento/Backend/Block/GlobalSearch.php
index f4a46283808f4..9af4e9faef761 100644
--- a/app/code/Magento/Backend/Block/GlobalSearch.php
+++ b/app/code/Magento/Backend/Block/GlobalSearch.php
@@ -3,19 +3,61 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Backend\Block;
+use Magento\Backend\Model\GlobalSearch\SearchEntityFactory;
+use Magento\Backend\Model\GlobalSearch\SearchEntity;
+use Magento\Framework\App\ObjectManager;
+
/**
* @api
* @since 100.0.2
*/
class GlobalSearch extends \Magento\Backend\Block\Template
{
+ /**
+ * @var SearchEntityFactory
+ */
+ private $searchEntityFactory;
+
/**
* @var string
*/
protected $_template = 'Magento_Backend::system/search.phtml';
+ /**
+ * @var array
+ */
+ private $entityResources;
+
+ /**
+ * @var array
+ */
+ private $entityPaths;
+
+ /**
+ * @param Template\Context $context
+ * @param array $data
+ * @param array $entityResources
+ * @param array $entityPaths
+ * @param SearchEntityFactory|null $searchEntityFactory
+ */
+ public function __construct(
+ Template\Context $context,
+ array $data = [],
+ array $entityResources = [],
+ array $entityPaths = [],
+ SearchEntityFactory $searchEntityFactory = null
+ ) {
+ $this->entityResources = $entityResources;
+ $this->entityPaths = $entityPaths;
+ $this->searchEntityFactory = $searchEntityFactory ?: ObjectManager::getInstance()
+ ->get(SearchEntityFactory::class);
+
+ parent::__construct($context, $data);
+ }
+
/**
* Get components configuration
* @return array
@@ -34,4 +76,48 @@ public function getWidgetInitOptions()
]
];
}
+
+ /**
+ * Get entities which are allowed to show.
+ *
+ * @return SearchEntity[]
+ */
+ public function getEntitiesToShow()
+ {
+ $allowedEntityTypes = [];
+ $entitiesToShow = [];
+
+ foreach ($this->entityResources as $entityType => $resource) {
+ if ($this->getAuthorization()->isAllowed($resource)) {
+ $allowedEntityTypes[] = $entityType;
+ }
+ }
+
+ foreach ($allowedEntityTypes as $entityType) {
+ $url = $this->getUrlEntityType($entityType);
+
+ $searchEntity = $this->searchEntityFactory->create();
+ $searchEntity->setId('searchPreview' . $entityType);
+ $searchEntity->setTitle('in ' . $entityType);
+ $searchEntity->setUrl($url);
+
+ $entitiesToShow[] = $searchEntity;
+ }
+
+ return $entitiesToShow;
+ }
+
+ /**
+ * Get url path by entity type.
+ *
+ * @param string $entityType
+ *
+ * @return string
+ */
+ private function getUrlEntityType(string $entityType)
+ {
+ $urlPath = $this->entityPaths[$entityType] ?? '';
+
+ return $this->getUrl($urlPath);
+ }
}
diff --git a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
index f19799d2e4939..034887c67d1ee 100644
--- a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
+++ b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
@@ -37,7 +37,7 @@ protected function _prepareForm()
['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']]
);
- $this->_prepareStoreFieldSet($form);
+ $this->_prepareStoreFieldset($form);
$form->addField(
'store_type',
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
index 40a5d92c56b6f..632603d389d21 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
@@ -127,7 +127,7 @@ public function getHtml()
/**
* @param string|null $index
- * @return string
+ * @return array|string|int|float|null
*/
public function getEscapedValue($index = null)
{
@@ -138,6 +138,11 @@ public function getEscapedValue($index = null)
$this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT)
);
}
+
+ if (is_string($value)) {
+ return $this->escapeHtml($value);
+ }
+
return $value;
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
index 96b3471db845e..a5e4a34389671 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
@@ -140,8 +140,8 @@ public function getHtml()
/**
* Return escaped value for calendar
*
- * @param string $index
- * @return string
+ * @param string|null $index
+ * @return array|string|int|float|null
*/
public function getEscapedValue($index = null)
{
@@ -150,6 +150,11 @@ public function getEscapedValue($index = null)
if ($value instanceof \DateTimeInterface) {
return $this->_localeDate->formatDateTime($value);
}
+
+ if (is_string($value)) {
+ return $this->escapeHtml($value);
+ }
+
return $value;
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
index d9b00d2ba2503..662cbedaed8db 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
@@ -3,8 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Backend\Block\Widget\Grid;
+use Magento\Backend\Block\Template\Context;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\AuthorizationInterface;
+use Magento\Framework\DataObject;
+use Magento\Framework\Json\EncoderInterface;
+
/**
* Grid widget massaction default block
*
@@ -14,4 +21,72 @@
*/
class Massaction extends \Magento\Backend\Block\Widget\Grid\Massaction\AbstractMassaction
{
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
+ /**
+ * Map bind item id to a particular acl type
+ * itemId => acl
+ *
+ * @var array
+ */
+ private $restrictions = [
+ 'enable' => 'Magento_Backend::toggling_cache_type',
+ 'disable' => 'Magento_Backend::toggling_cache_type',
+ 'refresh' => 'Magento_Backend::refresh_cache_type',
+ ];
+
+ /**
+ * Massaction constructor.
+ *
+ * @param Context $context
+ * @param EncoderInterface $jsonEncoder
+ * @param array $data
+ * @param AuthorizationInterface $authorization
+ */
+ public function __construct(
+ Context $context,
+ EncoderInterface $jsonEncoder,
+ array $data = [],
+ AuthorizationInterface $authorization = null
+ ) {
+ $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class);
+
+ parent::__construct($context, $jsonEncoder, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $itemId
+ * @param array|DataObject $item
+ *
+ * @return $this
+ */
+ public function addItem($itemId, $item)
+ {
+ if (!$this->isRestricted($itemId)) {
+ parent::addItem($itemId, $item);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Check if access to action restricted
+ *
+ * @param string $itemId
+ *
+ * @return bool
+ */
+ private function isRestricted(string $itemId): bool
+ {
+ if (!key_exists($itemId, $this->restrictions)) {
+ return false;
+ }
+
+ return !$this->authorization->isAllowed($this->restrictions[$itemId]);
+ }
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
index 1895bd08c464f..7a926b1c09c3e 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
@@ -11,6 +11,13 @@
class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_catalog_images';
+
/**
* Clean JS/css files cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
index 521cb9806a135..72f23ab65cf8a 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
@@ -11,6 +11,13 @@
class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_js_css';
+
/**
* Clean JS/css files cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
index adfbf7cfe63c8..27ae2fc31e150 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
@@ -10,6 +10,13 @@
class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_static_files';
+
/**
* Clean static files cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
index 2f9dc9ad6a7a5..ca89ea58fa6f3 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
@@ -8,6 +8,13 @@
class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_cache_storage';
+
/**
* Flush cache storage
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
index 0f55a59353d65..f0fed159e0f22 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
@@ -8,6 +8,13 @@
class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_magento_cache';
+
/**
* Flush all magento cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
index 204105852b9f1..2bfa937b06b77 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
@@ -16,6 +16,13 @@
*/
class MassDisable extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type';
+
/**
* @var State
*/
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
index 32acf47887c44..113e0f2d8961b 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
@@ -16,6 +16,13 @@
*/
class MassEnable extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type';
+
/**
* @var State
*/
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
index e18aa1555e11b..3843b030afb3d 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
@@ -11,6 +11,13 @@
class MassRefresh extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::refresh_cache_type';
+
/**
* Mass action for cache refresh
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php
index f03d58b9a3eb7..87153c381c1f7 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php
@@ -8,6 +8,13 @@
class Index extends \Magento\Backend\App\Action
{
+ /**
+ * Array of actions which can be processed without secret key validation
+ *
+ * @var string[]
+ */
+ protected $_publicActions = ['index'];
+
/**
* @var \Magento\Framework\View\Result\PageFactory
*/
@@ -34,7 +41,7 @@ public function execute()
{
/** @var \Magento\Backend\Model\View\Result\Page $resultPage */
$resultPage = $this->resultPageFactory->create();
- $resultPage->setStatusHeader(404, '1.1', 'Forbidden');
+ $resultPage->setStatusHeader(404, '1.1', 'Not Found');
$resultPage->setHeader('Status', '404 File not found');
$resultPage->addHandle('adminhtml_noroute');
return $resultPage;
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
index c9bce1cbf3888..421885a0c32a3 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
@@ -54,9 +54,9 @@ public function execute()
$user = $this->_objectManager->create(\Magento\User\Model\User::class)->load($userId);
$user->setId($userId)
- ->setUsername($this->getRequest()->getParam('username', false))
- ->setFirstname($this->getRequest()->getParam('firstname', false))
- ->setLastname($this->getRequest()->getParam('lastname', false))
+ ->setUserName($this->getRequest()->getParam('username', false))
+ ->setFirstName($this->getRequest()->getParam('firstname', false))
+ ->setLastName($this->getRequest()->getParam('lastname', false))
->setEmail(strtolower($this->getRequest()->getParam('email', false)));
if ($this->_objectManager->get(\Magento\Framework\Validator\Locale::class)->isValid($interfaceLocale)) {
diff --git a/app/code/Magento/Backend/Model/Auth/StorageInterface.php b/app/code/Magento/Backend/Model/Auth/StorageInterface.php
index 52b2b089c71e1..e643165a93317 100644
--- a/app/code/Magento/Backend/Model/Auth/StorageInterface.php
+++ b/app/code/Magento/Backend/Model/Auth/StorageInterface.php
@@ -23,7 +23,7 @@ interface StorageInterface
public function processLogin();
/**
- * Perform login specific actions
+ * Perform logout specific actions
*
* @return $this
* @abstract
diff --git a/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php b/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php
new file mode 100644
index 0000000000000..18691802b9218
--- /dev/null
+++ b/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php
@@ -0,0 +1,73 @@
+getData('id');
+ }
+
+ /**
+ * Get url.
+ *
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->getData('url');
+ }
+
+ /**
+ * Get title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->getData('title');
+ }
+
+ /**
+ * Set Id.
+ *
+ * @param string $value
+ */
+ public function setId(string $value)
+ {
+ $this->setData('id', $value);
+ }
+
+ /**
+ * Set url.
+ *
+ * @param string $value
+ */
+ public function setUrl(string $value)
+ {
+ $this->setData('url', $value);
+ }
+
+ /**
+ * Set title.
+ *
+ * @param string $value
+ */
+ public function setTitle(string $value)
+ {
+ $this->setData('title', $value);
+ }
+}
diff --git a/app/code/Magento/Backend/Model/Menu/Item.php b/app/code/Magento/Backend/Model/Menu/Item.php
index fe6564d24e891..42febe94d0abf 100644
--- a/app/code/Magento/Backend/Model/Menu/Item.php
+++ b/app/code/Magento/Backend/Model/Menu/Item.php
@@ -467,15 +467,15 @@ public function toArray()
{
return [
'parent_id' => $this->_parentId,
- 'module_name' => $this->_moduleName,
+ 'module' => $this->_moduleName,
'sort_index' => $this->_sortIndex,
- 'depends_on_config' => $this->_dependsOnConfig,
+ 'dependsOnConfig' => $this->_dependsOnConfig,
'id' => $this->_id,
'resource' => $this->_resource,
'path' => $this->_path,
'action' => $this->_action,
- 'depends_on_module' => $this->_dependsOnModule,
- 'tooltip' => $this->_tooltip,
+ 'dependsOnModule' => $this->_dependsOnModule,
+ 'toolTip' => $this->_tooltip,
'title' => $this->_title,
'target' => $this->target,
'sub_menu' => isset($this->_submenu) ? $this->_submenu->toArray() : null
@@ -492,15 +492,15 @@ public function toArray()
public function populateFromArray(array $data)
{
$this->_parentId = $this->_getArgument($data, 'parent_id');
- $this->_moduleName = $this->_getArgument($data, 'module_name', 'Magento_Backend');
+ $this->_moduleName = $this->_getArgument($data, 'module', 'Magento_Backend');
$this->_sortIndex = $this->_getArgument($data, 'sort_index');
- $this->_dependsOnConfig = $this->_getArgument($data, 'depends_on_config');
+ $this->_dependsOnConfig = $this->_getArgument($data, 'dependsOnConfig');
$this->_id = $this->_getArgument($data, 'id');
$this->_resource = $this->_getArgument($data, 'resource');
$this->_path = $this->_getArgument($data, 'path', '');
$this->_action = $this->_getArgument($data, 'action');
- $this->_dependsOnModule = $this->_getArgument($data, 'depends_on_module');
- $this->_tooltip = $this->_getArgument($data, 'tooltip', '');
+ $this->_dependsOnModule = $this->_getArgument($data, 'dependsOnModule');
+ $this->_tooltip = $this->_getArgument($data, 'toolTip');
$this->_title = $this->_getArgument($data, 'title');
$this->target = $this->_getArgument($data, 'target');
if (isset($data['sub_menu'])) {
diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php
index 48b443fe7ffd3..cf895268bcb7d 100644
--- a/app/code/Magento/Backend/Model/Url.php
+++ b/app/code/Magento/Backend/Model/Url.php
@@ -202,7 +202,7 @@ public function getUrl($routePath = null, $routeParams = null)
}
$cacheSecretKey = false;
- if (is_array($routeParams) && isset($routeParams['_cache_secret_key'])) {
+ if (isset($routeParams['_cache_secret_key'])) {
unset($routeParams['_cache_secret_key']);
$cacheSecretKey = true;
}
@@ -210,25 +210,22 @@ public function getUrl($routePath = null, $routeParams = null)
if (!$this->useSecretKey()) {
return $result;
}
+
$this->_setRoutePath($routePath);
$routeName = $this->_getRouteName('*');
$controllerName = $this->_getControllerName(self::DEFAULT_CONTROLLER_NAME);
$actionName = $this->_getActionName(self::DEFAULT_ACTION_NAME);
- if ($cacheSecretKey) {
- $secret = [self::SECRET_KEY_PARAM_NAME => "\${$routeName}/{$controllerName}/{$actionName}\$"];
- } else {
- $secret = [
- self::SECRET_KEY_PARAM_NAME => $this->getSecretKey($routeName, $controllerName, $actionName),
- ];
- }
- if (is_array($routeParams)) {
- $routeParams = array_merge($secret, $routeParams);
- } else {
- $routeParams = $secret;
- }
- if (is_array($this->_getRouteParams())) {
- $routeParams = array_merge($this->_getRouteParams(), $routeParams);
+
+ if (!isset($routeParams[self::SECRET_KEY_PARAM_NAME])) {
+ if (!is_array($routeParams)) {
+ $routeParams = [];
+ }
+ $secretKey = $cacheSecretKey
+ ? "\${$routeName}/{$controllerName}/{$actionName}\$"
+ : $this->getSecretKey($routeName, $controllerName, $actionName);
+ $routeParams[self::SECRET_KEY_PARAM_NAME] = $secretKey;
}
+
return parent::getUrl("{$routeName}/{$controllerName}/{$actionName}", $routeParams);
}
diff --git a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
index e2b77fb471acd..fbed7149aa565 100644
--- a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
+++ b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
@@ -30,8 +30,9 @@ public function parseExpression($expression)
$expression = trim($expression);
foreach ($this->_operations as $operation) {
$splittedExpr = preg_split('/\\' . $operation . '/', $expression, -1, PREG_SPLIT_DELIM_CAPTURE);
- if (count($splittedExpr) > 1) {
- for ($i = 0; $i < count($splittedExpr); $i++) {
+ $count = count($splittedExpr);
+ if ($count > 1) {
+ for ($i = 0; $i < $count; $i++) {
$stack = array_merge($stack, $this->parseExpression($splittedExpr[$i]));
if ($i > 0) {
$stack[] = $operation;
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php
new file mode 100644
index 0000000000000..6975b8ac092ad
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php
@@ -0,0 +1,76 @@
+objectManager = new ObjectManager($this);
+
+ $this->mockAuthorization = $this->getMockBuilder(Authorization::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAllowed'])
+ ->getMock();
+
+ $this->permissions = new Permissions($this->mockAuthorization);
+ }
+
+ public function testHasAccessToFlushCatalogImages()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_catalog_images')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushCatalogImages());
+ }
+
+ public function testHasAccessToFlushJsCss()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_js_css')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushJsCss());
+ }
+
+ public function testHasAccessToFlushStaticFiles()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_static_files')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushStaticFiles());
+ }
+}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php b/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php
new file mode 100644
index 0000000000000..0010ffad87524
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php
@@ -0,0 +1,123 @@
+ \Magento\Catalog\Controller\Adminhtml\Product::ADMIN_RESOURCE,
+ 'Orders' => \Magento\Sales\Controller\Adminhtml\Order::ADMIN_RESOURCE,
+ 'Customers' => \Magento\Customer\Controller\Adminhtml\Index::ADMIN_RESOURCE,
+ 'Pages' => \Magento\Cms\Controller\Adminhtml\Page\Index::ADMIN_RESOURCE,
+ ];
+
+ /**
+ * @var array
+ */
+ private $entityPaths = [
+ 'Products' => 'catalog/product/index/',
+ 'Orders' => 'sales/order/index/',
+ 'Customers' => 'customer/index/index',
+ 'Pages' => 'cms/page/index/',
+ ];
+
+ protected function setUp()
+ {
+ $objectManager = new ObjectManager($this);
+
+ $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class);
+ $this->urlBuilder = $this->createMock(\Magento\Framework\UrlInterface::class);
+ $context = $this->createMock(\Magento\Backend\Block\Template\Context::class);
+
+ $context->expects($this->atLeastOnce())->method('getAuthorization')->willReturn($this->authorization);
+ $context->expects($this->atLeastOnce())->method('getUrlBuilder')->willReturn($this->urlBuilder);
+
+ $this->searchEntityFactory = $this->createMock(\Magento\Backend\Model\GlobalSearch\SearchEntityFactory::class);
+
+ $this->globalSearch = $objectManager->getObject(
+ GlobalSearch::class,
+ [
+ 'context' => $context,
+ 'searchEntityFactory' => $this->searchEntityFactory,
+ 'entityResources' => $this->entityResources,
+ 'entityPaths' => $this->entityPaths,
+ ]
+ );
+ }
+
+ /**
+ * @param array $results
+ * @param int $expectedEntitiesQty
+ *
+ * @dataProvider getEntitiesToShowDataProvider
+ */
+ public function testGetEntitiesToShow(array $results, int $expectedEntitiesQty)
+ {
+ $searchEntity = $this->createMock(SearchEntity::class);
+
+ $this->authorization->expects($this->exactly(count($results)))->method('isAllowed')
+ ->willReturnOnConsecutiveCalls($results[0], $results[1], $results[2], $results[3]);
+ $this->urlBuilder->expects($this->exactly($expectedEntitiesQty))
+ ->method('getUrl')->willReturn('some/url/is/here');
+ $this->searchEntityFactory->expects($this->exactly($expectedEntitiesQty))
+ ->method('create')->willReturn($searchEntity);
+
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setId');
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setTitle');
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setUrl');
+
+ $this->assertSame($expectedEntitiesQty, count($this->globalSearch->getEntitiesToShow()));
+ }
+
+ public function getEntitiesToShowDataProvider()
+ {
+ return [
+ [
+ [true, false, true, false],
+ 2,
+ ],
+ [
+ [true, true, true, true],
+ 4,
+ ],
+ [
+ [false, false, false, false],
+ 0,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
index be171a8ed40bf..df242a4cf6129 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
@@ -117,7 +117,7 @@ public function testSetFilterTypePropagatesFilterTypeToColumns()
public function testGetRowUrlIfUrlPathNotSet()
{
- $this->assertEquals('#', $this->_block->getRowUrl(new \StdClass()));
+ $this->assertEquals('#', $this->_block->getRowUrl(new \stdClass()));
}
public function testGetRowUrl()
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
index da13af87b71ea..c5c56fd75fbe7 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
@@ -351,7 +351,7 @@ public function testSetGetGrid()
$this->_block->setFilter('StdClass');
- $grid = new \StdClass();
+ $grid = new \stdClass();
$this->_block->setGrid($grid);
$this->assertEquals($grid, $this->_block->getGrid());
}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
index bb389a996e1ed..29ce448a04ecb 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
@@ -10,6 +10,7 @@
namespace Magento\Backend\Test\Unit\Block\Widget\Grid;
use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker;
+use Magento\Framework\Authorization;
class MassactionTest extends \PHPUnit\Framework\TestCase
{
@@ -43,6 +44,11 @@ class MassactionTest extends \PHPUnit\Framework\TestCase
*/
protected $_requestMock;
+ /**
+ * @var Authorization|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $_authorizationMock;
+
/**
* @var VisibilityChecker|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -86,11 +92,17 @@ protected function setUp()
$this->visibilityCheckerMock = $this->getMockBuilder(VisibilityChecker::class)
->getMockForAbstractClass();
+ $this->_authorizationMock = $this->getMockBuilder(Authorization::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAllowed'])
+ ->getMock();
+
$arguments = [
'layout' => $this->_layoutMock,
'request' => $this->_requestMock,
'urlBuilder' => $this->_urlModelMock,
- 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id']
+ 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id'],
+ 'authorization' => $this->_authorizationMock,
];
$objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
@@ -145,6 +157,10 @@ public function testItemsProcessing($itemId, $item, $expectedItem)
->method('getUrl')
->willReturnMap($urlReturnValueMap);
+ $this->_authorizationMock->expects($this->any())
+ ->method('isAllowed')
+ ->willReturn(true);
+
$this->_block->addItem($itemId, $item);
$this->assertEquals(1, $this->_block->getCount());
@@ -184,6 +200,28 @@ public function itemsProcessingDataProvider()
"id" => 'test_id2',
]
)
+ ],
+ [
+ 'enabled',
+ new \Magento\Framework\DataObject(["label" => "Test Item Enabled", "url" => "*/*/test2"]),
+ new \Magento\Framework\DataObject(
+ [
+ "label" => "Test Item Enabled",
+ "url" => "http://localhost/index.php/backend/admin/test/test2",
+ "id" => 'enabled',
+ ]
+ )
+ ],
+ [
+ 'refresh',
+ new \Magento\Framework\DataObject(["label" => "Test Item Refresh", "url" => "*/*/test2"]),
+ new \Magento\Framework\DataObject(
+ [
+ "label" => "Test Item Refresh",
+ "url" => "http://localhost/index.php/backend/admin/test/test2",
+ "id" => 'refresh',
+ ]
+ )
]
];
}
@@ -237,7 +275,7 @@ public function testGetGridIdsJsonWithoutUseSelectAll()
public function testGetGridIdsJsonWithUseSelectAll(array $items, $result)
{
$this->_block->setUseSelectAll(true);
-
+
if ($this->_block->getMassactionIdField()) {
$massActionIdField = $this->_block->getMassactionIdField();
} else {
@@ -290,14 +328,20 @@ public function dataProviderGetGridIdsJsonWithUseSelectAll()
* @param int $count
* @param bool $withVisibilityChecker
* @param bool $isVisible
+ * @param bool $isAllowed
+ *
* @dataProvider addItemDataProvider
*/
- public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible)
+ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible, $isAllowed)
{
$this->visibilityCheckerMock->expects($this->any())
->method('isVisible')
->willReturn($isVisible);
+ $this->_authorizationMock->expects($this->any())
+ ->method('isAllowed')
+ ->willReturn($isAllowed);
+
if ($withVisibilityChecker) {
$item['visible'] = $this->visibilityCheckerMock;
}
@@ -311,7 +355,7 @@ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isV
->willReturnMap($urlReturnValueMap);
$this->_block->addItem($itemId, $item);
- $this->assertEquals($count, $this->_block->getCount());
+ $this->assertEquals($count, $this->_block->getCount(), $itemId);
}
/**
@@ -325,7 +369,8 @@ public function addItemDataProvider()
'item' => ['label' => 'Test 1', 'url' => '*/*/test1'],
'count' => 1,
'withVisibilityChecker' => false,
- '$isVisible' => false,
+ 'isVisible' => false,
+ 'isAllowed' => true,
],
[
'itemId' => 'test2',
@@ -333,21 +378,56 @@ public function addItemDataProvider()
'count' => 1,
'withVisibilityChecker' => false,
'isVisible' => true,
+ 'isAllowed' => true,
],
[
- 'itemId' => 'test1',
- 'item' => ['label' => 'Test 1. Hide', 'url' => '*/*/test1'],
+ 'itemId' => 'test3',
+ 'item' => ['label' => 'Test 3. Hide', 'url' => '*/*/test3'],
'count' => 0,
'withVisibilityChecker' => true,
'isVisible' => false,
+ 'isAllowed' => true,
],
[
- 'itemId' => 'test2',
- 'item' => ['label' => 'Test 2. Does not hide', 'url' => '*/*/test2'],
+ 'itemId' => 'test4',
+ 'item' => ['label' => 'Test 4. Does not hide', 'url' => '*/*/test4'],
'count' => 1,
'withVisibilityChecker' => true,
'isVisible' => true,
- ]
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'enable',
+ 'item' => ['label' => 'Test 5. Not restricted', 'url' => '*/*/test5'],
+ 'count' => 1,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'enable',
+ 'item' => ['label' => 'Test 5. restricted', 'url' => '*/*/test5'],
+ 'count' => 0,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => false,
+ ],
+ [
+ 'itemId' => 'refresh',
+ 'item' => ['label' => 'Test 6. Not Restricted', 'url' => '*/*/test6'],
+ 'count' => 1,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'refresh',
+ 'item' => ['label' => 'Test 6. Restricted', 'url' => '*/*/test6'],
+ 'count' => 0,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => false,
+ ],
];
}
}
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
index 31a13191750a3..2f0102ffd410d 100755
--- a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
@@ -20,7 +20,8 @@ public function testBeforeSave($value, $errorMessage = null)
\Magento\Backend\Model\Config\SessionLifetime\BackendModel::class
);
if ($errorMessage !== null) {
- $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $errorMessage);
+ $this->expectException(\Magento\Framework\Exception\LocalizedException::class);
+ $this->expectExceptionMessage($errorMessage);
}
$model->setValue($value);
$object = $model->beforeSave();
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
index 74368537c39c7..ad172cbfbd165 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
@@ -56,9 +56,9 @@ class ItemTest extends \PHPUnit\Framework\TestCase
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
];
protected function setUp()
diff --git a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
index b0c74461980a2..82f07e264b963 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
@@ -12,21 +12,21 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
[
'parent_id' => null,
- 'module_name' => 'Magento_Backend',
+ 'module' => 'Magento_Backend',
'sort_index' => null,
- 'depends_on_config' => 'system/config/isEnabled',
+ 'dependsOnConfig' => 'system/config/isEnabled',
'id' => 'item',
'resource' => 'Magento_Config::config',
'path' => '',
'action' => '/system/config',
- 'depends_on_module' => 'Magento_Backend',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'toolTip' => 'Item tooltip',
'title' => 'Item Title',
'sub_menu' => null,
'target' => null
@@ -38,43 +38,43 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => ['submenuArray'],
'target' => null
@@ -83,51 +83,51 @@
'data with submenu to constructor' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => null,
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => ['submenuArray'],
'target' => null
diff --git a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
index 30a43b0158ae3..b1a310d7d440b 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
@@ -11,22 +11,22 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
'sub_menu' => null,
],
[
'parent_id' => null,
- 'module_name' => 'Magento_Backend',
+ 'module' => 'Magento_Backend',
'sort_index' => null,
- 'depends_on_config' => 'system/config/isEnabled',
+ 'dependsOnConfig' => 'system/config/isEnabled',
'id' => 'item',
'resource' => 'Magento_Config::config',
'path' => '',
'action' => '/system/config',
- 'depends_on_module' => 'Magento_Backend',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'toolTip' => 'Item tooltip',
'title' => 'Item Title',
'sub_menu' => null,
'target' => null
@@ -35,46 +35,46 @@
'with submenu' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
'target' => null
]
@@ -82,38 +82,38 @@
'small set of data' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => null,
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
'target' => null
]
diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json
index d3c94c1e286e0..13a99176b6532 100644
--- a/app/code/Magento/Backend/composer.json
+++ b/app/code/Magento/Backend/composer.json
@@ -6,25 +6,25 @@
"magento/module-store": "100.2.*",
"magento/module-directory": "100.2.*",
"magento/module-developer": "100.2.*",
- "magento/module-eav": "100.2.*",
+ "magento/module-eav": "101.0.*",
"magento/module-reports": "100.2.*",
- "magento/module-sales": "100.2.*",
- "magento/module-quote": "100.2.*",
- "magento/module-catalog": "101.1.*",
- "magento/module-user": "100.2.*",
+ "magento/module-sales": "101.0.*",
+ "magento/module-quote": "101.0.*",
+ "magento/module-catalog": "102.0.*",
+ "magento/module-user": "101.0.*",
"magento/module-security": "100.2.*",
"magento/module-backup": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-translation": "100.2.*",
"magento/module-require-js": "100.2.*",
- "magento/module-config": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/module-config": "101.0.*",
+ "magento/framework": "101.0.*"
},
"suggest": {
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.3",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Backend/etc/acl.xml b/app/code/Magento/Backend/etc/acl.xml
index af4ab5856e94c..cf9471e75bed9 100644
--- a/app/code/Magento/Backend/etc/acl.xml
+++ b/app/code/Magento/Backend/etc/acl.xml
@@ -38,7 +38,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml
index 5154c4eb56c91..de8286e7f3ecc 100644
--- a/app/code/Magento/Backend/etc/adminhtml/di.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/di.xml
@@ -145,6 +145,30 @@
- Magento\Config\Model\Config\Structure\ElementVisibilityInterface::HIDDEN
- Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED
+
+
+
+
+
+
+
+ Magento\Backend\Block\Template
+
+
+
+
+
+ - Magento_Catalog::products
+ - Magento_Sales::sales_order
+ - Magento_Customer::manage
+ - Magento_Cms::page
+
+
+ - catalog/product/index/
+ - sales/order/index/
+ - customer/index/index
+ - cms/page/index/
+
diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml
index 27fd16cc920dc..3a58ef9f63e8d 100644
--- a/app/code/Magento/Backend/etc/adminhtml/system.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/system.xml
@@ -215,7 +215,7 @@
European Union Countries
Magento\Directory\Model\Config\Source\Country
-
+
Top destinations
Magento\Directory\Model\Config\Source\Country
@@ -409,7 +409,7 @@
Web
general
Magento_Config::web
-
+
Url Options
Add Store Code to Urls
@@ -419,7 +419,7 @@
Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.).]]>
-
+
Auto-redirect to Base URL
Magento\Config\Model\Config\Source\Web\Redirect
I.e. redirect from http://example.com/store/ to http://www.example.com/store/
diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv
index 2730d4d92835b..aa28a670b9205 100644
--- a/app/code/Magento/Backend/i18n/en_US.csv
+++ b/app/code/Magento/Backend/i18n/en_US.csv
@@ -214,10 +214,13 @@ YTD,YTD
"Admin session lifetime must be greater than or equal to 60 seconds","Admin session lifetime must be greater than or equal to 60 seconds"
Order,Order
"Order #%1","Order #%1"
-"Access denied","Access denied"
-"Please try to sign out and sign in again.","Please try to sign out and sign in again."
-"If you continue to receive this message, please contact the store owner.","If you continue to receive this message, please contact the store owner."
"You need more permissions to access this.","You need more permissions to access this."
+"Sorry, you need permissions to view this content.","Sorry, you need permissions to view this content."
+"Next steps","Next steps"
+"If you think this is an error, try signing out and signing in again.","If you think this is an error, try signing out and signing in again."
+"Contact a system administrator or store owner to gain permissions.","Contact a system administrator or store owner to gain permissions."
+"Return to","Return to"
+"previous page","previous page"
"Welcome, please sign in","Welcome, please sign in"
Username,Username
"user name","user name"
@@ -458,3 +461,7 @@ Pagination,Pagination
"Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used.","Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used."
"Anchor Text for Next","Anchor Text for Next"
"Theme Name","Theme Name"
+"In Products","In Products"
+"In Orders","In Orders"
+"In Customers","In Customers"
+"In Pages","In Pages"
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml
index f6a93fbd84099..50d210f71025b 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml
@@ -42,7 +42,7 @@
0
-
+
Cache Type
cache_type
@@ -53,7 +53,7 @@
true
-
+
Description
description
@@ -63,7 +63,7 @@
true
-
+
Tags
tags
@@ -73,7 +73,7 @@
0
-
+
Status
status
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
index ab5ddc414b51f..4bbe70b6cdb92 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
@@ -11,7 +11,11 @@
-
+
+
+ Magento\Backend\Block\Cache\Permissions
+
+
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml
index 52d88ce717043..b96614f4bd8db 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml
@@ -28,7 +28,7 @@
-
+
Store
100px
@@ -38,7 +38,7 @@
store_id
-
+
Design
options
@@ -47,7 +47,7 @@
design
-
+
Date From
left
@@ -57,7 +57,7 @@
date_from
-
+
Date To
left
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml
index 8dcb6e07b3c4e..126de5eb4084f 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml
@@ -18,7 +18,7 @@
storeGrid
-
+
Web Site
left
@@ -27,7 +27,7 @@
Magento\Backend\Block\System\Store\Grid\Render\Website
-
+
Store
left
@@ -36,7 +36,7 @@
Magento\Backend\Block\System\Store\Grid\Render\Group
-
+
Store View
left
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
index 852ecd5a07962..843328fbf17d7 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
@@ -12,12 +12,23 @@
* @see \Magento\Backend\Block\Denied
*/
?>
-= /* @escapeNotVerified */ __('Access denied') ?>
-hasAvailableResources()): ?>
-
-= /* @escapeNotVerified */ __('Please try to sign out and sign in again.') ?>
-= /* @escapeNotVerified */ __('If you continue to receive this message, please contact the store owner.') ?>
-
-
-= /* @escapeNotVerified */ __('You need more permissions to access this.') ?>
-
+
+
+
= $block->escapeHtml(__('Sorry, you need permissions to view this content.')) ?>
+
= $block->escapeHtml(__('Next steps')) ?>
+
+
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
index 40b7173f47417..8feccc9cf1b8f 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
@@ -29,7 +29,7 @@
data-mage-init='{"dropdown":{}}'
data-toggle="dropdown">
- = $block->escapeHtml($block->getUser()->getUsername()) ?>
+ = $block->escapeHtml($block->getUser()->getUserName()) ?>
+ href="https://marketplace.magento.com/">
= /* @escapeNotVerified */ __('Visit Magento Marketplaces') ?>
diff --git a/app/code/Magento/Marketplace/view/adminhtml/web/partners/images/magento-connect.png b/app/code/Magento/Marketplace/view/adminhtml/web/partners/images/magento-connect.png
deleted file mode 100644
index 575563f341b35..0000000000000
Binary files a/app/code/Magento/Marketplace/view/adminhtml/web/partners/images/magento-connect.png and /dev/null differ
diff --git a/app/code/Magento/Marketplace/view/adminhtml/web/partners/images/magento-marketplace.svg b/app/code/Magento/Marketplace/view/adminhtml/web/partners/images/magento-marketplace.svg
new file mode 100644
index 0000000000000..388544d5d7f3d
--- /dev/null
+++ b/app/code/Magento/Marketplace/view/adminhtml/web/partners/images/magento-marketplace.svg
@@ -0,0 +1 @@
+Artboard 1
\ No newline at end of file
diff --git a/app/code/Magento/MediaStorage/composer.json b/app/code/Magento/MediaStorage/composer.json
index 80e154ebc8ef1..34b47d65c7f0c 100644
--- a/app/code/Magento/MediaStorage/composer.json
+++ b/app/code/Magento/MediaStorage/composer.json
@@ -5,11 +5,11 @@
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
"magento/module-store": "100.2.*",
"magento/module-backend": "100.2.*",
- "magento/module-config": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/module-config": "101.0.*",
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.0",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json
index 587bbe88b4101..ad03bc767a449 100644
--- a/app/code/Magento/Msrp/composer.json
+++ b/app/code/Magento/Msrp/composer.json
@@ -4,19 +4,19 @@
"require": {
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
"magento/module-store": "100.2.*",
- "magento/module-catalog": "101.1.*",
+ "magento/module-catalog": "102.0.*",
"magento/module-downloadable": "100.2.*",
- "magento/module-eav": "100.2.*",
+ "magento/module-eav": "101.0.*",
"magento/module-grouped-product": "100.2.*",
"magento/module-tax": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/framework": "101.0.*"
},
"suggest": {
"magento/module-bundle": "100.2.*",
"magento/module-msrp-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.0",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php
index c62226dc8d063..2197598489358 100644
--- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php
+++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php
@@ -120,11 +120,7 @@ public function getPaymentHtml()
*/
public function getPayment()
{
- if (!$this->hasData('payment')) {
- $payment = new \Magento\Framework\DataObject($this->getRequest()->getPost('payment'));
- $this->setData('payment', $payment);
- }
- return $this->_getData('payment');
+ return $this->getCheckout()->getQuote()->getPayment();
}
/**
@@ -200,9 +196,9 @@ public function formatPrice($price)
/**
* @param Address $address
- * @return mixed
+ * @return array
*/
- public function getShippingAddressItems($address)
+ public function getShippingAddressItems($address): array
{
return $address->getAllVisibleItems();
}
@@ -309,16 +305,7 @@ public function getVirtualProductEditUrl()
*/
public function getVirtualItems()
{
- $items = [];
- foreach ($this->getBillingAddress()->getItemsCollection() as $_item) {
- if ($_item->isDeleted()) {
- continue;
- }
- if ($_item->getProduct()->getIsVirtual() && !$_item->getParentItemId()) {
- $items[] = $_item;
- }
- }
- return $items;
+ return $this->getBillingAddress()->getAllVisibleItems();
}
/**
diff --git a/app/code/Magento/Multishipping/Block/Checkout/Results.php b/app/code/Magento/Multishipping/Block/Checkout/Results.php
new file mode 100644
index 0000000000000..2331092be641d
--- /dev/null
+++ b/app/code/Magento/Multishipping/Block/Checkout/Results.php
@@ -0,0 +1,190 @@
+multishipping = $multishipping;
+ $this->addressConfig = $addressConfig;
+ $this->orderRepository = $orderRepository;
+ $this->session = $session;
+ }
+
+ /**
+ * Returns shipping addresses from quote.
+ *
+ * @return array
+ */
+ public function getQuoteShippingAddresses(): array
+ {
+ return $this->multishipping->getQuote()->getAllShippingAddresses();
+ }
+
+ /**
+ * Returns all failed addresses from quote.
+ *
+ * @return array
+ */
+ public function getFailedAddresses(): array
+ {
+ $addresses = $this->getQuoteShippingAddresses();
+ if ($this->getAddressError($this->getQuoteBillingAddress())) {
+ $addresses[] = $this->getQuoteBillingAddress();
+ }
+ return $addresses;
+ }
+
+ /**
+ * Retrieve order shipping address.
+ *
+ * @param int $orderId
+ * @return OrderAddress|null
+ */
+ public function getOrderShippingAddress(int $orderId)
+ {
+ return $this->orderRepository->get($orderId)->getShippingAddress();
+ }
+
+ /**
+ * Retrieve quote billing address.
+ *
+ * @return QuoteAddress
+ */
+ public function getQuoteBillingAddress(): QuoteAddress
+ {
+ return $this->getCheckout()->getQuote()->getBillingAddress();
+ }
+
+ /**
+ * Returns formatted shipping address from placed order.
+ *
+ * @param OrderAddress $address
+ * @return string
+ */
+ public function formatOrderShippingAddress(OrderAddress $address): string
+ {
+ return $this->getAddressOneline($address->getData());
+ }
+
+ /**
+ * Returns formatted shipping address from quote.
+ *
+ * @param QuoteAddress $address
+ * @return string
+ */
+ public function formatQuoteShippingAddress(QuoteAddress $address): string
+ {
+ return $this->getAddressOneline($address->getData());
+ }
+
+ /**
+ * Checks if address type is shipping.
+ *
+ * @param QuoteAddress $address
+ * @return bool
+ */
+ public function isShippingAddress(QuoteAddress $address): bool
+ {
+ return $address->getAddressType() === QuoteAddress::ADDRESS_TYPE_SHIPPING;
+ }
+
+ /**
+ * Get unescaped address formatted as one line string.
+ *
+ * @param array $address
+ * @return string
+ */
+ private function getAddressOneline(array $address): string
+ {
+ $renderer = $this->addressConfig->getFormatByCode('oneline')->getRenderer();
+
+ return $renderer->renderArray($address);
+ }
+
+ /**
+ * Returns address error.
+ *
+ * @param QuoteAddress $address
+ * @return string
+ */
+ public function getAddressError(QuoteAddress $address): string
+ {
+ $errors = $this->session->getAddressErrors();
+
+ return $errors[$address->getId()] ?? '';
+ }
+
+ /**
+ * Add title to block head.
+ *
+ * @throws LocalizedException
+ * @return Success
+ */
+ protected function _prepareLayout(): Success
+ {
+ /** @var Title $pageTitle */
+ $pageTitle = $this->getLayout()->getBlock('page.main.title');
+ if ($pageTitle) {
+ $title = $this->getOrderIds() ? $pageTitle->getPartlySuccessTitle() : $pageTitle->getFailedTitle();
+ $pageTitle->setPageTitle($title);
+ }
+
+ return parent::_prepareLayout();
+ }
+}
diff --git a/app/code/Magento/Multishipping/Block/Checkout/Success.php b/app/code/Magento/Multishipping/Block/Checkout/Success.php
index 3f18132d04a45..0f39e03c56c63 100644
--- a/app/code/Magento/Multishipping/Block/Checkout/Success.php
+++ b/app/code/Magento/Multishipping/Block/Checkout/Success.php
@@ -36,7 +36,7 @@ public function __construct(
*/
public function getOrderIds()
{
- $ids = $this->_session->getOrderIds(true);
+ $ids = $this->_session->getOrderIds();
if ($ids && is_array($ids)) {
return $ids;
}
diff --git a/app/code/Magento/Multishipping/Block/DataProviders/Billing.php b/app/code/Magento/Multishipping/Block/DataProviders/Billing.php
new file mode 100644
index 0000000000000..b2514598fd4a9
--- /dev/null
+++ b/app/code/Magento/Multishipping/Block/DataProviders/Billing.php
@@ -0,0 +1,74 @@
+addressConfig = $addressConfig;
+ $this->configProvider = $configProvider;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * Get address formatted as html string.
+ *
+ * @param Address $address
+ * @return string
+ */
+ public function getAddressHtml(Address $address): string
+ {
+ $renderer = $this->addressConfig->getFormatByCode('html')->getRenderer();
+
+ return $renderer->renderArray($address->getData());
+ }
+
+ /**
+ * Returns serialized checkout config.
+ *
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ public function getSerializedCheckoutConfigs(): string
+ {
+ return $this->serializer->serialize($this->configProvider->getConfig());
+ }
+}
diff --git a/app/code/Magento/Multishipping/Block/DataProviders/Overview.php b/app/code/Magento/Multishipping/Block/DataProviders/Overview.php
new file mode 100644
index 0000000000000..dff64b97dcd7a
--- /dev/null
+++ b/app/code/Magento/Multishipping/Block/DataProviders/Overview.php
@@ -0,0 +1,74 @@
+session = $session;
+ }
+
+ /**
+ * Returns address error.
+ *
+ * @param Address $address
+ * @return string
+ */
+ public function getAddressError(Address $address): string
+ {
+ $addressErrors = $this->getAddressErrors();
+
+ return $addressErrors[$address->getId()] ?? '';
+ }
+
+ /**
+ * Returns all stored errors.
+ *
+ * @return array
+ */
+ public function getAddressErrors(): array
+ {
+ if (empty($this->addressErrors)) {
+ $this->addressErrors = $this->session->getAddressErrors(true);
+ }
+
+ return $this->addressErrors ?? [];
+ }
+
+ /**
+ * Creates anchor name for address Id.
+ *
+ * @param int $addressId
+ * @return string
+ */
+ public function getAddressAnchorName(int $addressId): string
+ {
+ return 'a' . $addressId;
+ }
+}
diff --git a/app/code/Magento/Multishipping/Block/DataProviders/Success.php b/app/code/Magento/Multishipping/Block/DataProviders/Success.php
new file mode 100644
index 0000000000000..254a7e0fc50a9
--- /dev/null
+++ b/app/code/Magento/Multishipping/Block/DataProviders/Success.php
@@ -0,0 +1,18 @@
+getRequest()->getPost('payment', []);
- $payment['checks'] = [
- \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_COUNTRY,
- \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_CURRENCY,
- \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX,
- \Magento\Payment\Model\Method\AbstractMethod::CHECK_ZERO_TOTAL,
- ];
- $this->_getCheckout()->setPaymentMethod($payment);
-
+ if (!empty($payment)) {
+ $payment['checks'] = [
+ AbstractMethod::CHECK_USE_FOR_COUNTRY,
+ AbstractMethod::CHECK_USE_FOR_CURRENCY,
+ AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX,
+ AbstractMethod::CHECK_ZERO_TOTAL,
+ ];
+ $this->_getCheckout()->setPaymentMethod($payment);
+ }
$this->_getState()->setCompleteStep(State::STEP_BILLING);
$this->_view->loadLayout();
$this->_view->renderLayout();
- } catch (\Magento\Framework\Exception\LocalizedException $e) {
+ } catch (LocalizedException $e) {
$this->messageManager->addError($e->getMessage());
$this->_redirect('*/*/billing');
} catch (\Exception $e) {
- $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e);
+ $this->_objectManager->get(LoggerInterface::class)->critical($e);
$this->messageManager->addException($e, __('We cannot open the overview page.'));
$this->_redirect('*/*/billing');
}
diff --git a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php
index d23f8863d0728..f05a7f43b8118 100644
--- a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php
+++ b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php
@@ -9,6 +9,7 @@
use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Framework\Exception\PaymentException;
+use Magento\Framework\Session\SessionManagerInterface;
/**
* Class OverviewPost
@@ -32,6 +33,11 @@ class OverviewPost extends \Magento\Multishipping\Controller\Checkout
*/
protected $agreementsValidator;
+ /**
+ * @var SessionManagerInterface
+ */
+ private $session;
+
/**
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Customer\Model\Session $customerSession
@@ -40,6 +46,7 @@ class OverviewPost extends \Magento\Multishipping\Controller\Checkout
* @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator
* @param \Psr\Log\LoggerInterface $logger
* @param \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator
+ * @param SessionManagerInterface $session
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
@@ -48,11 +55,14 @@ public function __construct(
AccountManagementInterface $accountManagement,
\Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator,
\Psr\Log\LoggerInterface $logger,
- \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator
+ \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator,
+ SessionManagerInterface $session
) {
$this->formKeyValidator = $formKeyValidator;
$this->logger = $logger;
$this->agreementsValidator = $agreementValidator;
+ $this->session = $session;
+
parent::__construct(
$context,
$customerSession,
@@ -95,11 +105,17 @@ public function execute()
$paymentInstance->setCcCid($payment['cc_cid']);
}
$this->_getCheckout()->createOrders();
- $this->_getState()->setActiveStep(State::STEP_SUCCESS);
$this->_getState()->setCompleteStep(State::STEP_OVERVIEW);
- $this->_getCheckout()->getCheckoutSession()->clearQuote();
- $this->_getCheckout()->getCheckoutSession()->setDisplaySuccess(true);
- $this->_redirect('*/*/success');
+
+ if ($this->session->getAddressErrors()) {
+ $this->_getState()->setActiveStep(State::STEP_RESULTS);
+ $this->_redirect('*/*/results');
+ } else {
+ $this->_getState()->setActiveStep(State::STEP_SUCCESS);
+ $this->_getCheckout()->getCheckoutSession()->clearQuote();
+ $this->_getCheckout()->getCheckoutSession()->setDisplaySuccess(true);
+ $this->_redirect('*/*/success');
+ }
} catch (PaymentException $e) {
$message = $e->getMessage();
if (!empty($message)) {
diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Results.php b/app/code/Magento/Multishipping/Controller/Checkout/Results.php
new file mode 100644
index 0000000000000..863dac840803c
--- /dev/null
+++ b/app/code/Magento/Multishipping/Controller/Checkout/Results.php
@@ -0,0 +1,14 @@
+state = $state;
+ $this->multishipping = $multishipping;
+
+ parent::__construct($context);
+ }
+
/**
* Multishipping checkout success page
*
@@ -17,13 +49,13 @@ class Success extends \Magento\Multishipping\Controller\Checkout
*/
public function execute()
{
- if (!$this->_getState()->getCompleteStep(State::STEP_OVERVIEW)) {
+ if (!$this->state->getCompleteStep(State::STEP_OVERVIEW)) {
$this->_redirect('*/*/addresses');
return;
}
$this->_view->loadLayout();
- $ids = $this->_getCheckout()->getOrderIds();
+ $ids = $this->multishipping->getOrderIds();
$this->_eventManager->dispatch('multishipping_checkout_controller_success_action', ['order_ids' => $ids]);
$this->_view->renderLayout();
}
diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php
index f9d6d0adaae93..c510ccffd3611 100644
--- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php
+++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php
@@ -6,11 +6,14 @@
namespace Magento\Multishipping\Model\Checkout\Type;
use Magento\Customer\Api\AddressRepositoryInterface;
+use Magento\Framework\Exception\NotFoundException;
use Magento\Framework\Pricing\PriceCurrencyInterface;
+use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Model\Order\Email\Sender\OrderSender;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\App\ObjectManager;
use Magento\Directory\Model\AllowedCountries;
+use Psr\Log\LoggerInterface;
/**
* Multishipping checkout model
@@ -157,6 +160,16 @@ class Multishipping extends \Magento\Framework\DataObject
*/
private $shippingAssignmentProcessor;
+ /**
+ * @var Multishipping\PlaceOrderFactory
+ */
+ private $placeOrderFactory;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
/**
* Constructor
*
@@ -184,6 +197,8 @@ class Multishipping extends \Magento\Framework\DataObject
* @param array $data
* @param \Magento\Quote\Api\Data\CartExtensionFactory|null $cartExtensionFactory
* @param AllowedCountries|null $allowedCountryReader
+ * @param Multishipping\PlaceOrderFactory $placeOrderFactory
+ * @param LoggerInterface $logger
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -210,7 +225,9 @@ public function __construct(
\Magento\Quote\Model\Quote\TotalsCollector $totalsCollector,
array $data = [],
\Magento\Quote\Api\Data\CartExtensionFactory $cartExtensionFactory = null,
- AllowedCountries $allowedCountryReader = null
+ AllowedCountries $allowedCountryReader = null,
+ Multishipping\PlaceOrderFactory $placeOrderFactory = null,
+ LoggerInterface $logger = null
) {
$this->_eventManager = $eventManager;
$this->_scopeConfig = $scopeConfig;
@@ -237,6 +254,10 @@ public function __construct(
->get(\Magento\Quote\Api\Data\CartExtensionFactory::class);
$this->allowedCountryReader = $allowedCountryReader ?: ObjectManager::getInstance()
->get(AllowedCountries::class);
+ $this->placeOrderFactory = $placeOrderFactory ?: ObjectManager::getInstance()
+ ->get(Multishipping\PlaceOrderFactory::class);
+ $this->logger = $logger ?: ObjectManager::getInstance()
+ ->get(LoggerInterface::class);
parent::__construct($data);
$this->_init();
}
@@ -760,21 +781,48 @@ public function createOrders()
);
}
+ $paymentProviderCode = $this->getQuote()->getPayment()->getMethod();
+ $placeOrderService = $this->placeOrderFactory->create($paymentProviderCode);
+ $exceptionList = $placeOrderService->place($orders);
+ $this->logExceptions($exceptionList);
+
+ /** @var OrderInterface[] $failedOrders */
+ $failedOrders = [];
+ /** @var OrderInterface[] $successfulOrders */
+ $successfulOrders = [];
foreach ($orders as $order) {
- $order->place();
- $order->save();
+ if (isset($exceptionList[$order->getIncrementId()])) {
+ $failedOrders[] = $order;
+ } else {
+ $successfulOrders[] = $order;
+ }
+ }
+
+ $placedAddressItems = [];
+ foreach ($successfulOrders as $order) {
+ $orderIds[$order->getId()] = $order->getIncrementId();
if ($order->getCanSendNewEmailFlag()) {
$this->orderSender->send($order);
}
- $orderIds[$order->getId()] = $order->getIncrementId();
+ $placedAddressItems = array_merge($placedAddressItems, $this->getQuoteAddressItems($order));
}
- $this->_session->setOrderIds($orderIds);
- $this->_checkoutSession->setLastQuoteId($this->getQuote()->getId());
-
- $this->getQuote()->setIsActive(false);
- $this->quoteRepository->save($this->getQuote());
+ $addressErrors = [];
+ if (!empty($failedOrders)) {
+ $this->removePlacedItemsFromQuote($shippingAddresses, $placedAddressItems);
+ $addressErrors = $this->getQuoteAddressErrors(
+ $failedOrders,
+ $shippingAddresses,
+ $exceptionList
+ );
+ } else {
+ $this->_checkoutSession->setLastQuoteId($this->getQuote()->getId());
+ $this->getQuote()->setIsActive(false);
+ $this->quoteRepository->save($this->getQuote());
+ }
+ $this->_session->setOrderIds($orderIds);
+ $this->_session->setAddressErrors($addressErrors);
$this->_eventManager->dispatch(
'checkout_submit_all_after',
['orders' => $orders, 'quote' => $this->getQuote()]
@@ -787,6 +835,19 @@ public function createOrders()
}
}
+ /**
+ * Logs exceptions.
+ *
+ * @param \Exception[] $exceptionList
+ * @return void
+ */
+ private function logExceptions(array $exceptionList)
+ {
+ foreach ($exceptionList as $exception) {
+ $this->logger->critical($exception);
+ }
+ }
+
/**
* Collect quote totals and save quote object
*
@@ -817,13 +878,21 @@ public function reset()
*/
public function validateMinimumAmount()
{
- return !($this->_scopeConfig->isSetFlag(
+ $minimumOrderActive = $this->_scopeConfig->isSetFlag(
'sales/minimum_order/active',
\Magento\Store\Model\ScopeInterface::SCOPE_STORE
- ) && $this->_scopeConfig->isSetFlag(
+ );
+
+ if ($this->_scopeConfig->isSetFlag(
'sales/minimum_order/multi_address',
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE
- ) && !$this->getQuote()->validateMinimumAmount());
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE)
+ ) {
+ $result = !($minimumOrderActive && !$this->getQuote()->validateMinimumAmount());
+ } else {
+ $result = !($minimumOrderActive && !$this->validateMinimumAmountForAddressItems());
+ }
+
+ return $result;
}
/**
@@ -873,7 +942,10 @@ public function getMinimumAmountError()
public function getOrderIds($asAssoc = false)
{
$idsAssoc = $this->_session->getOrderIds();
- return $asAssoc ? $idsAssoc : array_keys($idsAssoc);
+ if ($idsAssoc !== null) {
+ return $asAssoc ? $idsAssoc : array_keys($idsAssoc);
+ }
+ return [];
}
/**
@@ -1031,4 +1103,149 @@ private function getShippingAssignmentProcessor()
}
return $this->shippingAssignmentProcessor;
}
+
+ /**
+ * Validate minimum amount for "Checkout with Multiple Addresses" when
+ * "Validate Each Address Separately in Multi-address Checkout" is No.
+ *
+ * @return bool
+ */
+ private function validateMinimumAmountForAddressItems()
+ {
+ $result = true;
+ $storeId = $this->getQuote()->getStoreId();
+
+ $minAmount = $this->_scopeConfig->getValue(
+ 'sales/minimum_order/amount',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ $taxInclude = $this->_scopeConfig->getValue(
+ 'sales/minimum_order/tax_including',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+
+ $addresses = $this->getQuote()->getAllAddresses();
+
+ $baseTotal = 0;
+ foreach ($addresses as $address) {
+ $taxes = $taxInclude ? $address->getBaseTaxAmount() : 0;
+ $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes;
+ }
+
+ if ($baseTotal < $minAmount) {
+ $result = false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Remove successfully placed items from quote.
+ *
+ * @param \Magento\Quote\Model\Quote\Address[] $shippingAddresses
+ * @param int[] $placedAddressItems
+ * @return void
+ */
+ private function removePlacedItemsFromQuote(array $shippingAddresses, array $placedAddressItems)
+ {
+ foreach ($shippingAddresses as $address) {
+ foreach ($address->getAllItems() as $addressItem) {
+ if (in_array($addressItem->getId(), $placedAddressItems)) {
+ if ($addressItem->getProduct()->getIsVirtual()) {
+ $addressItem->isDeleted(true);
+ } else {
+ $address->isDeleted(true);
+ }
+
+ $this->decreaseQuoteItemQty($addressItem->getQuoteItemId(), $addressItem->getQty());
+ }
+ }
+ }
+ $this->save();
+ }
+
+ /**
+ * Decrease quote item quantity.
+ *
+ * @param int $quoteItemId
+ * @param int $qty
+ * @return void
+ */
+ private function decreaseQuoteItemQty(int $quoteItemId, int $qty)
+ {
+ $quoteItem = $this->getQuote()->getItemById($quoteItemId);
+ if ($quoteItem) {
+ $newItemQty = $quoteItem->getQty() - $qty;
+ if ($newItemQty > 0) {
+ $quoteItem->setQty($newItemQty);
+ } else {
+ $this->getQuote()->removeItem($quoteItem->getId());
+ $this->getQuote()->setIsMultiShipping(1);
+ }
+ }
+ }
+
+ /**
+ * Returns quote address id that was assigned to order.
+ *
+ * @param OrderInterface $order
+ * @param \Magento\Quote\Model\Quote\Address[] $addresses
+ *
+ * @return int
+ * @throws NotFoundException
+ */
+ private function searchQuoteAddressId(OrderInterface $order, array $addresses): int
+ {
+ $items = $order->getItems();
+ $item = array_pop($items);
+ foreach ($addresses as $address) {
+ foreach ($address->getAllItems() as $addressItem) {
+ if ($addressItem->getId() == $item->getQuoteItemId()) {
+ return (int)$address->getId();
+ }
+ }
+ }
+
+ throw new NotFoundException(__('Quote address for failed order not found.'));
+ }
+
+ /**
+ * @param OrderInterface[] $orders
+ * @param \Magento\Quote\Model\Quote\Address[] $addresses
+ * @param \Exception[] $exceptionList
+ *
+ * @return string[]
+ * @throws NotFoundException
+ */
+ private function getQuoteAddressErrors(array $orders, array $addresses, array $exceptionList): array
+ {
+ $addressErrors = [];
+ foreach ($orders as $failedOrder) {
+ if (!isset($exceptionList[$failedOrder->getIncrementId()])) {
+ throw new NotFoundException(__('Exception for failed order not found.'));
+ }
+ $addressId = $this->searchQuoteAddressId($failedOrder, $addresses);
+ $addressErrors[$addressId] = $exceptionList[$failedOrder->getIncrementId()]->getMessage();
+ }
+
+ return $addressErrors;
+ }
+
+ /**
+ * Returns quote address item id.
+ *
+ * @param OrderInterface $order
+ * @return array
+ */
+ private function getQuoteAddressItems(OrderInterface $order): array
+ {
+ $placedAddressItems = [];
+ foreach ($order->getItems() as $orderItem) {
+ $placedAddressItems[] = $orderItem->getQuoteItemId();
+ }
+
+ return $placedAddressItems;
+ }
}
diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderDefault.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderDefault.php
new file mode 100644
index 0000000000000..03904102341a3
--- /dev/null
+++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderDefault.php
@@ -0,0 +1,46 @@
+orderManagement = $orderManagement;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function place(array $orderList): array
+ {
+ $errorList = [];
+ foreach ($orderList as $order) {
+ try {
+ $this->orderManagement->place($order);
+ } catch (\Exception $e) {
+ $incrementId = $order->getIncrementId();
+ $errorList[$incrementId] = $e;
+ }
+ }
+
+ return $errorList;
+ }
+}
diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderFactory.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderFactory.php
new file mode 100644
index 0000000000000..00ab729a4a9ef
--- /dev/null
+++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderFactory.php
@@ -0,0 +1,50 @@
+objectManager = $objectManager;
+ $this->placeOrderPool = $placeOrderPool;
+ }
+
+ /**
+ * @param string $paymentProviderCode
+ * @return PlaceOrderInterface
+ */
+ public function create(string $paymentProviderCode): PlaceOrderInterface
+ {
+ $service = $this->placeOrderPool->get($paymentProviderCode);
+ if ($service === null) {
+ $service = $this->objectManager->get(PlaceOrderDefault::class);
+ }
+
+ return $service;
+ }
+}
diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php
new file mode 100644
index 0000000000000..70b4c6f99278a
--- /dev/null
+++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php
@@ -0,0 +1,22 @@
+services = $tmapFactory->createSharedObjectsMap(
+ [
+ 'array' => $services,
+ 'type' => PlaceOrderInterface::class
+ ]
+ );
+ }
+
+ /**
+ * Returns place order service for defined payment provider.
+ *
+ * @param string $paymentProviderCode
+ * @return PlaceOrderInterface|null
+ */
+ public function get(string $paymentProviderCode)
+ {
+ if (!isset($this->services[$paymentProviderCode])) {
+ return null;
+ }
+
+ return $this->services[$paymentProviderCode];
+ }
+}
diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/State.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/State.php
index 2615ad1b540c8..2e53817351d3c 100644
--- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/State.php
+++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/State.php
@@ -25,6 +25,8 @@ class State extends \Magento\Framework\DataObject
const STEP_SUCCESS = 'multishipping_success';
+ const STEP_RESULTS = 'multishipping_results';
+
/**
* Allow steps array
*
@@ -61,6 +63,7 @@ public function __construct(Session $checkoutSession, Multishipping $multishippi
self::STEP_BILLING => new \Magento\Framework\DataObject(['label' => __('Billing Information')]),
self::STEP_OVERVIEW => new \Magento\Framework\DataObject(['label' => __('Place Order')]),
self::STEP_SUCCESS => new \Magento\Framework\DataObject(['label' => __('Order Success')]),
+ self::STEP_RESULTS => new \Magento\Framework\DataObject(['label' => __('Order Results')]),
];
foreach ($this->_steps as $step) {
diff --git a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/SuccessTest.php b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/SuccessTest.php
index 26e22487cf445..403e6cfcb48dc 100644
--- a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/SuccessTest.php
+++ b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/SuccessTest.php
@@ -56,14 +56,14 @@ protected function setUp()
public function testGetOrderIdsWithoutId()
{
- $this->sessionMock->expects($this->once())->method('getOrderIds')->with(true)->will($this->returnValue(null));
+ $this->sessionMock->method('getOrderIds')->willReturn(null);
$this->assertFalse($this->model->getOrderIds());
}
public function testGetOrderIdsWithEmptyIdsArray()
{
- $this->sessionMock->expects($this->once())->method('getOrderIds')->with(true)->will($this->returnValue([]));
+ $this->sessionMock->method('getOrderIds')->willReturn([]);
$this->assertFalse($this->model->getOrderIds());
}
@@ -71,7 +71,7 @@ public function testGetOrderIdsWithEmptyIdsArray()
public function testGetOrderIds()
{
$ids = [100, 102, 103];
- $this->sessionMock->expects($this->once())->method('getOrderIds')->with(true)->will($this->returnValue($ids));
+ $this->sessionMock->method('getOrderIds')->willReturn($ids);;
$this->assertEquals($ids, $this->model->getOrderIds());
}
diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderDefaultTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderDefaultTest.php
new file mode 100644
index 0000000000000..61a88b8810f15
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderDefaultTest.php
@@ -0,0 +1,68 @@
+orderManagement = $this->getMockForAbstractClass(OrderManagementInterface::class);
+
+ $this->placeOrderDefault = new PlaceOrderDefault($this->orderManagement);
+ }
+
+ public function testPlace()
+ {
+ $incrementId = '000000001';
+
+ $order = $this->getMockForAbstractClass(OrderInterface::class);
+ $order->method('getIncrementId')->willReturn($incrementId);
+ $orderList = [$order];
+
+ $this->orderManagement->expects($this->once())
+ ->method('place')
+ ->with($order)
+ ->willReturn($order);
+ $errors = $this->placeOrderDefault->place($orderList);
+
+ $this->assertEmpty($errors);
+ }
+
+ public function testPlaceWithErrors()
+ {
+ $incrementId = '000000001';
+
+ $order = $this->getMockForAbstractClass(OrderInterface::class);
+ $order->method('getIncrementId')->willReturn($incrementId);
+ $orderList = [$order];
+
+ $exception = new \Exception('error');
+ $this->orderManagement->method('place')->willThrowException($exception);
+ $errors = $this->placeOrderDefault->place($orderList);
+
+ $this->assertEquals(
+ [$incrementId => $exception],
+ $errors
+ );
+ }
+}
diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderFactoryTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderFactoryTest.php
new file mode 100644
index 0000000000000..4b92c739f55c0
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderFactoryTest.php
@@ -0,0 +1,80 @@
+objectManager = $this->getMockForAbstractClass(ObjectManagerInterface::class);
+
+ $this->placeOrderPool = $this->getMockBuilder(PlaceOrderPool::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->placeOrderFactory = new PlaceOrderFactory($this->objectManager, $this->placeOrderPool);
+ }
+
+ public function testCreate()
+ {
+ $paymentProviderCode = 'code';
+
+ $placeOrder = $this->getMockForAbstractClass(PlaceOrderInterface::class);
+ $this->placeOrderPool->method('get')
+ ->with($paymentProviderCode)
+ ->willReturn($placeOrder);
+
+ $instance = $this->placeOrderFactory->create($paymentProviderCode);
+
+ $this->assertInstanceOf(PlaceOrderInterface::class, $instance);
+ }
+
+ /**
+ * Checks that default place order service is created when place order pull returns null.
+ */
+ public function testCreateWithDefault()
+ {
+ $paymentProviderCode = 'code';
+
+ $this->placeOrderPool->method('get')
+ ->with($paymentProviderCode)
+ ->willReturn(null);
+ $placeOrder = $this->getMockBuilder(PlaceOrderDefault::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManager->method('get')
+ ->with(PlaceOrderDefault::class)
+ ->willReturn($placeOrder);
+
+ $instance = $this->placeOrderFactory->create($paymentProviderCode);
+
+ $this->assertInstanceOf(PlaceOrderDefault::class, $instance);
+ }
+}
diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderPoolTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderPoolTest.php
new file mode 100644
index 0000000000000..9289f5afd1aad
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/Multishipping/PlaceOrderPoolTest.php
@@ -0,0 +1,48 @@
+getMockBuilder(TMapFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $tMapFactory->method('createSharedObjectsMap')->willReturn($placeOrderList);
+
+ $placeOrderPool = new PlaceOrderPool($tMapFactory);
+ $result = $placeOrderPool->get($paymentProviderCode);
+
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDataProvider(): array
+ {
+ $placeOrder = $this->getMockForAbstractClass(PlaceOrderInterface::class);
+ $placeOrderList = ['payment_code' => $placeOrder];
+
+ return [
+ 'code exists in pool' => ['payment_code', $placeOrderList, $placeOrder],
+ 'no code in pool' => ['some_code', $placeOrderList, null],
+ ];
+ }
+}
diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php
index 1d779c11d5935..f90e85a904352 100644
--- a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php
+++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php
@@ -118,13 +118,18 @@ class MultishippingTest extends \PHPUnit\Framework\TestCase
*/
private $quoteRepositoryMock;
+ /**
+ * @var PHPUnit_Framework_MockObject_MockObject
+ */
+ private $scopeConfigMock;
+
protected function setUp()
{
$this->checkoutSessionMock = $this->createSimpleMock(Session::class);
$this->customerSessionMock = $this->createSimpleMock(CustomerSession::class);
$orderFactoryMock = $this->createSimpleMock(OrderFactory::class);
$eventManagerMock = $this->createSimpleMock(ManagerInterface::class);
- $scopeConfigMock = $this->createSimpleMock(ScopeConfigInterface::class);
+ $this->scopeConfigMock = $this->createSimpleMock(ScopeConfigInterface::class);
$sessionMock = $this->createSimpleMock(Generic::class);
$addressFactoryMock = $this->createSimpleMock(AddressFactory::class);
$toOrderMock = $this->createSimpleMock(ToOrder::class);
@@ -166,7 +171,7 @@ protected function setUp()
$orderFactoryMock,
$this->addressRepositoryMock,
$eventManagerMock,
- $scopeConfigMock,
+ $this->scopeConfigMock,
$sessionMock,
$addressFactoryMock,
$toOrderMock,
@@ -497,4 +502,37 @@ private function createSimpleMock($className)
->disableOriginalConstructor()
->getMock();
}
+
+ public function testValidateMinimumAmountMultiAddressTrue()
+ {
+ $this->scopeConfigMock->expects($this->exactly(2))->method('isSetFlag')->withConsecutive(
+ ['sales/minimum_order/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE],
+ ['sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE]
+ )->willReturnOnConsecutiveCalls(true, true);
+
+ $this->checkoutSessionMock->expects($this->atLeastOnce())->method('getQuote')->willReturn($this->quoteMock);
+ $this->quoteMock->expects($this->once())->method('validateMinimumAmount')->willReturn(false);
+ $this->assertFalse($this->model->validateMinimumAmount());
+ }
+
+ public function testValidateMinimumAmountMultiAddressFalse()
+ {
+ $addressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class);
+ $this->scopeConfigMock->expects($this->exactly(2))->method('isSetFlag')->withConsecutive(
+ ['sales/minimum_order/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE],
+ ['sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE]
+ )->willReturnOnConsecutiveCalls(true, false);
+
+ $this->scopeConfigMock->expects($this->exactly(2))->method('getValue')->withConsecutive(
+ ['sales/minimum_order/amount', \Magento\Store\Model\ScopeInterface::SCOPE_STORE],
+ ['sales/minimum_order/tax_including', \Magento\Store\Model\ScopeInterface::SCOPE_STORE]
+ )->willReturnOnConsecutiveCalls(100, false);
+
+ $this->checkoutSessionMock->expects($this->atLeastOnce())->method('getQuote')->willReturn($this->quoteMock);
+ $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn(1);
+ $this->quoteMock->expects($this->once())->method('getAllAddresses')->willReturn([$addressMock]);
+ $addressMock->expects($this->once())->method('getBaseSubtotalWithDiscount')->willReturn(101);
+
+ $this->assertTrue($this->model->validateMinimumAmount());
+ }
}
diff --git a/app/code/Magento/Multishipping/composer.json b/app/code/Magento/Multishipping/composer.json
index 456248065e949..2e7e5160a55bf 100644
--- a/app/code/Magento/Multishipping/composer.json
+++ b/app/code/Magento/Multishipping/composer.json
@@ -3,21 +3,19 @@
"description": "N/A",
"require": {
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/framework": "101.0.*",
"magento/module-store": "100.2.*",
"magento/module-checkout": "100.2.*",
- "magento/module-sales": "100.2.*",
+ "magento/module-sales": "101.0.*",
"magento/module-payment": "100.2.*",
"magento/module-tax": "100.2.*",
- "magento/module-customer": "100.2.*",
- "magento/module-quote": "100.2.*",
- "magento/framework": "100.2.*",
- "magento/module-directory": "100.2.*"
- },
- "suggest": {
+ "magento/module-customer": "101.0.*",
+ "magento/module-quote": "101.0.*",
+ "magento/module-directory": "100.2.*",
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.0",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_billing.xml
index 8d78bad7a9ecc..5fcca5d9214a6 100644
--- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_billing.xml
+++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_billing.xml
@@ -15,6 +15,9 @@
+
+ Magento\Multishipping\Block\DataProviders\Billing
+
diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_overview.xml
index f6584d2dcb2fd..376bc7b7d8ca8 100644
--- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_overview.xml
+++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_overview.xml
@@ -17,6 +17,7 @@
+ Magento\Multishipping\Block\DataProviders\Overview
Magento_Multishipping::checkout/item/default.phtml
Magento_Multishipping::checkout/overview/item.phtml
diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_results.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_results.xml
new file mode 100644
index 0000000000000..90f13ea28e02f
--- /dev/null
+++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_results.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ Order results
+
+
+
+
+ We could only complete part of your order.
+ We were unable to complete your order.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_success.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_success.xml
index 98dd6e21910f8..d03282e551b9e 100644
--- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_success.xml
+++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_success.xml
@@ -12,11 +12,15 @@
- We received your order!
+ Thank you for your purchase!
-
+
+
+ Magento\Multishipping\Block\DataProviders\Success
+
+
diff --git a/app/code/Magento/Multishipping/view/frontend/requirejs-config.js b/app/code/Magento/Multishipping/view/frontend/requirejs-config.js
index c00005138eee4..f14159ba0a85a 100644
--- a/app/code/Magento/Multishipping/view/frontend/requirejs-config.js
+++ b/app/code/Magento/Multishipping/view/frontend/requirejs-config.js
@@ -8,7 +8,8 @@ var config = {
'*': {
multiShipping: 'Magento_Multishipping/js/multi-shipping',
orderOverview: 'Magento_Multishipping/js/overview',
- payment: 'Magento_Multishipping/js/payment'
+ payment: 'Magento_Multishipping/js/payment',
+ billingLoader: 'Magento_Checkout/js/checkout-loader'
}
}
};
diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml
index 277328c1b5b2a..d8514ca77f9c2 100644
--- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml
+++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml
@@ -4,52 +4,109 @@
* See COPYING.txt for license details.
*/
-// @codingStandardsIgnoreFile
-
-?>
-
-