diff --git a/.gitignore b/.gitignore
index 9269cefc51fa0..b7f681b292e4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,9 +23,12 @@ atlassian*
/lib/internal/flex/varien/.settings
/node_modules
/.grunt
+/Gruntfile.js
+/package.json
/pub/media/*.*
!/pub/media/.htaccess
+/pub/media/analytics/*
/pub/media/catalog/*
!/pub/media/catalog/.htaccess
/pub/media/customer/*
@@ -49,3 +52,5 @@ atlassian*
!/var/.htaccess
/vendor/*
!/vendor/.htaccess
+/generated/*
+!/generated/.htaccess
diff --git a/.php_cs b/.php_cs
index 5bd1a01537611..4bd705bb09a2f 100644
--- a/.php_cs
+++ b/.php_cs
@@ -1,6 +1,6 @@
Welcome
Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting edge, feature-rich eCommerce solution that gets results.
-The installation instructions that used to be here are now published on our GitHub site. Use the information on this page to get started or go directly to the guide .
+## Magento system requirements
+[Magento system requirements](http://devdocs.magento.com/magento-system-requirements.html)
-
New to Magento? Need some help?
-If you're not sure about the following, you probably need a little help before you start installing the Magento software:
+## Install Magento
+To install Magento, see either:
-* Is the Magento software installed already ?
-* What's a terminal, command prompt, or Secure Shell (ssh) ?
-* Where's my Magento server and how do I access it?
-* What's PHP ?
-* What's Apache ?
-* What's MySQL ?
-
-Step 1: Verify your prerequisites
-
-Use the following table to verify you have the correct prerequisites to install the Magento software.
-
-
-
-
- Prerequisite
- How to check
- For more information
-
-
- Apache 2.2 or 2.4
- Ubuntu: apache2 -v
- CentOS: httpd -v
- Apache
-
-
- PHP 5.6.x, 7.0.2 or 7.0.6
- php -v
- PHP Ubuntu PHP CentOS
-
- MySQL 5.6.x
- mysql -u [root user name] -p
- MySQL
-
-
-
-
-Step 2: Prepare to install
-
-After verifying your prerequisites, perform the following tasks in order to prepare to install the Magento software.
-
-1. Install Composer
-2. Clone the Magento repository
-
-Step 3: Install and verify the installation
-
-1. Update installation dependencies
-2. Install Magento:
- * Install Magento software using the web interface
- * Install Magento software using the command line
-2. Verify the installation
+* [Magento DevBox](https://magento.com/tech-resources/download), the easiest way to get started with Magento.
+* [Installation guide](http://devdocs.magento.com/guides/v2.0/install-gde/bk-install-guide.html)
Contributing to the Magento 2 code base
Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions.
diff --git a/app/autoload.php b/app/autoload.php
index b817bafa7fa4c..6fdfcd07fdff1 100644
--- a/app/autoload.php
+++ b/app/autoload.php
@@ -2,7 +2,7 @@
/**
* Register basic autoloader that uses include path
*
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
use Magento\Framework\Autoload\AutoloaderRegistry;
diff --git a/app/bootstrap.php b/app/bootstrap.php
index ec60a1708dacc..eed7b6d798ad3 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -1,6 +1,6 @@
= 50600 && PHP_VERSION_ID < 50700 || PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) {
+if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID >= 50605 && PHP_VERSION_ID < 50700 || PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) {
if (PHP_SAPI == 'cli') {
- echo 'Magento supports PHP 5.6, 7.0.2, 7.0.4, and 7.0.6 or later. ' .
+ echo 'Magento supports PHP 5.6.5, 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';
} else {
echo <<
- Magento supports PHP 5.6, 7.0.2, 7.0.4, and 7.0.6 or later. Please read
+
Magento supports PHP 5.6.5, 7.0.2, 7.0.4, and 7.0.6 or later. Please read
Magento System Requirements .
@@ -35,6 +35,17 @@
$mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002;
umask($mask);
+if (empty($_SERVER['ENABLE_IIS_REWRITES']) || ($_SERVER['ENABLE_IIS_REWRITES'] != 1)) {
+ /*
+ * Unset headers used by IIS URL rewrites.
+ */
+ unset($_SERVER['HTTP_X_REWRITE_URL']);
+ unset($_SERVER['HTTP_X_ORIGINAL_URL']);
+ unset($_SERVER['IIS_WasUrlRewritten']);
+ unset($_SERVER['UNENCODED_URL']);
+ unset($_SERVER['ORIG_PATH_INFO']);
+}
+
if (!empty($_SERVER['MAGE_PROFILER'])
&& isset($_SERVER['HTTP_ACCEPT'])
&& strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false
@@ -47,3 +58,7 @@
}
date_default_timezone_set('UTC');
+
+/* Adjustment of precision value for several versions of PHP */
+ini_set('precision', 17);
+ini_set('serialize_precision', 17);
diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php
index 322c61b32a534..a7b11138628a4 100644
--- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php
+++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php
@@ -2,7 +2,7 @@
/**
* Adminhtml AdminNotification Severity Renderer
*
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Notice.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Notice.php
index b370cbd8b1a6f..aa95028cfb4a0 100644
--- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Notice.php
+++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Notice.php
@@ -2,7 +2,7 @@
/**
* Adminhtml AdminNotification Severity Renderer
*
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\AdminNotification\Block\Grid\Renderer;
diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Severity.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Severity.php
index 7d0ccd163b8e0..c348b6cc76101 100644
--- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Severity.php
+++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Severity.php
@@ -2,7 +2,7 @@
/**
* Adminhtml AdminNotification Severity Renderer
*
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\AdminNotification\Block\Grid\Renderer;
diff --git a/app/code/Magento/AdminNotification/Block/Inbox.php b/app/code/Magento/AdminNotification/Block/Inbox.php
index 899d141c9b07f..984de28fd4b94 100644
--- a/app/code/Magento/AdminNotification/Block/Inbox.php
+++ b/app/code/Magento/AdminNotification/Block/Inbox.php
@@ -2,7 +2,7 @@
/**
* Adminhtml AdminNotification inbox grid
*
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\AdminNotification\Block;
diff --git a/app/code/Magento/AdminNotification/Block/System/Messages.php b/app/code/Magento/AdminNotification/Block/System/Messages.php
index 223941c6e43f7..db2ecf62f0685 100644
--- a/app/code/Magento/AdminNotification/Block/System/Messages.php
+++ b/app/code/Magento/AdminNotification/Block/System/Messages.php
@@ -1,6 +1,6 @@
diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/di.xml b/app/code/Magento/AdminNotification/etc/adminhtml/di.xml
index 8986ce73472e5..1be5f99616cc3 100644
--- a/app/code/Magento/AdminNotification/etc/adminhtml/di.xml
+++ b/app/code/Magento/AdminNotification/etc/adminhtml/di.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/events.xml b/app/code/Magento/AdminNotification/etc/adminhtml/events.xml
index 07bd1ae5e8e92..9c897af0a2f94 100644
--- a/app/code/Magento/AdminNotification/etc/adminhtml/events.xml
+++ b/app/code/Magento/AdminNotification/etc/adminhtml/events.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml
index f64dcbdb51cfd..47c6644b9f369 100644
--- a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml
+++ b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/routes.xml b/app/code/Magento/AdminNotification/etc/adminhtml/routes.xml
index 9ca1f2d4cd1ae..a710049993270 100644
--- a/app/code/Magento/AdminNotification/etc/adminhtml/routes.xml
+++ b/app/code/Magento/AdminNotification/etc/adminhtml/routes.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/system.xml b/app/code/Magento/AdminNotification/etc/adminhtml/system.xml
index 8038b914fd65b..ab0cff5f897c1 100644
--- a/app/code/Magento/AdminNotification/etc/adminhtml/system.xml
+++ b/app/code/Magento/AdminNotification/etc/adminhtml/system.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/config.xml b/app/code/Magento/AdminNotification/etc/config.xml
index f1a1abb44546b..c088f7b5e11a7 100644
--- a/app/code/Magento/AdminNotification/etc/config.xml
+++ b/app/code/Magento/AdminNotification/etc/config.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/di.xml b/app/code/Magento/AdminNotification/etc/di.xml
index a5f94685ed9d1..03e415dd3714e 100644
--- a/app/code/Magento/AdminNotification/etc/di.xml
+++ b/app/code/Magento/AdminNotification/etc/di.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/etc/module.xml b/app/code/Magento/AdminNotification/etc/module.xml
index 77d9d4877ac0b..8fdfc713293d3 100644
--- a/app/code/Magento/AdminNotification/etc/module.xml
+++ b/app/code/Magento/AdminNotification/etc/module.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/registration.php b/app/code/Magento/AdminNotification/registration.php
index ab3e6a6170db0..9bd6a540462f7 100644
--- a/app/code/Magento/AdminNotification/registration.php
+++ b/app/code/Magento/AdminNotification/registration.php
@@ -1,6 +1,6 @@
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_index.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_index.xml
index 1d3650fa2afcb..1083e32f3c942 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_index.xml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_index.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml
index 780e48378a3a4..4ac9f806e8c66 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/requirejs-config.js b/app/code/Magento/AdminNotification/view/adminhtml/requirejs-config.js
index 69739a39dd4e3..c866e796e0ade 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/requirejs-config.js
+++ b/app/code/Magento/AdminNotification/view/adminhtml/requirejs-config.js
@@ -1,5 +1,5 @@
/**
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
@@ -10,4 +10,4 @@ var config = {
toolbarEntry: 'Magento_AdminNotification/toolbar_entry'
}
}
-};
\ No newline at end of file
+};
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml
index 260c7f9be9684..5e0e23246764a 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml
@@ -1,6 +1,6 @@
getNoticeMessageText(); ?>
getReadDetailsText(); ?>
-
\ No newline at end of file
+
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml
index cd4a9a88ebe0c..a70a5e97b57c3 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml
@@ -1,6 +1,6 @@
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/columns/message.js b/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/columns/message.js
index aa5477ebafcf0..aedf996278834 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/columns/message.js
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/columns/message.js
@@ -1,5 +1,5 @@
/**
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
define([
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/listing.js b/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/listing.js
index 1fbda8d0196ca..ae17af57fbef4 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/listing.js
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/js/grid/listing.js
@@ -1,5 +1,5 @@
/**
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/system/notification.js b/app/code/Magento/AdminNotification/view/adminhtml/web/system/notification.js
index e3d170ef09c57..2628174c1cd25 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/web/system/notification.js
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/system/notification.js
@@ -1,7 +1,8 @@
/**
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
define([
'jquery',
'mage/template',
@@ -15,20 +16,22 @@ define([
modalClass: 'modal-system-messages',
systemMessageTemplate:
'<% _.each(data.items, function(item) { %>' +
- '
' +
+ ' ' +
'<%= item.text %>' +
' ' +
'<% }); %>'
},
- _create: function() {
+ /** @inheritdoc */
+ _create: function () {
this.options.title = $('#message-system-all').attr('title');
this._super();
},
+ /** @inheritdoc */
openModal: function (severity) {
var superMethod = $.proxy(this._super, this);
- //this.modal.options
$.ajax({
url: this.options.ajaxUrl,
@@ -56,6 +59,8 @@ define([
return this;
},
+
+ /** @inheritdoc */
closeModal: function () {
this._super();
}
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/cells/message.html b/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/cells/message.html
index 869842e8ee87b..b6f659885e864 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/cells/message.html
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/cells/message.html
@@ -1,8 +1,8 @@
\ No newline at end of file
+ html="$col.getLabel($row())"/>
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/listing.html b/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/listing.html
index 80f2f4f2423fb..bce380af785a4 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/listing.html
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/template/grid/listing.html
@@ -1,6 +1,6 @@
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/toolbar_entry.js b/app/code/Magento/AdminNotification/view/adminhtml/web/toolbar_entry.js
index 2d69f616f029a..ede614f116147 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/web/toolbar_entry.js
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/toolbar_entry.js
@@ -1,17 +1,23 @@
/**
- * Copyright © 2016 Magento. All rights reserved.
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
define([
- "jquery",
- "jquery/ui",
- "domReady!"
+ 'jquery',
+ 'jquery/ui',
+ 'domReady!'
], function ($) {
'use strict';
- // Mark notification as read via AJAX call
+ /**
+ * Mark notification as read via AJAX call.
+ *
+ * @param {String} notificationId
+ */
var markNotificationAsRead = function (notificationId) {
var requestUrl = $('.notifications-wrapper .admin__action-dropdown-menu').attr('data-mark-as-read-url');
+
$.ajax({
url: requestUrl,
type: 'POST',
@@ -22,19 +28,24 @@ define([
showLoader: false
});
},
-
notificationCount = $('.notifications-wrapper').attr('data-notification-count'),
- // Remove notification from the list
+ /**
+ * Remove notification from the list.
+ *
+ * @param {jQuery} notificationEntry
+ */
removeNotificationFromList = function (notificationEntry) {
+ var notificationIcon, actionElement;
+
notificationEntry.remove();
notificationCount--;
$('.notifications-wrapper').attr('data-notification-count', notificationCount);
- if (notificationCount == 0) {
+ if (notificationCount == 0) {// eslint-disable-line eqeqeq
// Change appearance of the bubble and its behavior when the last notification is removed
$('.notifications-wrapper .admin__action-dropdown-menu').remove();
- var notificationIcon = $('.notifications-wrapper .notifications-icon');
+ notificationIcon = $('.notifications-wrapper .notifications-icon');
notificationIcon.removeAttr('data-toggle');
notificationIcon.off('click.dropdown');
$('.notifications-action .notifications-counter').text('').hide();
@@ -45,12 +56,16 @@ define([
}
$('.notifications-entry-last .notifications-counter').text(notificationCount);
// Modify caption of the 'See All' link
- var actionElement = $('.notifications-wrapper .admin__action-dropdown-menu .last .action-more');
+ actionElement = $('.notifications-wrapper .admin__action-dropdown-menu .last .action-more');
actionElement.text(actionElement.text().replace(/\d+/, notificationCount));
}
},
- // Show notification details
+ /**
+ * Show notification details.
+ *
+ * @param {jQuery} notificationEntry
+ */
showNotificationDetails = function (notificationEntry) {
var notificationDescription = notificationEntry.find('.notifications-entry-description'),
notificationDescriptionEnd = notificationEntry.find('.notifications-entry-description-end');
@@ -59,20 +74,22 @@ define([
notificationDescriptionEnd.addClass('_show');
}
- if(notificationDescription.hasClass('_cutted')) {
+ if (notificationDescription.hasClass('_cutted')) {
notificationDescription.removeClass('_cutted');
}
};
// Show notification description when corresponding item is clicked
- $('.notifications-wrapper .admin__action-dropdown-menu .notifications-entry').on('click.showNotification', function (event) {
- // hide notification dropdown
- $('.notifications-wrapper .notifications-icon').trigger('click.dropdown');
+ $('.notifications-wrapper .admin__action-dropdown-menu .notifications-entry').on(
+ 'click.showNotification',
+ function (event) {
+ // hide notification dropdown
+ $('.notifications-wrapper .notifications-icon').trigger('click.dropdown');
- showNotificationDetails($(this));
- event.stopPropagation();
-
- });
+ showNotificationDetails($(this));
+ event.stopPropagation();
+ }
+ );
// Remove corresponding notification from the list and mark it as read
$('.notifications-close').on('click.removeNotification', function (event) {
@@ -83,19 +100,19 @@ define([
removeNotificationFromList(notificationEntry);
// Checking for last unread notification to hide dropdown
- if (notificationCount == 0) {
+ if (notificationCount == 0) {// eslint-disable-line eqeqeq
$('.notifications-wrapper').removeClass('active')
- .find('.notifications-action').removeAttr('data-toggle')
- .off('click.dropdown');
+ .find('.notifications-action')
+ .removeAttr('data-toggle')
+ .off('click.dropdown');
}
event.stopPropagation();
});
// Hide notifications bubble
- if (notificationCount == 0) {
+ if (notificationCount == 0) {// eslint-disable-line eqeqeq
$('.notifications-action .notifications-counter').hide();
} else {
$('.notifications-action .notifications-counter').show();
}
-
-});
\ No newline at end of file
+});
diff --git a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php
index ccbef52b5a818..d35a409b58b98 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php
@@ -1,6 +1,6 @@
'',
ImportAdvancedPricing::COL_TIER_PRICE_QTY => '',
ImportAdvancedPricing::COL_TIER_PRICE => '',
+ ImportAdvancedPricing::COL_TIER_PRICE_TYPE => ''
];
/**
@@ -279,6 +280,8 @@ protected function getExportData()
}
/**
+ * Correct export data.
+ *
* @param array $exportData
* @return array
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
@@ -302,6 +305,12 @@ protected function correctExportData($exportData)
: 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];
}
@@ -311,11 +320,25 @@ protected function correctExportData($exportData)
$customExportData[$key] = $exportRow;
unset($exportRow);
}
+
return $customExportData;
}
/**
- * Get Tier and Group Pricing
+ * Check type for tier price.
+ *
+ * @param string $tierPricePercentage
+ * @return string
+ */
+ private function tierPriceTypeValue($tierPricePercentage)
+ {
+ return $tierPricePercentage
+ ? ImportAdvancedPricing::TIER_PRICE_TYPE_PERCENT
+ : ImportAdvancedPricing::TIER_PRICE_TYPE_FIXED;
+ }
+
+ /**
+ * Get tier prices.
*
* @param array $listSku
* @param string $table
@@ -336,6 +359,7 @@ protected function getTierPrices(array $listSku, $table)
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',
];
if (isset($exportFilter) && !empty($exportFilter)) {
$price = $exportFilter['tier_price'];
@@ -371,6 +395,9 @@ protected function getTierPrices(array $listSku, $table)
if (isset($price[1]) && !empty($price[1])) {
$select->where('ap.value <= ?', $price[1]);
}
+ 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);
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
index d751fdc75882d..0bb44c238c720 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
@@ -1,6 +1,6 @@
'Tier Price data price or quantity value is invalid',
ValidatorInterface::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid',
ValidatorInterface::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group is invalid',
+ ValidatorInterface::ERROR_INVALID_TIER_PRICE_TYPE => 'Value for \'tier_price_value_type\' ' .
+ 'attribute contains incorrect value, acceptable values are Fixed, Discount',
ValidatorInterface::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete',
ValidatorInterface::ERROR_INVALID_ATTRIBUTE_DECIMAL =>
'Value for \'%s\' attribute contains incorrect value, acceptable values are in decimal format',
@@ -70,7 +80,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract
protected $needColumnCheck = true;
/**
- * Valid column names
+ * Valid column names.
*
* @array
*/
@@ -80,6 +90,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract
self::COL_TIER_PRICE_CUSTOMER_GROUP,
self::COL_TIER_PRICE_QTY,
self::COL_TIER_PRICE,
+ self::COL_TIER_PRICE_TYPE
];
/**
@@ -379,7 +390,10 @@ protected function saveAndReplaceAdvancedPrices()
$rowData[self::COL_TIER_PRICE_CUSTOMER_GROUP]
),
'qty' => $rowData[self::COL_TIER_PRICE_QTY],
- 'value' => $rowData[self::COL_TIER_PRICE],
+ 'value' => $rowData[self::COL_TIER_PRICE_TYPE] === self::TIER_PRICE_TYPE_FIXED
+ ? $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])
];
}
@@ -429,7 +443,7 @@ protected function saveProductPrices(array $priceData, $table)
}
}
if ($priceIn) {
- $this->_connection->insertOnDuplicate($tableName, $priceIn, ['value']);
+ $this->_connection->insertOnDuplicate($tableName, $priceIn, ['value', 'percentage_value']);
}
}
return $this;
@@ -542,19 +556,24 @@ protected function retrieveOldSkus()
*/
protected function processCountExistingPrices($prices, $table)
{
+ $oldSkus = $this->retrieveOldSkus();
+ $existProductIds = array_intersect_key($oldSkus, $prices);
+ if (!count($existProductIds)) {
+ return $this;
+ }
+
$tableName = $this->_resourceFactory->create()->getTable($table);
$productEntityLinkField = $this->getProductEntityLinkField();
$existingPrices = $this->_connection->fetchAssoc(
$this->_connection->select()->from(
$tableName,
['value_id', $productEntityLinkField, 'all_groups', 'customer_group_id']
- )
+ )->where($productEntityLinkField . ' IN (?)', $existProductIds)
);
- $oldSkus = $this->retrieveOldSkus();
foreach ($existingPrices as $existingPrice) {
- foreach ($oldSkus as $sku => $productId) {
- if ($existingPrice[$productEntityLinkField] == $productId && isset($prices[$sku])) {
- $this->incrementCounterUpdated($prices[$sku], $existingPrice);
+ foreach ($prices as $sku => $skuPrices) {
+ if (isset($oldSkus[$sku]) && $existingPrice[$productEntityLinkField] == $oldSkus[$sku]) {
+ $this->incrementCounterUpdated($skuPrices, $existingPrice);
}
}
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php
index 051628e7e4a92..ffc191ac655f7 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php
@@ -1,6 +1,6 @@
hasEmptyColumns($value)
) {
$this->_addMessages([self::ERROR_TIER_DATA_INCOMPLETE]);
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php
new file mode 100644
index 0000000000000..d00d36b0eda27
--- /dev/null
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php
@@ -0,0 +1,48 @@
+_addMessages([RowValidatorInterface::ERROR_INVALID_TIER_PRICE_TYPE]);
+ $isValid = false;
+ }
+
+ return $isValid;
+ }
+}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php
index 150555709b8b7..bc57b8482963a 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php
@@ -1,6 +1,6 @@
tierPriceType = $objectManager->getObject(
+ AdvancedPricing\Validator\TierPriceType::class,
+ []
+ );
+ }
+
+ /**
+ * Test for isValid() method.
+ *
+ * @dataProvider isValidDataProvider
+ * @param array $value
+ * @param bool $expectedResult
+ */
+ public function testIsValid(array $value, $expectedResult)
+ {
+ $result = $this->tierPriceType->isValid($value);
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ /**
+ * Data Provider for testIsValid().
+ *
+ * @return array
+ */
+ public function isValidDataProvider()
+ {
+ return [
+ [
+ [AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_FIXED],
+ true
+ ],
+ [
+ [AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_PERCENT],
+ true
+ ],
+ [
+ [],
+ true
+ ],
+ [
+ [AdvancedPricing::COL_TIER_PRICE_TYPE => null],
+ true
+ ],
+ [
+ [AdvancedPricing::COL_TIER_PRICE_TYPE => 'wrong type'],
+ false
+ ]
+ ];
+ }
+}
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 ad67ee5cc556a..6a0fd28ac7466 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
@@ -1,6 +1,6 @@
advancedPricing
+ $skuProduct = 'product1';
+ $sku = $data[0][AdvancedPricing::COL_SKU];
+ $advancedPricing = $this->getAdvancedPricingMock(
+ [
+ 'retrieveOldSkus',
+ 'validateRow',
+ 'addRowError',
+ 'getCustomerGroupId',
+ 'getWebSiteId',
+ 'deleteProductTierPrices',
+ 'getBehavior',
+ 'saveAndReplaceAdvancedPrices',
+ 'processCountExistingPrices',
+ 'processCountNewPrices'
+ ]
+ );
+ $advancedPricing
->expects($this->any())
->method('getBehavior')
->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND);
$this->dataSourceModel->expects($this->at(0))->method('getNextBunch')->willReturn($data);
- $this->advancedPricing->expects($this->any())->method('validateRow')->willReturn(true);
+ $advancedPricing->expects($this->any())->method('validateRow')->willReturn(true);
- $this->advancedPricing->expects($this->any())->method('getCustomerGroupId')->willReturnMap(
+ $advancedPricing->expects($this->any())->method('getCustomerGroupId')->willReturnMap(
[
[$data[0][AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP], $tierCustomerGroupId],
]
);
- $this->advancedPricing->expects($this->any())->method('getWebSiteId')->willReturnMap(
+ $advancedPricing->expects($this->any())->method('getWebSiteId')->willReturnMap(
[
[$data[0][AdvancedPricing::COL_TIER_PRICE_WEBSITE], $tierWebsiteId],
]
);
- $this->advancedPricing->expects($this->any())->method('saveProductPrices')->will($this->returnSelf());
+ $oldSkus = [$sku => $skuProduct];
+ $expectedTierPrices[$sku][0][self::LINK_FIELD] = $skuProduct;
+ $advancedPricing->expects($this->once())->method('retrieveOldSkus')->willReturn($oldSkus);
+ $this->connection->expects($this->once())
+ ->method('insertOnDuplicate')
+ ->with(self::TABLE_NAME, $expectedTierPrices[$sku], ['value', 'percentage_value']);
- $this->advancedPricing->expects($this->any())->method('processCountExistingPrices')->willReturnSelf();
- $this->advancedPricing->expects($this->any())->method('processCountNewPrices')->willReturnSelf();
+ $advancedPricing->expects($this->any())->method('processCountExistingPrices')->willReturnSelf();
+ $advancedPricing->expects($this->any())->method('processCountNewPrices')->willReturnSelf();
- $result = $this->invokeMethod($this->advancedPricing, 'saveAndReplaceAdvancedPrices');
+ $result = $this->invokeMethod($advancedPricing, 'saveAndReplaceAdvancedPrices');
- $this->assertEquals($this->advancedPricing, $result);
+ $this->assertEquals($advancedPricing, $result);
+ }
+
+ /**
+ * Test method saveAndReplaceAdvancedPrices with append import behaviour.
+ */
+ public function testSaveAndReplaceAdvancedPricesAppendBehaviourDataAndCallsWithoutTierPrice()
+ {
+ $data = [
+ 0 => [
+ AdvancedPricing::COL_SKU => 'sku value',
+ AdvancedPricing::COL_TIER_PRICE_WEBSITE => null,
+ AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'tier price customer group value - not all groups',
+ AdvancedPricing::COL_TIER_PRICE_QTY => 'tier price qty value',
+ AdvancedPricing::COL_TIER_PRICE => 'tier price value',
+ AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_FIXED
+ ],
+ ];
+ $tierCustomerGroupId = 'tier customer group id value';
+ $tierWebsiteId = 'tier website id value';
+ $expectedTierPrices = [];
+
+ $skuProduct = 'product1';
+ $sku = $data[0][AdvancedPricing::COL_SKU];
+ $advancedPricing = $this->getAdvancedPricingMock(
+ [
+ 'retrieveOldSkus',
+ 'validateRow',
+ 'addRowError',
+ 'getCustomerGroupId',
+ 'getWebSiteId',
+ 'deleteProductTierPrices',
+ 'getBehavior',
+ 'saveAndReplaceAdvancedPrices',
+ 'processCountExistingPrices',
+ 'processCountNewPrices'
+ ]
+ );
+ $advancedPricing
+ ->expects($this->any())
+ ->method('getBehavior')
+ ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND);
+ $this->dataSourceModel->expects($this->at(0))->method('getNextBunch')->willReturn($data);
+ $advancedPricing->expects($this->any())->method('validateRow')->willReturn(true);
+
+ $advancedPricing->expects($this->any())->method('getCustomerGroupId')->willReturnMap(
+ [
+ [$data[0][AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP], $tierCustomerGroupId],
+ ]
+ );
+
+ $advancedPricing->expects($this->any())->method('getWebSiteId')->willReturnMap(
+ [
+ [$data[0][AdvancedPricing::COL_TIER_PRICE_WEBSITE], $tierWebsiteId],
+ ]
+ );
+
+ $oldSkus = [$sku => $skuProduct];
+ $expectedTierPrices[$sku][0][self::LINK_FIELD] = $skuProduct;
+ $advancedPricing->expects($this->never())->method('retrieveOldSkus')->willReturn($oldSkus);
+ $this->connection->expects($this->never())
+ ->method('insertOnDuplicate')
+ ->with(self::TABLE_NAME, $expectedTierPrices[$sku], ['value', 'percentage_value']);
+
+ $advancedPricing->expects($this->any())->method('processCountExistingPrices')->willReturnSelf();
+ $advancedPricing->expects($this->any())->method('processCountNewPrices')->willReturnSelf();
+
+ $result = $this->invokeMethod($advancedPricing, 'saveAndReplaceAdvancedPrices');
+
+ $this->assertEquals($advancedPricing, $result);
}
/**
@@ -575,6 +665,7 @@ public function saveAndReplaceAdvancedPricesAppendBehaviourDataProvider()
AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'tier price customer group value - not all groups ',
AdvancedPricing::COL_TIER_PRICE_QTY => 'tier price qty value',
AdvancedPricing::COL_TIER_PRICE => 'tier price value',
+ AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_FIXED
],
],
'$tierCustomerGroupId' => 'tier customer group id value',
@@ -585,25 +676,25 @@ public function saveAndReplaceAdvancedPricesAppendBehaviourDataProvider()
'sku value' => [
[
'all_groups' => false,
- //$rowData[self::COL_TIER_PRICE_CUSTOMER_GROUP] == self::VALUE_ALL_GROUPS
'customer_group_id' => 'tier customer group id value',
- //$tierCustomerGroupId
'qty' => 'tier price qty value',
'value' => 'tier price value',
'website_id' => 'tier website id value',
+ 'percentage_value' => null
],
],
],
],
- [// tier customer group is equal to all group
+ [
'$data' => [
0 => [
AdvancedPricing::COL_SKU => 'sku value',
//tier
AdvancedPricing::COL_TIER_PRICE_WEBSITE => 'tier price website value',
- AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => AdvancedPricing::VALUE_ALL_GROUPS,
+ AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'tier price customer group value - not all groups ',
AdvancedPricing::COL_TIER_PRICE_QTY => 'tier price qty value',
AdvancedPricing::COL_TIER_PRICE => 'tier price value',
+ AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_PERCENT
],
],
'$tierCustomerGroupId' => 'tier customer group id value',
@@ -613,33 +704,44 @@ public function saveAndReplaceAdvancedPricesAppendBehaviourDataProvider()
'$expectedTierPrices' => [
'sku value' => [
[
- 'all_groups' => true,
- //$rowData[self::COL_TIER_PRICE_CUSTOMER_GROUP] == self::VALUE_ALL_GROUPS
+ 'all_groups' => false,
'customer_group_id' => 'tier customer group id value',
- //$tierCustomerGroupId
'qty' => 'tier price qty value',
- 'value' => 'tier price value',
+ 'value' => 0,
+ 'percentage_value' => 'tier price value',
'website_id' => 'tier website id value',
],
],
],
],
- [
+ [// tier customer group is equal to all group
'$data' => [
0 => [
AdvancedPricing::COL_SKU => 'sku value',
//tier
- AdvancedPricing::COL_TIER_PRICE_WEBSITE => null,
- AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'tier price customer group value - not all groups',
+ AdvancedPricing::COL_TIER_PRICE_WEBSITE => 'tier price website value',
+ AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => AdvancedPricing::VALUE_ALL_GROUPS,
AdvancedPricing::COL_TIER_PRICE_QTY => 'tier price qty value',
AdvancedPricing::COL_TIER_PRICE => 'tier price value',
+ AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_FIXED
],
],
'$tierCustomerGroupId' => 'tier customer group id value',
'$groupCustomerGroupId' => 'group customer group id value',
'$tierWebsiteId' => 'tier website id value',
'$groupWebsiteId' => 'group website id value',
- '$expectedTierPrices' => [],
+ '$expectedTierPrices' => [
+ 'sku value' => [
+ [
+ 'all_groups' => true,
+ 'customer_group_id' => 'tier customer group id value',
+ 'qty' => 'tier price qty value',
+ 'value' => 'tier price value',
+ 'website_id' => 'tier website id value',
+ 'percentage_value' => null
+ ],
+ ],
+ ],
],
[
'$data' => [
@@ -650,6 +752,7 @@ public function saveAndReplaceAdvancedPricesAppendBehaviourDataProvider()
AdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'tier price customer group value - not all groups',
AdvancedPricing::COL_TIER_PRICE_QTY => 'tier price qty value',
AdvancedPricing::COL_TIER_PRICE => 'tier price value',
+ AdvancedPricing::COL_TIER_PRICE_TYPE => AdvancedPricing::TIER_PRICE_TYPE_FIXED
],
],
'$tierCustomerGroupId' => 'tier customer group id value',
@@ -660,12 +763,11 @@ public function saveAndReplaceAdvancedPricesAppendBehaviourDataProvider()
'sku value' => [
[
'all_groups' => false,
- //$rowData[self::COL_TIER_PRICE_CUSTOMER_GROUP] == self::VALUE_ALL_GROUPS
'customer_group_id' => 'tier customer group id value',
- //$tierCustomerGroupId
'qty' => 'tier price qty value',
'value' => 'tier price value',
'website_id' => 'tier website id value',
+ 'percentage_value' => null
],
]
],
@@ -746,7 +848,7 @@ public function testSaveProductPrices($priceData, $oldSkus, $priceIn, $callNum)
$this->connection->expects($this->exactly($callNum))
->method('insertOnDuplicate')
- ->with(self::TABLE_NAME, $priceIn, ['value']);
+ ->with(self::TABLE_NAME, $priceIn, ['value', 'percentage_value']);
$this->invokeMethod($this->advancedPricing, 'saveProductPrices', [$priceData, 'table']);
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php
index d65271d9ace54..67f6d33a9df15 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php
@@ -1,6 +1,6 @@
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/AdvancedPricingImportExport/etc/di.xml b/app/code/Magento/AdvancedPricingImportExport/etc/di.xml
index c3444069c14c0..950054ab52412 100644
--- a/app/code/Magento/AdvancedPricingImportExport/etc/di.xml
+++ b/app/code/Magento/AdvancedPricingImportExport/etc/di.xml
@@ -1,7 +1,7 @@
@@ -14,6 +14,7 @@
- Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\TierPrice
- Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\Website
+ - Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\TierPriceType
diff --git a/app/code/Magento/AdvancedPricingImportExport/etc/export.xml b/app/code/Magento/AdvancedPricingImportExport/etc/export.xml
index f04e399fdfedb..da176e7bd73e1 100644
--- a/app/code/Magento/AdvancedPricingImportExport/etc/export.xml
+++ b/app/code/Magento/AdvancedPricingImportExport/etc/export.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdvancedPricingImportExport/etc/import.xml b/app/code/Magento/AdvancedPricingImportExport/etc/import.xml
index 711a7bf270ce6..80c8873aad387 100644
--- a/app/code/Magento/AdvancedPricingImportExport/etc/import.xml
+++ b/app/code/Magento/AdvancedPricingImportExport/etc/import.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdvancedPricingImportExport/etc/module.xml b/app/code/Magento/AdvancedPricingImportExport/etc/module.xml
index 4ae0d6516e66f..f9ad9f34b2ad6 100644
--- a/app/code/Magento/AdvancedPricingImportExport/etc/module.xml
+++ b/app/code/Magento/AdvancedPricingImportExport/etc/module.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/AdvancedPricingImportExport/registration.php b/app/code/Magento/AdvancedPricingImportExport/registration.php
index 86dcff3d60a68..a9477f3a9a1fb 100644
--- a/app/code/Magento/AdvancedPricingImportExport/registration.php
+++ b/app/code/Magento/AdvancedPricingImportExport/registration.php
@@ -1,6 +1,6 @@
subscriptionStatusProvider = $labelStatusProvider;
+ }
+
+ /**
+ * Unset some non-related element parameters
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue();
+ $element->setData(
+ 'value',
+ $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/Controller/Adminhtml/BasicTier/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BasicTier/SignUp.php
new file mode 100644
index 0000000000000..73c39d7c14a59
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/BasicTier/SignUp.php
@@ -0,0 +1,62 @@
+config = $config;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::report_basic_tier');
+ }
+
+ /**
+ * Provides link to Basic Tier signup
+ *
+ * @return \Magento\Framework\Controller\AbstractResult
+ */
+ public function execute()
+ {
+ return $this->resultRedirectFactory->create()->setUrl(
+ $this->config->getConfigDataValue($this->basicTierUrlPath)
+ );
+ }
+}
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..ac9d93a579ffd
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
@@ -0,0 +1,71 @@
+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 (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/Activate.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Activate.php
new file mode 100644
index 0000000000000..56cc44bbc95dd
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Activate.php
@@ -0,0 +1,118 @@
+subscription = $subscription;
+ $this->logger = $logger;
+ $this->notificationTime = $notificationTime;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Activate subscription to Magento Analytics via AJAX.
+ *
+ * @return Json
+ */
+ public function execute()
+ {
+ try {
+ if ($this->getRequest()->getParam($this->subscriptionApprovedField)) {
+ $this->subscription->enable();
+ } else {
+ $this->notificationTime->unsetLastTimeNotificationValue();
+ }
+ $responseContent = [
+ 'success' => true,
+ 'error_message' => '',
+ ];
+ } catch (LocalizedException $e) {
+ $responseContent = [
+ 'success' => false,
+ 'error_message' => $e->getMessage(),
+ ];
+ $this->logger->error($e->getMessage());
+ } catch (\Exception $e) {
+ $responseContent = [
+ 'success' => false,
+ 'error_message' => __(
+ 'Sorry, there was an error processing your registration request to Magento Analytics. '
+ . 'Please try again later.'
+ ),
+ ];
+ $this->logger->error($e->getMessage());
+ }
+ /** @var Json $resultJson */
+ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON);
+ return $resultJson->setData($responseContent);
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Postpone.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Postpone.php
new file mode 100644
index 0000000000000..b87730132e3d4
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Postpone.php
@@ -0,0 +1,101 @@
+dateTimeFactory = $dateTimeFactory;
+ $this->notificationTime = $notificationTime;
+ $this->logger = $logger;
+ parent::__construct($context);
+
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Postpones notification about subscription
+ *
+ * @return Json
+ */
+ public function execute()
+ {
+ try {
+ $dateTime = $this->dateTimeFactory->create();
+ $responseContent = [
+ 'success' => $this->notificationTime->storeLastTimeNotification($dateTime->getTimestamp()),
+ 'error_message' => ''
+ ];
+ } catch (LocalizedException $e) {
+ $this->logger->error($e->getMessage());
+ $responseContent = [
+ 'success' => false,
+ 'error_message' => $e->getMessage()
+ ];
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage());
+ $responseContent = [
+ 'success' => false,
+ 'error_message' => __('Error occurred during postponement notification')
+ ];
+ }
+ /** @var Json $resultJson */
+ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON);
+ return $resultJson->setData($responseContent);
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/CollectData.php b/app/code/Magento/Analytics/Cron/CollectData.php
new file mode 100644
index 0000000000000..5b2abd2fbe6a9
--- /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..b8d79ae2707b3
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/SignUp.php
@@ -0,0 +1,131 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->inboxFactory = $inboxFactory;
+ $this->inboxResource = $inboxResource;
+ $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) {
+ $this->deleteAnalyticsCronExpr();
+ return false;
+ }
+
+ if ($attemptsCount <= 0) {
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $inboxNotification = $this->inboxFactory->create();
+ $inboxNotification->addNotice(
+ "Analytics subscription unsuccessful",
+ "Analytics subscription unsuccessful"
+ );
+ $this->inboxResource->save($inboxNotification);
+ 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..4ed1346ade9a5
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/Update.php
@@ -0,0 +1,78 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Execute scheduled update operation
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $updateResult = $this->connector->execute('update');
+ if ($updateResult === false) {
+ return false;
+ }
+ $this->configWriter->delete(BaseUrlConfigPlugin::UPDATE_CRON_STRING_PATH);
+ $this->flagManager->deleteFlag(BaseUrlConfigPlugin::OLD_BASE_URL_FLAG_CODE);
+ $this->reinitableConfig->reinit();
+ return true;
+ }
+}
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..abef37f3a62d3
--- /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/Condition/CanViewNotification.php b/app/code/Magento/Analytics/Model/Condition/CanViewNotification.php
new file mode 100644
index 0000000000000..a7dc93bcab800
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Condition/CanViewNotification.php
@@ -0,0 +1,68 @@
+notificationTime = $notificationTime;
+ $this->dateTimeFactory = $dateTimeFactory;
+ }
+
+ /**
+ * Validate is notification popup can be shown
+ *
+ * @return bool
+ */
+ public function validate()
+ {
+ $lastNotificationTime = $this->notificationTime->getLastTimeNotification();
+ if (!$lastNotificationTime) {
+ return false;
+ }
+ $datetime = $this->dateTimeFactory->create();
+ return (
+ $datetime->getTimestamp() >= $lastNotificationTime + $this->notificationInterval
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config.php b/app/code/Magento/Analytics/Model/Config.php
new file mode 100644
index 0000000000000..a4601ec4430c4
--- /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/CollectionTime.php b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
new file mode 100644
index 0000000000000..208f3a5dad7c8
--- /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..8cbb64fb26337
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
@@ -0,0 +1,71 @@
+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 {
+ $this->subscriptionHandler->process($this);
+ } 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..0f169a780dcb6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
@@ -0,0 +1,174 @@
+configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ $this->notificationTime = $notificationTime;
+ }
+
+ /**
+ * Performs change subscription environment on config value change.
+ *
+ * Activate process of subscription handling
+ * if subscription was activated and Analytics token has not been received
+ * or interrupt subscription handling.
+ *
+ * @param Value $configValue
+ *
+ * @return bool
+ */
+ public function process(Value $configValue)
+ {
+ if (!$configValue->isValueChanged()) {
+ return true;
+ }
+
+ $enabled = $configValue->getData('value');
+
+ if (!$enabled) {
+ $this->disableCollectionData();
+ }
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ if ($enabled) {
+ $this->setCronSchedule();
+ $this->setAttemptsFlag();
+ $this->notificationTime->unsetLastTimeNotificationValue();
+ } else {
+ $this->unsetAttemptsFlag();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Set cron schedule setting into config for activation of subscription process.
+ *
+ * @return bool
+ */
+ private function setCronSchedule()
+ {
+ $cronExprArray = [
+ '0', # Minute
+ '*', # Hour
+ '*', # Day of the Month
+ '*', # Month of the Year
+ '*', # Day of the Week
+ ];
+
+ $cronExprString = join(' ', $cronExprArray);
+
+ $this->configWriter->save(self::CRON_STRING_PATH, $cronExprString);
+
+ 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);
+ }
+
+ /**
+ * 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..c9759855e843f
--- /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/Data.php b/app/code/Magento/Analytics/Model/Config/Data.php
new file mode 100644
index 0000000000000..addc305b71a9e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Data.php
@@ -0,0 +1,29 @@
+ [
+ * '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..e76cc57ee37d8
--- /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/Reader/Xml.php b/app/code/Magento/Analytics/Model/Config/Reader/Xml.php
new file mode 100644
index 0000000000000..4a333f00324f9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Reader/Xml.php
@@ -0,0 +1,61 @@
+ 'name',
+ ];
+
+ /**
+ * @param FileResolverInterface $fileResolver
+ * @param ConverterInterface $converter
+ * @param SchemaLocatorInterface $schemaLocator
+ * @param ValidationStateInterface $validationState
+ * @param string $fileName
+ * @param array $idAttributes
+ * @param string $domDocumentClass
+ * @param string $defaultScope
+ */
+ public function __construct(
+ FileResolverInterface $fileResolver,
+ ConverterInterface $converter,
+ SchemaLocatorInterface $schemaLocator,
+ ValidationStateInterface $validationState,
+ $fileName = 'analytics.xml',
+ $idAttributes = [],
+ $domDocumentClass = Dom::class,
+ $defaultScope = 'global'
+ ) {
+ parent::__construct(
+ $fileResolver,
+ $converter,
+ $schemaLocator,
+ $validationState,
+ $fileName,
+ $idAttributes,
+ $domDocumentClass,
+ $defaultScope
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/SchemaLocator.php b/app/code/Magento/Analytics/Model/Config/SchemaLocator.php
new file mode 100644
index 0000000000000..1785174f4da65
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/SchemaLocator.php
@@ -0,0 +1,56 @@
+urnResolver = $urnResolver;
+ }
+
+ /**
+ * Get path to merged config schema.
+ *
+ * @return string|null
+ */
+ public function getSchema()
+ {
+ return $this->urnResolver->getRealPath($this->schema);
+ }
+
+ /**
+ * Get path to pre file validation schema.
+ *
+ * @return string|null
+ */
+ public function getPerFileSchema()
+ {
+ return null;
+ }
+}
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..6bc00465b0607
--- /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..be719e5640f1a
--- /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..8ac1a5fce4371
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
@@ -0,0 +1,20 @@
+curlFactory = $curlFactory;
+ $this->responseFactory = $responseFactory;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request($method, $url, $body = '', array $headers = [], $version = '1.1')
+ {
+ $curl = $this->curlFactory->create();
+
+ $curl->write($method, $url, $version, $headers, $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 false;
+ }
+
+ return $this->responseFactory->create($result);
+ }
+}
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..37eb6865544a0
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
@@ -0,0 +1,29 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $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()
+ {
+ $otp = false;
+
+ if ($this->analyticsToken->isTokenExist()) {
+ try {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->otpUrlConfigPath),
+ $this->getRequestJson(),
+ ['Content-Type: application/json']
+ );
+
+ if ($response) {
+ $otp = $this->extractOtp($response);
+
+ if (!$otp) {
+ $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.'
+ )
+ );
+ }
+ }
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+ }
+
+ return $otp;
+ }
+
+ /**
+ * Prepares request data in JSON format.
+ *
+ * @return string
+ */
+ private function getRequestJson()
+ {
+ return json_encode(
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+ }
+
+ /**
+ * Extracts an OTP from the response.
+ *
+ * Returns the OTP or FALSE if the OTP is not found.
+ *
+ * @param HttpResponse $response
+ * @return string|false
+ */
+ private function extractOtp(HttpResponse $response)
+ {
+ $otp = false;
+
+ if ($response->getStatus() === 201) {
+ $body = json_decode($response->getBody(), 1);
+
+ $otp = !empty($body['otp']) ? $body['otp'] : false;
+ }
+
+ return $otp;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/SignUpCommand.php b/app/code/Magento/Analytics/Model/Connector/SignUpCommand.php
new file mode 100644
index 0000000000000..f9fd78420be46
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/SignUpCommand.php
@@ -0,0 +1,74 @@
+analyticsToken = $analyticsToken;
+ $this->integrationManager = $integrationManager;
+ $this->signUpRequest = $signUpRequest;
+ }
+
+ /**
+ * 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()
+ {
+ $integrationToken = $this->integrationManager->generateToken();
+ if ($integrationToken) {
+ $this->integrationManager->activateIntegration();
+ $responseToken = $this->signUpRequest->call($integrationToken->getToken());
+ if ($responseToken) {
+ $this->analyticsToken->storeToken($responseToken);
+ }
+ }
+ return ((bool)$integrationToken && isset($responseToken) && (bool)$responseToken);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/SignUpRequest.php b/app/code/Magento/Analytics/Model/Connector/SignUpRequest.php
new file mode 100644
index 0000000000000..74bfa84d0ef62
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/SignUpRequest.php
@@ -0,0 +1,135 @@
+config = $config;
+ $this->httpClient = $httpClient;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Prepares request data in JSON format.
+ *
+ * @param string $integrationToken
+ * @return string
+ */
+ private function getRequestJson($integrationToken)
+ {
+ return json_encode(
+ [
+ "token" => $integrationToken,
+ "url" => $this->config->getConfigDataValue(
+ Store::XML_PATH_SECURE_BASE_URL
+ )
+ ]
+ );
+ }
+
+ /**
+ * Extracts an MBI access token from the response.
+ *
+ * Returns the token or FALSE if the token is not found.
+ *
+ * @param HttpResponse $response
+ * @return string|false
+ */
+ private function extractAccessToken(HttpResponse $response)
+ {
+ $token = false;
+
+ if ($response->getStatus() === 201) {
+ $body = json_decode($response->getBody(), 1);
+
+ if (isset($body['access-token']) && !empty($body['access-token'])) {
+ $token = $body['access-token'];
+ }
+ }
+
+ return $token;
+ }
+
+ /**
+ * Performs a 'signUp' call to MBI service.
+ *
+ * Returns MBI access token or FALSE in case of failure.
+ *
+ * @param string $integrationToken
+ * @return string|false
+ */
+ public function call($integrationToken)
+ {
+ $token = false;
+
+ try {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getConfigDataValue($this->signUpUrlPath),
+ $this->getRequestJson($integrationToken),
+ ['Content-Type: application/json']
+ );
+
+ if ($response) {
+ $token = $this->extractAccessToken($response);
+
+ if (!$token) {
+ $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.'
+ )
+ );
+ }
+ }
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+
+ return $token;
+ }
+}
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..ba9e779122f55
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
@@ -0,0 +1,124 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Executes update request to MBI api in case store url was changed
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ try {
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::PUT,
+ $this->config->getConfigDataValue($this->updateUrlPath),
+ $this->getRequestJson(),
+ ['Content-Type: application/json']
+ );
+
+ if ($response) {
+ $result = $response->getStatus() === 201;
+ 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.'
+ )
+ );
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Prepares request data in JSON format.
+ * @return string
+ */
+ private function getRequestJson()
+ {
+ return json_encode(
+ [
+ "url" => $this->flagManager->getFlagData(BaseUrlConfigPlugin::OLD_BASE_URL_FLAG_CODE),
+ "new-url" => $this->config->getConfigDataValue(
+ Store::XML_PATH_SECURE_BASE_URL
+ ),
+ "access-token" => $this->analyticsToken->getToken(),
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php
new file mode 100644
index 0000000000000..b3b691399994d
--- /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..358765ea79347
--- /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/ExportDataHandler.php b/app/code/Magento/Analytics/Model/ExportDataHandler.php
new file mode 100644
index 0000000000000..3f451e8a07872
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ExportDataHandler.php
@@ -0,0 +1,205 @@
+filesystem = $filesystem;
+ $this->archive = $archive;
+ $this->reportWriter = $reportWriter;
+ $this->cryptographer = $cryptographer;
+ $this->fileRecorder = $fileRecorder;
+ }
+
+ /**
+ * Execute collecting new data for MBI.
+ *
+ * @return bool
+ */
+ 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/FileInfo.php b/app/code/Magento/Analytics/Model/FileInfo.php
new file mode 100644
index 0000000000000..b7c38709944d3
--- /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..a55df9070a0d0
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfoManager.php
@@ -0,0 +1,122 @@
+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..c1ef8da247147
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileRecorder.php
@@ -0,0 +1,137 @@
+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/FlagManager.php b/app/code/Magento/Analytics/Model/FlagManager.php
new file mode 100644
index 0000000000000..47c8f0a0da2dd
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FlagManager.php
@@ -0,0 +1,93 @@
+flagFactory = $flagFactory;
+ $this->flagResource = $flagResource;
+ }
+
+ /**
+ * Return raw data from flag
+ * @param string $flagCode
+ * @return mixed
+ */
+ public function getFlagData($flagCode)
+ {
+ return $this->getFlagObject($flagCode)->getFlagData();
+ }
+
+ /**
+ * Save flag by code
+ * @param string $flagCode
+ * @param mixed $value
+ * @return bool
+ */
+ public function saveFlag($flagCode, $value)
+ {
+ $flag = $this->getFlagObject($flagCode);
+ $flag->setFlagData($value);
+ $this->flagResource->save($flag);
+ return true;
+ }
+
+ /**
+ * Delete flag by code
+ *
+ * @param string $flagCode
+ * @return bool
+ */
+ public function deleteFlag($flagCode)
+ {
+ $flag = $this->getFlagObject($flagCode);
+ if ($flag->getId()) {
+ $this->flagResource->delete($flag);
+ }
+ return true;
+ }
+
+ /**
+ * Returns flag object
+ *
+ * @param string $flagCode
+ * @return Flag
+ */
+ private function getFlagObject($flagCode)
+ {
+ /** @var Flag $flag */
+ $flag = $this->flagFactory
+ ->create(['data' => ['flag_code' => $flagCode]]);
+ $this->flagResource->load($flag, $flagCode, 'flag_code');
+ return $flag;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/IntegrationManager.php b/app/code/Magento/Analytics/Model/IntegrationManager.php
new file mode 100644
index 0000000000000..bf625f03dc69a
--- /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|string
+ */
+ 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..cbbbca93d6c47
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Link.php
@@ -0,0 +1,60 @@
+url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+
+ /**
+ * @param string $url
+ * @return void
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ }
+
+ /**
+ * @param string $initializationVector
+ * @return void
+ */
+ public function setInitializationVector($initializationVector)
+ {
+ $this->initializationVector = $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..2d4e7dad04073
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/LinkProvider.php
@@ -0,0 +1,67 @@
+linkInterfaceFactory = $linkInterfaceFactory;
+ $this->fileInfoManager = $fileInfoManager;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get()
+ {
+ $fileInfo = $this->fileInfoManager->load();
+ if ($fileInfo->getPath() === null || $fileInfo->getInitializationVector() === null) {
+ throw new Exception(__('File is not ready yet.'), 0, Exception::HTTP_NOT_FOUND);
+ }
+ $link = $this->linkInterfaceFactory->create();
+ $link->setUrl(
+ $this->storeManager->getStore()->getBaseUrl(
+ UrlInterface::URL_TYPE_MEDIA
+ ) . $fileInfo->getPath()
+ );
+ $link->setInitializationVector(base64_encode($fileInfo->getInitializationVector()));
+ return $link;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/NotificationTime.php b/app/code/Magento/Analytics/Model/NotificationTime.php
new file mode 100644
index 0000000000000..4df4cc970ee6d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/NotificationTime.php
@@ -0,0 +1,65 @@
+flagManager = $flagManager;
+ }
+
+ /**
+ * Stores last notification time
+ *
+ * @param string $value
+ * @return bool
+ */
+ public function storeLastTimeNotification($value)
+ {
+ return $this->flagManager->saveFlag(self::NOTIFICATION_TIME, $value);
+ }
+
+ /**
+ * Returns last time when merchant was notified about Analytic services
+ *
+ * @return int
+ */
+ public function getLastTimeNotification()
+ {
+ return $this->flagManager->getFlagData(self::NOTIFICATION_TIME);
+ }
+
+ /**
+ * Remove last notification time flag.
+ *
+ * @return bool
+ */
+ public function unsetLastTimeNotificationValue()
+ {
+ return $this->flagManager->deleteFlag(self::NOTIFICATION_TIME);
+ }
+}
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..e4d4441edbbf4
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
@@ -0,0 +1,101 @@
+flagManager = $flagManager;
+ $this->subscriptionStatusProvider = $subscriptionStatusProvider;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Sets update analytics cron job if base url was changed.
+ *
+ * @param \Magento\Config\Model\Config\Backend\Baseurl $subject
+ * @param \Magento\Config\Model\Config\Backend\Baseurl $result
+ * @return \Magento\Config\Model\Config\Backend\Baseurl
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterAfterSave(
+ \Magento\Config\Model\Config\Backend\Baseurl $subject,
+ \Magento\Config\Model\Config\Backend\Baseurl $result
+ ) {
+ if (!$result->isValueChanged()) {
+ return $result;
+ }
+
+ if ($this->isPluginApplicable($result)) {
+ $this->flagManager->saveFlag(static::OLD_BASE_URL_FLAG_CODE, $result->getOldValue());
+ $this->configWriter->save(self::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param \Magento\Config\Model\Config\Backend\Baseurl $result
+ *
+ * @return bool
+ */
+ private function isPluginApplicable(\Magento\Config\Model\Config\Backend\Baseurl $result)
+ {
+ return $result->getData('path') === \Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL
+ && $this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::ENABLED;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ProviderFactory.php b/app/code/Magento/Analytics/Model/ProviderFactory.php
new file mode 100644
index 0000000000000..b146c79ccb0b7
--- /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..e408106796dc4
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
@@ -0,0 +1,75 @@
+analyticsToken = $analyticsToken;
+ $this->otpRequest = $otpRequest;
+ $this->config = $config;
+ }
+
+ /**
+ * Provide URL on resource with reports.
+ *
+ * @return string
+ */
+ public function getUrl()
+ {
+ $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..80b1210f9677a
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriter.php
@@ -0,0 +1,102 @@
+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..20a8e5db4ffb8
--- /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..314225dc3d346
--- /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/Subscription.php b/app/code/Magento/Analytics/Model/Subscription.php
new file mode 100644
index 0000000000000..e59fbcb1c3d70
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Subscription.php
@@ -0,0 +1,108 @@
+configValueFactory = $configValueFactory;
+ $this->configStructure = $configStructure;
+ $this->configValueResource = $configValueResource;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Set subscription enabled config value.
+ *
+ * @return boolean
+ */
+ public function enable()
+ {
+ /** @var Field $field */
+ $field = $this->configStructure->getElement($this->enabledConfigStructurePath);
+ /** @var Value $configValue */
+ $configValue = $field->hasBackendModel()
+ ? $field->getBackendModel()
+ : $this->configValueFactory->create();
+ $configPath = $field->getConfigPath() ?: $this->enabledConfigStructurePath;
+
+ $this->configValueResource
+ ->load($configValue, $configPath, 'path');
+
+ $configValue->setValue($this->yesValueDropdown);
+ $configValue->setPath($configPath);
+
+ $this->configValueResource
+ ->save($configValue);
+
+ $this->reinitableConfig->reinit();
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
new file mode 100644
index 0000000000000..80dd2f7cc1bee
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
@@ -0,0 +1,109 @@
+systemConfig = $systemConfig;
+ $this->analyticsToken = $analyticsToken;
+ }
+
+ /**
+ * 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.
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ $checkboxState = $this->systemConfig->get('default/analytics/subscription/enabled');
+ return $this->resolveStatus($checkboxState);
+ }
+
+ /**
+ * Resolves subscription status depending on
+ * subscription config value (enabled, disabled).
+ *
+ * @param bool $isSubscriptionEnabled
+ *
+ * @return string
+ */
+ private function resolveStatus($isSubscriptionEnabled)
+ {
+ if (!$isSubscriptionEnabled) {
+ return static::DISABLED;
+ }
+
+ $status = static::PENDING;
+
+ if ($this->analyticsToken->isTokenExist()) {
+ $status = static::ENABLED;
+ }
+
+ return $status;
+ }
+
+ /**
+ * Retrieve status for subscription that enabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForEnabledSubscription()
+ {
+ return $this->resolveStatus(true);
+ }
+
+ /**
+ * Retrieve status for subscription that disabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForDisabledSubscription()
+ {
+ return $this->resolveStatus(false);
+ }
+}
diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md
new file mode 100644
index 0000000000000..cb8fdc3287190
--- /dev/null
+++ b/app/code/Magento/Analytics/README.md
@@ -0,0 +1,12 @@
+## Overview
+
+The Magento_Analytics module provides integration with
+[Magento Business Intelligence](https://magento.com/products/business-intelligence). Admin user can subscribe to
+Advanced Analytics, navigate to Advanced Analytics reports and Magento Business Intelligence subscription page.
+
+The Magento_Analytics module adds:
+
+- Advanced Analytics subscription pop-up
+- backend menu link to Magento Business Intelligence subscription page under Reports menu
+- Magento Analytics configuration page in Stores-Configuration->General menu
+- configuration to build a data collection for BI system
\ No newline at end of file
diff --git a/app/code/Magento/Analytics/ReportXml/Config.php b/app/code/Magento/Analytics/ReportXml/Config.php
new file mode 100644
index 0000000000000..53d4cee09191d
--- /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..c0152ed262c52
--- /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/Data.php b/app/code/Magento/Analytics/ReportXml/Config/Data.php
new file mode 100644
index 0000000000000..1d3f69dc30373
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Data.php
@@ -0,0 +1,29 @@
+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/Config/Reader/Xml.php b/app/code/Magento/Analytics/ReportXml/Config/Reader/Xml.php
new file mode 100644
index 0000000000000..2ff333cbb81ef
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Reader/Xml.php
@@ -0,0 +1,56 @@
+ 'name'
+ ];
+
+ /**
+ * @param \Magento\Framework\Config\FileResolverInterface $fileResolver
+ * @param \Magento\Analytics\ReportXml\Config\Converter\Xml $converter
+ * @param \Magento\Analytics\ReportXml\Config\SchemaLocator $schemaLocator
+ * @param \Magento\Framework\Config\ValidationStateInterface $validationState
+ * @param string $fileName
+ * @param array $idAttributes
+ * @param string $domDocumentClass
+ * @param string $defaultScope
+ */
+ public function __construct(
+ \Magento\Framework\Config\FileResolverInterface $fileResolver,
+ \Magento\Analytics\ReportXml\Config\Converter\Xml $converter,
+ \Magento\Analytics\ReportXml\Config\SchemaLocator $schemaLocator,
+ \Magento\Framework\Config\ValidationStateInterface $validationState,
+ $fileName = 'reports.xml',
+ $idAttributes = [],
+ $domDocumentClass = \Magento\Framework\Config\Dom::class,
+ $defaultScope = 'global'
+ ) {
+ parent::__construct(
+ $fileResolver,
+ $converter,
+ $schemaLocator,
+ $validationState,
+ $fileName,
+ $idAttributes,
+ $domDocumentClass,
+ $defaultScope
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/SchemaLocator.php b/app/code/Magento/Analytics/ReportXml/Config/SchemaLocator.php
new file mode 100644
index 0000000000000..0a5a49e9b9744
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/SchemaLocator.php
@@ -0,0 +1,51 @@
+urnResolver = $urnResolver;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSchema()
+ {
+ return $this->urnResolver->getRealPath($this->realPath);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPerFileSchema()
+ {
+ return null;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ConfigInterface.php b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
new file mode 100644
index 0000000000000..f706321aa767c
--- /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..04764b92e91e2
--- /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..2ae835bb3344f
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
@@ -0,0 +1,63 @@
+nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ }
+
+ /**
+ * 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->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..a457b8bd126a1
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
@@ -0,0 +1,105 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ }
+
+ /**
+ * 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->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..57f08c236116a
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
@@ -0,0 +1,69 @@
+nameResolver = $nameResolver;
+ }
+
+ /**
+ * 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 \Zend_Db_Expr(
+ strtoupper($attributeData['function']) . '(' . $prefix . $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..9eb506cbb105f
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
@@ -0,0 +1,163 @@
+ '%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 \Zend_Db_Expr($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 \Zend_Db_Expr) {
+ $expression = $columns[$condition['attribute']];
+ } else {
+ $expression = $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(' AND ', $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..52d8177cbb8a9
--- /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..303cd09262b0e
--- /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..efe958fee315e
--- /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..3fc12ccd0a39c
--- /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..2932d42228af2
--- /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..2bdf2432600dd
--- /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..001a35354cf0a
--- /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..9a8a8ffab0861
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
@@ -0,0 +1,79 @@
+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
+ * @param SearchCriteria|null $criteria
+ * @return \IteratorIterator
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getReport($name, SearchCriteria $criteria = null)
+ {
+ $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..b9a46fd286bd7
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
@@ -0,0 +1,96 @@
+resourceConnection = $resourceConnection;
+ $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();
+ foreach ($selectParts as $partName => $partValue) {
+ $select->setPart($partName, $partValue);
+ }
+ 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..b8d311ee5ea15
--- /dev/null
+++ b/app/code/Magento/Analytics/Setup/InstallData.php
@@ -0,0 +1,43 @@
+notificationTime = $notificationTime;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
+ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
+ {
+ $this->notificationTime->storeLastTimeNotification(1);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BasicTier/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BasicTier/SignUpTest.php
new file mode 100644
index 0000000000000..c9311ddf1ccc8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BasicTier/SignUpTest.php
@@ -0,0 +1,84 @@
+configMock = $this->getMockBuilder(Config::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()
+ {
+ $basicTierUrlPath = 'analytics/url/basic_tier';
+ $this->configMock->expects($this->once())
+ ->method('getConfigDataValue')
+ ->with($basicTierUrlPath)
+ ->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..faedcedd2b5e8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
@@ -0,0 +1,155 @@
+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')],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/ActivateTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/ActivateTest.php
new file mode 100644
index 0000000000000..a1cf41ace802c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/ActivateTest.php
@@ -0,0 +1,229 @@
+resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultJsonMock = $this->getMockBuilder(Json::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subscriptionModelMock = $this->getMockBuilder(Subscription::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->requestMock = $this->getMockBuilder(RequestInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->notificationTimeMock = $this->getMockBuilder(NotificationTime::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->activateController = $this->objectManagerHelper->getObject(
+ Activate::class,
+ [
+ 'resultFactory' => $this->resultFactoryMock,
+ 'subscription' => $this->subscriptionModelMock,
+ 'logger' => $this->loggerMock,
+ 'notificationTime' => $this->notificationTimeMock,
+ '_request' => $this->requestMock,
+ 'subscriptionApprovedFiled' => $this->subscriptionApprovedField,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider executeDataProvider
+ *
+ * @param bool $isSubscriptionEnabled
+ * @return void
+ */
+ public function testExecute($isSubscriptionEnabled)
+ {
+ $successResult = [
+ 'success' => true,
+ 'error_message' => '',
+ ];
+
+ $this->requestMock
+ ->expects($this->once())
+ ->method('getParam')
+ ->with($this->subscriptionApprovedField)
+ ->willReturn($isSubscriptionEnabled);
+
+ if ($isSubscriptionEnabled) {
+ $this->subscriptionModelMock
+ ->expects($this->once())
+ ->method('enable')
+ ->willReturn(true);
+ } else {
+ $this->notificationTimeMock
+ ->expects($this->once())
+ ->method('unsetLastTimeNotificationValue')
+ ->willReturn(true);
+ }
+
+ $this->resultFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_JSON)
+ ->willReturn($this->resultJsonMock);
+ $this->resultJsonMock->expects($this->once())
+ ->method('setData')
+ ->with($successResult)
+ ->willReturnSelf();
+ $this->assertSame(
+ $this->resultJsonMock,
+ $this->activateController->execute()
+ );
+ }
+
+ /**
+ * @dataProvider executeExceptionsDataProvider
+ *
+ * @param \Exception $exception
+ */
+ public function testExecuteWithException(\Exception $exception)
+ {
+ $this->requestMock
+ ->expects($this->once())
+ ->method('getParam')
+ ->with($this->subscriptionApprovedField)
+ ->willReturn(true);
+
+ $this->subscriptionModelMock
+ ->expects($this->once())
+ ->method('enable')
+ ->willThrowException($exception);
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_JSON)
+ ->willReturn($this->resultJsonMock);
+
+ if ($exception instanceof LocalizedException) {
+ $this->resultJsonMock
+ ->expects($this->once())
+ ->method('setData')
+ ->with([
+ 'success' => false,
+ 'error_message' => $exception->getMessage(),
+ ])
+ ->willReturnSelf();
+ } else {
+ $this->resultJsonMock
+ ->expects($this->once())
+ ->method('setData')
+ ->withAnyParameters()
+ ->willReturnSelf();
+ }
+
+ $this->assertSame(
+ $this->resultJsonMock,
+ $this->activateController->execute()
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function executeExceptionsDataProvider()
+ {
+ return [
+ [new LocalizedException(__('TestMessage'))],
+ [new \Exception('TestMessage')],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function executeDataProvider()
+ {
+ return [
+ [true],
+ [false],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/PostponeTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/PostponeTest.php
new file mode 100644
index 0000000000000..78027298028d1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/PostponeTest.php
@@ -0,0 +1,168 @@
+dateTimeFactoryMock = $this->getMockBuilder(DateTimeFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->dateTimeMock = $this->getMockBuilder(\DateTime::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->notificationTimeMock = $this->getMockBuilder(NotificationTime::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->getMock();
+ $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultMock = $this->getMockBuilder(Json::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_JSON)
+ ->willReturn($this->resultMock);
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->action = $objectManagerHelper->getObject(
+ Postpone::class,
+ [
+ 'resultFactory' => $this->resultFactoryMock,
+ 'dateTimeFactory' => $this->dateTimeFactoryMock,
+ 'notificationTime' => $this->notificationTimeMock,
+ 'logger' => $this->loggerMock
+ ]
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->dateTimeFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->dateTimeMock);
+ $this->dateTimeMock->expects($this->once())
+ ->method('getTimestamp')
+ ->willReturn(100500);
+ $this->notificationTimeMock->expects($this->once())
+ ->method('storeLastTimeNotification')
+ ->with(100500)
+ ->willReturn(true);
+ $this->resultMock->expects($this->once())
+ ->method('setData')
+ ->with(
+ [
+ 'success' => true,
+ 'error_message' => ''
+ ],
+ false,
+ []
+ )->willReturnSelf();
+ $this->assertEquals($this->resultMock, $this->action->execute());
+ }
+
+ public function testExecuteFailedWithLocalizedException()
+ {
+ $this->dateTimeFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->dateTimeMock);
+ $this->dateTimeMock->expects($this->once())
+ ->method('getTimestamp')
+ ->willReturn(100500);
+ $this->notificationTimeMock->expects($this->once())
+ ->method('storeLastTimeNotification')
+ ->with(100500)
+ ->willThrowException(new LocalizedException(__('Error message')));
+ $this->resultMock->expects($this->once())
+ ->method('setData')
+ ->with(
+ [
+ 'success' => false,
+ 'error_message' => 'Error message'
+ ],
+ false,
+ []
+ )->willReturnSelf();
+ $this->assertEquals($this->resultMock, $this->action->execute());
+ }
+
+ public function testExecuteFailedWithException()
+ {
+ $this->dateTimeFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->dateTimeMock);
+ $this->dateTimeMock->expects($this->once())
+ ->method('getTimestamp')
+ ->willReturn(100500);
+ $this->notificationTimeMock->expects($this->once())
+ ->method('storeLastTimeNotification')
+ ->with(100500)
+ ->willThrowException(new \Exception('Any message'));
+ $this->resultMock->expects($this->once())
+ ->method('setData')
+ ->with(
+ [
+ 'success' => false,
+ 'error_message' => __('Error occurred during postponement notification')
+ ],
+ false,
+ []
+ )->willReturnSelf();
+ $this->assertEquals($this->resultMock, $this->action->execute());
+ }
+}
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..1b65a14659c86
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
@@ -0,0 +1,92 @@
+exportDataHandlerMock = $this->getMockBuilder(ExportDataHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $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..1b5d0d6a92b1c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
@@ -0,0 +1,171 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->inboxFactoryMock = $this->getMockBuilder(InboxFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->inboxResourceMock = $this->getMockBuilder(InboxResource::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->inboxMock = $this->getMockBuilder(Inbox::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUp = new SignUp(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->inboxFactoryMock,
+ $this->inboxResourceMock,
+ $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->inboxFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->inboxMock);
+ $this->inboxMock->expects($this->once())
+ ->method('addNotice');
+ $this->inboxResourceMock->expects($this->once())
+ ->method('save')
+ ->with($this->inboxMock);
+ $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..ba52571710212
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
@@ -0,0 +1,93 @@
+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->update = new Update(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->reinitableConfigMock,
+ $this->flagManagerMock
+ );
+ }
+
+ public function testExecute()
+ {
+ $this->connectorMock->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(true);
+ $this->configWriterMock->expects($this->once())
+ ->method('delete')
+ ->with(BaseUrlConfigPlugin::UPDATE_CRON_STRING_PATH);
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(BaseUrlConfigPlugin::OLD_BASE_URL_FLAG_CODE);
+ $this->reinitableConfigMock->expects($this->once())
+ ->method('reinit');
+ $this->assertTrue($this->update->execute());
+ }
+
+ public function testExecuteUnsuccess()
+ {
+ $this->connectorMock->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->assertFalse($this->update->execute());
+ }
+}
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..2409ec7c110ca
--- /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/Condition/CanViewNotificationTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Condition/CanViewNotificationTest.php
new file mode 100644
index 0000000000000..f361fde03f02d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Condition/CanViewNotificationTest.php
@@ -0,0 +1,81 @@
+dateTimeFactoryMock = $this->getMockBuilder(DateTimeFactory::class)
+ ->getMock();
+ $this->dateTimeMock = $this->getMockBuilder(\DateTime::class)
+ ->getMock();
+ $this->notificationTimeMock = $this->getMockBuilder(NotificationTime::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManager = new ObjectManager($this);
+ $this->canViewNotification = $objectManager->getObject(
+ CanViewNotification::class,
+ [
+ 'notificationTime' => $this->notificationTimeMock,
+ 'dateTimeFactory' => $this->dateTimeFactoryMock
+ ]
+ );
+ }
+
+ public function testValidate()
+ {
+ $this->notificationTimeMock->expects($this->once())
+ ->method('getLastTimeNotification')
+ ->willReturn(1);
+ $this->dateTimeFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->dateTimeMock);
+ $this->dateTimeMock->expects($this->once())
+ ->method('getTimestamp')
+ ->willReturn(10005000);
+ $this->assertTrue($this->canViewNotification->validate());
+ }
+
+ public function testValidateFlagRemoved()
+ {
+ $this->notificationTimeMock->expects($this->once())
+ ->method('getLastTimeNotification')
+ ->willReturn(null);
+ $this->dateTimeFactoryMock->expects($this->never())
+ ->method('create');
+ $this->assertFalse($this->canViewNotification->validate());
+ }
+}
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..132aa4aac9529
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
@@ -0,0 +1,112 @@
+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..f7a299cd4c657
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
@@ -0,0 +1,187 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->tokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->notificationTimeMock = $this->getMockBuilder(NotificationTime::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configValueMock = $this->getMockBuilder(Value::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,
+ 'notificationTime' => $this->notificationTimeMock,
+ ]
+ );
+ }
+
+ /**
+ * @param int|null $value null means that $value was not changed
+ * @param bool $isTokenExist
+ *
+ * @dataProvider processDataProvider
+ */
+ public function testProcess($value, $isTokenExist)
+ {
+ $this->configValueMock
+ ->expects($this->once())
+ ->method('isValueChanged')
+ ->willReturn(is_int($value));
+ $this->configValueMock
+ ->expects(is_int($value) ? $this->once() : $this->never())
+ ->method('getData')
+ ->with('value')
+ ->willReturn($value);
+ $this->configWriterMock
+ ->expects(($value === 0) ? $this->once() : $this->never())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects(is_int($value) ? $this->once() : $this->never())
+ ->method('isTokenExist')
+ ->willReturn($isTokenExist);
+ if (is_int($value) && !$isTokenExist) {
+ if ($value === 1) {
+ $this->addProcessWithEnabledTrueAsserts();
+ } elseif ($value === 0) {
+ $this->addProcessWithEnabledFalseAsserts();
+ }
+ }
+ $this->assertTrue(
+ $this->subscriptionHandler->process($this->configValueMock)
+ );
+ }
+
+ /**
+ * Add assertions for method process in case when new config value equals 1.
+ *
+ * @return void
+ */
+ private function addProcessWithEnabledTrueAsserts()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->with(SubscriptionHandler::CRON_STRING_PATH, "0 * * * *")
+ ->method('save');
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue)
+ ->willReturn(true);
+ $this->notificationTimeMock
+ ->expects($this->once())
+ ->method('unsetLastTimeNotificationValue')
+ ->willReturn(true);
+ }
+
+ /**
+ * Add assertions for method process in case when new config value equals 0.
+ *
+ * @return void
+ */
+ private function addProcessWithEnabledFalseAsserts()
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(true);
+ }
+
+ /**
+ * Data provider for process test.
+ *
+ * @return array
+ */
+ public function processDataProvider()
+ {
+ return [
+ 'Config value has not changed and token exist' => [null, true],
+ 'Config value has not changed and token doesn\'t exist' => [null, false],
+ 'Config value is "No" and token exist' => [0, true],
+ 'Config value is "Yes" and token exist' => [1, true],
+ 'Config value is "No" and token doesn\'t exist' => [0, false],
+ ];
+ }
+}
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..336c80217b4c6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
@@ -0,0 +1,99 @@
+subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->enabledModel = $this->objectManagerHelper->getObject(
+ Enabled::class,
+ [
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ '_logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccess()
+ {
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('process')
+ ->with($this->enabledModel)
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testExecuteAfterSaveFailedWithLocalizedException()
+ {
+ $exception = new \Exception('Message');
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('process')
+ ->with($this->enabledModel)
+ ->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..72c70e3a85398
--- /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..d39ada40f811e
--- /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..41883e2c8e3ad
--- /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/SchemaLocatorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/SchemaLocatorTest.php
new file mode 100644
index 0000000000000..e486268e06bb2
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/SchemaLocatorTest.php
@@ -0,0 +1,80 @@
+urnResolverMock = $this->getMockBuilder(UrnResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->schemaLocator = $this->objectManagerHelper->getObject(
+ SchemaLocator::class,
+ [
+ 'urnResolver' => $this->urnResolverMock,
+ 'schema' => $this->schema,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSchema()
+ {
+ $schemaRealPath = '/path/test.xml';
+
+ $this->urnResolverMock
+ ->expects($this->once())
+ ->method('getRealPath')
+ ->with($this->schema)
+ ->willReturn($schemaRealPath);
+
+ $this->assertSame($schemaRealPath, $this->schemaLocator->getSchema());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetPerFileSchema()
+ {
+ $this->assertNull($this->schemaLocator->getPerFileSchema());
+ }
+}
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..0213e61d9ab35
--- /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..0876a9dba3db6
--- /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..20f7e80bf0287
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
@@ -0,0 +1,203 @@
+curlMock = $this->getMockBuilder(
+ \Magento\Framework\HTTP\Adapter\Curl::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseMock = $this->getMockBuilder(
+ \Zend_Http_Response::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->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,
+ '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' => ['Content-Type: application/json'],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestSuccess(array $data)
+ {
+ $responseString = 'This is response.';
+
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ $data['version']
+ );
+ $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($this->responseMock);
+
+ $this->assertEquals(
+ $this->responseMock,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestError(array $data)
+ {
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ $data['version']
+ );
+ $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->assertFalse(
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+}
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..f8faef12adc77
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
@@ -0,0 +1,275 @@
+loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(
+ \Magento\Framework\App\Config\ScopeConfigInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseMock = $this->getMockBuilder(
+ \Zend_Http_Response::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\Connector\Http\ClientInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\AnalyticsToken::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Connector\OTPRequest::class,
+ [
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'config' => $this->configMock,
+ 'httpClient' => $this->httpClientMock,
+ 'logger' => $this->loggerMock
+ ]
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'otp' => 'thisisotp',
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'headers' => ['Content-Type: application/json'],
+ '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'],
+ $data['headers']
+ )
+ ->willReturn($this->responseMock);
+
+ $this->responseMock->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(201);
+ $this->responseMock->expects($this->any())
+ ->method('getBody')
+ ->willReturn('{"otp": "' . $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 testCallTransportFailure()
+ {
+ $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'],
+ $data['headers']
+ )
+ ->willReturn(false);
+
+ $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'],
+ $data['headers']
+ )
+ ->willReturn($this->responseMock);
+
+ $this->responseMock->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(409);
+
+ $this->loggerMock->expects($this->once())
+ ->method('warning');
+
+ $this->assertFalse($this->subject->call());
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallException()
+ {
+ $data = $this->getTestData();
+
+ $exception = new \Exception('Test Exception');
+
+ $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'],
+ $data['headers']
+ )
+ ->willThrowException($exception);
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with($exception);
+
+ $this->assertFalse($this->subject->call());
+ }
+}
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..42d7ea0824ad5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
@@ -0,0 +1,126 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationManagerMock = $this->getMockBuilder(IntegrationManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationToken = $this->getMockBuilder(IntegrationToken::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getToken'])
+ ->getMock();
+ $this->integrationToken->expects($this->any())
+ ->method('getToken')
+ ->willReturn('IntegrationToken');
+ $this->signUpRequestMock = $this->getMockBuilder(SignUpRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->signUpCommand = $objectManagerHelper->getObject(
+ SignUpCommand::class,
+ [
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'integrationManager' => $this->integrationManagerMock,
+ 'signUpRequest' => $this->signUpRequestMock
+ ]
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $this->signUpRequestMock->expects($this->once())
+ ->method('call')
+ ->with('IntegrationToken')
+ ->willReturn('MAToken');
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('storeToken')
+ ->with('MAToken')
+ ->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')
+ ->willReturn(true);
+ $this->signUpRequestMock->expects($this->never())
+ ->method('call')
+ ->willReturn('MAToken');
+ $this->analyticsTokenMock->expects($this->never())
+ ->method('storeToken')
+ ->willReturn(true);
+ $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);
+ $this->signUpRequestMock->expects($this->once())
+ ->method('call')
+ ->with('IntegrationToken')
+ ->willReturn(false);
+ $this->analyticsTokenMock->expects($this->never())
+ ->method('storeToken');
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpRequestTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpRequestTest.php
new file mode 100644
index 0000000000000..98ea13e092fa6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpRequestTest.php
@@ -0,0 +1,226 @@
+loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(
+ \Magento\Config\Model\Config::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseMock = $this->getMockBuilder(
+ \Zend_Http_Response::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\Connector\Http\ClientInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Connector\SignUpRequest::class,
+ [
+ 'config' => $this->configMock,
+ 'httpClient' => $this->httpClientMock,
+ 'logger' => $this->loggerMock
+ ]
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'integration-token' => 'thisisintegrationtoken',
+ 'headers' => ['Content-Type: application/json'],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> '{"token":"thisisintegrationtoken","url":"http:\/\/www.mystore.com"}',
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallSuccess()
+ {
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getConfigDataValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers']
+ )
+ ->willReturn($this->responseMock);
+
+ $this->responseMock->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(201);
+ $this->responseMock->expects($this->any())
+ ->method('getBody')
+ ->willReturn('{"access-token": "' . $data['access-token'] . '"}');
+
+ $this->assertEquals(
+ $data['access-token'],
+ $this->subject->call($data['integration-token'])
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallTransportFailure()
+ {
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getConfigDataValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers']
+ )
+ ->willReturn(false);
+
+ $this->assertFalse(
+ $this->subject->call($data['integration-token'])
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoAccessToken()
+ {
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getConfigDataValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers']
+ )
+ ->willReturn($this->responseMock);
+
+ $this->responseMock->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(409);
+
+ $this->loggerMock->expects($this->once())
+ ->method('warning');
+
+ $this->assertFalse(
+ $this->subject->call($data['integration-token'])
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallException()
+ {
+ $data = $this->getTestData();
+
+ $exception = new \Exception('Test Exception');
+
+ $this->configMock->expects($this->any())
+ ->method('getConfigDataValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers']
+ )
+ ->willThrowException($exception);
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with($exception);
+
+ $this->assertFalse(
+ $this->subject->call($data['integration-token'])
+ );
+ }
+}
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..f16944d2f3417
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
@@ -0,0 +1,139 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseMock = $this->getMockBuilder(\Zend_Http_Response::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->updateCommand = new UpdateCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->loggerMock,
+ $this->flagManagerMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $url = "old.localhost.com";
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $requestJson = sprintf('{"url":"%s","new-url":"%s","access-token":"%s"}', $url, $configVal, $token);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+
+ $this->configMock->expects($this->any())
+ ->method('getConfigDataValue')
+ ->willReturn($configVal);
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(BaseUrlConfigPlugin::OLD_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,
+ $requestJson,
+ ['Content-Type: application/json']
+ )->willReturn($this->responseMock);
+
+ $this->responseMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn(201);
+
+ $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..a52186adf989e
--- /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..4dccb54e192f5
--- /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..a3ea8b040c992
--- /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/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
new file mode 100644
index 0000000000000..21fe5fdf8081c
--- /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..79617efff51ed
--- /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..282ab67a523dc
--- /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..244b46439a492
--- /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/FlagManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FlagManagerTest.php
new file mode 100644
index 0000000000000..77abb0c87e1ea
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FlagManagerTest.php
@@ -0,0 +1,123 @@
+flagFactoryMock = $this->getMockBuilder(FlagFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagResourceMock = $this->getMockBuilder(FlagResource::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagMock = $this->getMockBuilder(Flag::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManager = new FlagManager(
+ $this->flagFactoryMock,
+ $this->flagResourceMock
+ );
+ }
+
+ public function testGetFlagData()
+ {
+ $flagCode = "flag";
+ $this->setupFlagObject($flagCode);
+ $this->flagMock->expects($this->once())
+ ->method('getFlagData')
+ ->willReturn(10);
+ $this->assertEquals($this->flagManager->getFlagData($flagCode), 10);
+ }
+
+ public function testSaveFlag()
+ {
+ $flagCode = "flag";
+ $this->setupFlagObject($flagCode);
+ $this->flagMock->expects($this->once())
+ ->method('setFlagData')
+ ->with(10);
+ $this->flagResourceMock->expects($this->once())
+ ->method('save')
+ ->with($this->flagMock);
+ $this->assertTrue($this->flagManager->saveFlag($flagCode, 10));
+ }
+
+ /**
+ * @dataProvider flagExistDataProvider
+ *
+ * @param bool $isFlagExist
+ */
+ public function testDeleteFlag($isFlagExist)
+ {
+ $flagCode = "flag";
+ $this->setupFlagObject($flagCode);
+ $this->flagMock
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn($isFlagExist);
+ if ($isFlagExist) {
+ $this->flagResourceMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with($this->flagMock);
+ }
+ $this->assertTrue($this->flagManager->deleteFlag($flagCode));
+ }
+
+ private function setupFlagObject($flagCode)
+ {
+ $this->flagFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(['data' => ['flag_code' => $flagCode]])
+ ->willReturn($this->flagMock);
+ $this->flagResourceMock->expects($this->once())
+ ->method('load')
+ ->with($this->flagMock, $flagCode, 'flag_code');
+ }
+
+ /**
+ * Provide variations of the flag existence.
+ *
+ * @return array
+ */
+ public function flagExistDataProvider()
+ {
+ return [
+ [true],
+ [false]
+ ];
+ }
+}
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..14af7c7e66e99
--- /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..e0ae69c975f7e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
@@ -0,0 +1,162 @@
+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,
+ [
+ 'linkInterfaceFactory' => $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')
+ ->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->linkInterfaceMock->expects($this->once())->method('setUrl')->with(
+ $baseUrl . $fileInfoPath
+ );
+ $this->linkInterfaceMock->expects($this->once())
+ ->method('setInitializationVector')
+ ->with(base64_encode($fileInitializationVector));
+ $this->assertEquals($this->linkInterfaceMock, $this->linkProvider->get());
+ }
+
+ /**
+ * @param string|null $fileInfoPath
+ * @param string|null $fileInitializationVector
+ *
+ * @dataProvider fileNotReadyDataProvider
+ * @expectedException \Magento\Framework\Webapi\Exception
+ * @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]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/NotificationTimeTest.php b/app/code/Magento/Analytics/Test/Unit/Model/NotificationTimeTest.php
new file mode 100644
index 0000000000000..78bb694645fe8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/NotificationTimeTest.php
@@ -0,0 +1,76 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->notificationTime = $objectManagerHelper->getObject(
+ NotificationTime::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ ]
+ );
+ }
+
+ public function testStoreLastTimeNotification()
+ {
+ $value = 100500;
+
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with(NotificationTime::NOTIFICATION_TIME, $value)
+ ->willReturn(true);
+ $this->assertTrue($this->notificationTime->storeLastTimeNotification($value));
+ }
+
+ public function testGetLastTimeNotification()
+ {
+ $value = 100500;
+
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(NotificationTime::NOTIFICATION_TIME)
+ ->willReturn(true);
+ $this->assertEquals($value, $this->notificationTime->getLastTimeNotification());
+ }
+
+ public function testUnsetLastTimeNotificationValue()
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('deleteFlag')
+ ->with(NotificationTime::NOTIFICATION_TIME)
+ ->willReturn(true);
+ $this->assertTrue($this->notificationTime->unsetLastTimeNotificationValue());
+ }
+}
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..bf6456610f7a3
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
@@ -0,0 +1,190 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configValueMock = $this->getMockBuilder(Baseurl::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->subscriptionStatusProvider = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->plugin = $this->objectManagerHelper->getObject(
+ BaseUrlConfigPlugin::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'subscriptionStatusProvider' => $this->subscriptionStatusProvider,
+ 'configWriter' => $this->configWriterMock
+ ]
+ );
+ }
+
+ /**
+ * @param array $testData
+ * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $saveConfigInvokeMatcher
+ * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $oldValueInvokeMatcher
+ * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $saveFlagInvokeMatcher
+ * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $configValueGetPathMatcher
+ *
+ * @return void
+ * @dataProvider pluginDataProvider
+ */
+ public function testPluginForAfterSave(
+ array $testData,
+ \PHPUnit_Framework_MockObject_Matcher_InvokedCount $saveConfigInvokeMatcher,
+ \PHPUnit_Framework_MockObject_Matcher_InvokedCount $oldValueInvokeMatcher,
+ \PHPUnit_Framework_MockObject_Matcher_InvokedCount $saveFlagInvokeMatcher,
+ \PHPUnit_Framework_MockObject_Matcher_InvokedCount $configValueGetPathMatcher
+ ) {
+ $this->configValueMock->expects($this->once())
+ ->method('isValueChanged')
+ ->willReturn($testData['isValueChanged']);
+
+ $this->configValueMock->expects($configValueGetPathMatcher)
+ ->method('getData')
+ ->with('path')
+ ->willReturn($testData['path']);
+ $this->subscriptionStatusProvider->expects($this->any())->method('getStatus')
+ ->willReturn($testData['subscriptionStatus']);
+
+ $oldUrl = 'mage.dev';
+ $this->configValueMock->expects($oldValueInvokeMatcher)
+ ->method('getOldValue')
+ ->willReturn($oldUrl);
+ $this->flagManagerMock->expects($saveFlagInvokeMatcher)
+ ->method('saveFlag')
+ ->with(BaseUrlConfigPlugin::OLD_BASE_URL_FLAG_CODE, $oldUrl);
+
+ $this->configWriterMock->expects($saveConfigInvokeMatcher)->method('save')
+ ->with(
+ BaseUrlConfigPlugin::UPDATE_CRON_STRING_PATH,
+ '0 * * * *'
+ );
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function pluginDataProvider()
+ {
+ return [
+ 'setup_subscription_update_cron_job' => [
+ 'testData' => [
+ 'isValueChanged' => true,
+ 'subscriptionStatus' => SubscriptionStatusProvider::ENABLED,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL
+ ],
+ 'saveConfigInvokeMatcher' => $this->once(),
+ 'oldValueInvokeMatcher' => $this->once(),
+ 'saveFlagInvokeMatcher' => $this->once(),
+ 'configValueGetPathMatcher' => $this->once(),
+ ],
+ 'base_url_not_changed' => [
+ 'testData' => [
+ 'isValueChanged' => false,
+ 'subscriptionStatus' => SubscriptionStatusProvider::ENABLED,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL
+ ],
+ 'saveConfigInvokeMatcher' => $this->never(),
+ 'oldValueInvokeMatcher' => $this->never(),
+ 'saveFlagInvokeMatcher' => $this->never(),
+ 'configValueGetPathMatcher' => $this->never(),
+ ],
+ 'analytics_disabled' => [
+ 'testData' => [
+ 'isValueChanged' => true,
+ 'subscriptionStatus' => SubscriptionStatusProvider::DISABLED,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL
+ ],
+ 'saveConfigInvokeMatcher' => $this->never(),
+ 'oldValueInvokeMatcher' => $this->never(),
+ 'saveFlagInvokeMatcher' => $this->never(),
+ 'configValueGetPathMatcher' => $this->once(),
+ ],
+ 'analytics_pending' => [
+ 'testData' => [
+ 'isValueChanged' => true,
+ 'subscriptionStatus' => SubscriptionStatusProvider::PENDING,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL
+ ],
+ 'saveConfigInvokeMatcher' => $this->never(),
+ 'oldValueInvokeMatcher' => $this->never(),
+ 'saveFlagInvokeMatcher' => $this->never(),
+ 'configValueGetPathMatcher' => $this->once(),
+ ],
+ 'unsecure_url_changed' => [
+ 'testData' => [
+ 'isValueChanged' => true,
+ 'subscriptionStatus' => SubscriptionStatusProvider::PENDING,
+ 'path' => Store::XML_PATH_UNSECURE_BASE_URL
+ ],
+ 'saveConfigInvokeMatcher' => $this->never(),
+ 'oldValueInvokeMatcher' => $this->never(),
+ 'saveFlagInvokeMatcher' => $this->never(),
+ 'configValueGetPathMatcher' => $this->once(),
+ ]
+ ];
+ }
+}
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..4f3648a776b53
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
@@ -0,0 +1,122 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->otpRequestMock = $this->getMockBuilder(OTPRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportUrlProvider = $this->objectManagerHelper->getObject(
+ ReportUrlProvider::class,
+ [
+ 'config' => $this->configMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'otpRequest' => $this->otpRequestMock,
+ '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'],
+ ];
+ }
+}
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..4f9a9aaf185a6
--- /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..7bd6a0c5d5534
--- /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..13c76282b0b20
--- /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..4e32308d51e3c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
@@ -0,0 +1,94 @@
+systemConfigMock = $this->getMockBuilder(System::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->statusProvider = $this->objectManagerHelper->getObject(
+ SubscriptionStatusProvider::class,
+ [
+ 'systemConfig' => $this->systemConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider statusDataProvider
+ *
+ * @param bool $isSubscriptionEnabled
+ * @param bool $hasToken
+ * @param int $attempts
+ * @param string $expectedStatus
+ */
+ public function testGetStatus($isSubscriptionEnabled, $hasToken, $attempts, $expectedStatus)
+ {
+ $this->analyticsTokenMock->expects($this->exactly($attempts))
+ ->method('isTokenExist')
+ ->willReturn($hasToken);
+ $this->systemConfigMock->expects($this->once())
+ ->method('get')
+ ->with('default/analytics/subscription/enabled')
+ ->willReturn($isSubscriptionEnabled);
+ $this->assertEquals($expectedStatus, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function statusDataProvider()
+ {
+ return [
+ 'TestWithEnabledStatus' => [true, true, 1, "Enabled"],
+ 'TestWithPendingStatus' => [true, false, 1, "Pending"],
+ 'TestWithDisabledStatus' => [false, false, 0, "Disabled"],
+ 'TestWithDisabledStatus2' => [false, true, 0, "Disabled"],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionTest.php b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionTest.php
new file mode 100644
index 0000000000000..693b97cb0aff2
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionTest.php
@@ -0,0 +1,197 @@
+configValueFactoryMock = $this->getMockBuilder(ValueFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configValueMock = $this->getMockBuilder(Value::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['setValue', 'setPath'])
+ ->getMock();
+
+ $this->configStructureMock = $this->getMockBuilder(SearchInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configValueResourceMock = $this->getMockBuilder(AbstractDb::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->elementFieldMock = $this->getMockBuilder(Field::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionModel = $this->objectManagerHelper->getObject(
+ SubscriptionModel::class,
+ [
+ 'configValueFactory' => $this->configValueFactoryMock,
+ 'configStructure' => $this->configStructureMock,
+ 'configValueResource' => $this->configValueResourceMock,
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'enabledConfigStructurePath' => $this->enableConfigStructurePath,
+ 'yesValueDropdown' => $this->yesValueDropdown,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider enabledDataProvider
+ *
+ * @param boolean $backendModel
+ * @param string $configPath
+ *
+ * @return void
+ */
+ public function testEnabled($backendModel, $configPath)
+ {
+ $this->configStructureMock
+ ->expects($this->once())
+ ->method('getElement')
+ ->with($this->enableConfigStructurePath)
+ ->willReturn($this->elementFieldMock);
+ $this->elementFieldMock
+ ->expects($this->once())
+ ->method('hasBackendModel')
+ ->willReturn($backendModel);
+ if ($backendModel) {
+ $this->elementFieldMock
+ ->expects($this->once())
+ ->method('getBackendModel')
+ ->willReturn($this->configValueMock);
+ } else {
+ $this->configValueFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->willReturn($this->configValueMock);
+ }
+ $this->elementFieldMock
+ ->expects($this->once())
+ ->method('getConfigPath')
+ ->willReturn($configPath);
+ $configPath = $configPath ?: $this->enableConfigStructurePath;
+ $this->configValueResourceMock
+ ->expects($this->once())
+ ->method('load')
+ ->with($this->configValueMock, $configPath, 'path')
+ ->willReturnSelf();
+ $this->configValueMock
+ ->expects($this->once())
+ ->method('setValue')
+ ->with(1)
+ ->willReturnSelf();
+ $this->configValueMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with($configPath)
+ ->willReturnSelf();
+ $this->configValueResourceMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->configValueMock)
+ ->willReturnSelf();
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+ $this->assertTrue($this->subscriptionModel->enable());
+ }
+
+ /**
+ * @return array
+ */
+ public function enabledDataProvider()
+ {
+ return [
+ 'TestWithBackendModelWithoutConfigPath' => [true, null],
+ 'TestWithBackendModelWithConfigPath' => [true, $this->configPath],
+ 'TestWithoutBackendModelWithoutConfigPath' => [false, null],
+ 'TestWithoutBackendModelWithConfigPath' => [false, $this->configPath],
+ ];
+ }
+}
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..57e2514259c1c
--- /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..b92ea2e866f74
--- /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/SchemaLocatorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/SchemaLocatorTest.php
new file mode 100644
index 0000000000000..a4b14490a6fba
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/SchemaLocatorTest.php
@@ -0,0 +1,74 @@
+urnResolverMock = $this->getMockBuilder(UrnResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->schemaLocator = $this->objectManagerHelper->getObject(
+ SchemaLocator::class,
+ [
+ 'urnResolver' => $this->urnResolverMock,
+ 'realPath' => $this->examplePath,
+ ]
+ );
+ }
+
+ public function testGetSchema()
+ {
+ $schema = 'schema';
+
+ $this->urnResolverMock
+ ->expects($this->once())
+ ->method('getRealPath')
+ ->with($this->examplePath)
+ ->willReturn($schema);
+
+ $this->assertSame($schema, $this->schemaLocator->getSchema());
+ }
+
+ public function testGetPerFileSchema()
+ {
+ $this->assertNull($this->schemaLocator->getPerFileSchema());
+ }
+}
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..b1103893a25cf
--- /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..21c4e6dd08019
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
@@ -0,0 +1,64 @@
+dataMock = $this->getMockBuilder(Data::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..9c88eb7616e30
--- /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..4266b7602e009
--- /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..e5e847648039d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
@@ -0,0 +1,121 @@
+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->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
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssemble()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ]
+ ]
+ ];
+
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+
+ $this->nameResolverMock->expects($this->any())
+ ->method('getName')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['name']);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFrom')
+ ->with([$queryConfigMock['source']['alias'] => $queryConfigMock['source']['name']]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfigMock['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, $queryConfigMock)
+ );
+ }
+}
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..f32f9dc3fa152
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
@@ -0,0 +1,261 @@
+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->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
+ ]
+ );
+ }
+
+ /**
+ * @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
+ *
+ * @dataProvider assembleNotEmptyDataProvider
+ * @return void
+ */
+ public function testAssembleNotEmpty(array $queryConfigMock)
+ {
+ $filtersMock = [];
+
+ $joinsMock = [
+ 'billing' => [
+ 'link-type' => 'left',
+ 'table' => [
+ 'billing' => 'sales_order_address'
+ ],
+ 'condition' => '(billing.parent_id = `sales`.`entity_id`)'
+ ]
+ ];
+
+ $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->any())
+ ->method('getName')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['name']);
+
+ $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'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+}
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..ae79f7536c504
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
@@ -0,0 +1,108 @@
+nameResolverMock = $this->getMockBuilder(NameResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->columnsResolver = new ColumnsResolver($this->nameResolverMock);
+ }
+
+ public function testGetColumnsWithoutAttributes()
+ {
+ $this->assertEquals($this->columnsResolver->getColumns($this->selectBuilderMock, []), []);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testGetColumns($expression, $attributeData)
+ {
+ $columnAlias = 'fn';
+ $expr = new \Zend_Db_Expr($expression);
+ $expectedResult = [$columnAlias => $expr];
+ $columns = [$columnAlias => 'name'];
+ $entityConfig['attribute'] = ['attribute1' => $attributeData];
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getColumns')
+ ->willReturn($columns);
+ $this->nameResolverMock->expects($this->at(0))
+ ->method('getAlias')
+ ->with($attributeData)
+ ->willReturn($columnAlias);
+ $this->nameResolverMock->expects($this->at(1))
+ ->method('getAlias')
+ ->with($entityConfig)
+ ->willReturn($columnAlias);
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($attributeData)
+ ->willReturn('name');
+ $group = ['g'];
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getGroup')
+ ->willReturn($group);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setGroup')
+ ->with(array_merge($group, $expectedResult));
+ $this->assertEquals(
+ $this->columnsResolver->getColumns(
+ $this->selectBuilderMock,
+ $entityConfig
+ ),
+ $expectedResult
+ );
+ }
+
+ public function dataProvider()
+ {
+ return [
+ 'TestWithFunction' =>
+ [
+ 'expression' => "SUM( DISTINCT fn.name)",
+ 'attributeData' => ['adata1', 'function' => 'SUM', 'distinct' => true, 'group' => true],
+ ],
+ 'TestWithoutFunction' => [
+ 'expression' => "fn.name",
+ 'attributeData' => ['adata1', 'distinct' => true, 'group' => true],
+ ],
+ ];
+ }
+}
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..1edf6a79ffa14
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
@@ -0,0 +1,102 @@
+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" => "3",
+ "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 \Zend_Db_Expr("(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->once())
+ ->method('quoteIdentifier')
+ ->willReturn("'Smith'");
+ $result = "(n.id != 1 OR ((n.first_name = 'John'))) AND (n.last_name = 'Smith')";
+ $this->assertEquals(
+ $this->conditionResolver->getFilter($this->selectBuilderMock, $filterConfig, $aliasName),
+ $result
+ );
+ }
+}
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..9bcbd1dadbd87
--- /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..ae64b28cd97a0
--- /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..b070000ea9f9d
--- /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..dee374b3bae05
--- /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..1b8502ad0fc53
--- /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..9b5a4bca74b2b
--- /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..ee20a59074485
--- /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..e6f6c5144d338
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
@@ -0,0 +1,113 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydrator = new SelectHydrator($this->resourceConnectionMock);
+ }
+
+ 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);
+ }
+
+ public function testRecreate()
+ {
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $parts =
+ [
+ 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
+ ];
+ $selectParts = [];
+ foreach ($parts as $key => $part) {
+ $this->selectMock->expects($this->at($key))
+ ->method('setPart')
+ ->with($part, 'part' . $key);
+ $selectParts[$part] = 'part' . $key;
+ }
+ $this->selectHydrator->recreate($selectParts);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Ui/DataProvider/DummyDataProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Ui/DataProvider/DummyDataProviderTest.php
new file mode 100644
index 0000000000000..8e0cd06ef272b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Ui/DataProvider/DummyDataProviderTest.php
@@ -0,0 +1,228 @@
+ 'value'];
+
+ /**
+ * @return void
+ */
+ protected function setUp()
+ {
+ $this->searchResultMock = $this->getMockBuilder(SearchResultInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->searchCriteriaMock = $this->getMockBuilder(SearchCriteriaInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->dataCollectionMock = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->filterMock = $this->getMockBuilder(Filter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->dummyDataProvider = $this->objectManagerHelper->getObject(
+ DummyDataProvider::class,
+ [
+ 'name' => $this->providerName,
+ 'searchResult' => $this->searchResultMock,
+ 'searchCriteria' => $this->searchCriteriaMock,
+ 'collection' => $this->dataCollectionMock,
+ 'data' => ['config' => $this->configData],
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetName()
+ {
+ $this->assertSame($this->providerName, $this->dummyDataProvider->getName());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetConfigData()
+ {
+ $this->assertSame($this->configData, $this->dummyDataProvider->getConfigData());
+ $dataProvider = $this->objectManagerHelper
+ ->getObject(
+ DummyDataProvider::class,
+ []
+ );
+ $this->assertSame([], $dataProvider->getConfigData());
+ }
+
+ /**
+ * @return void
+ */
+ public function testSetConfigData()
+ {
+ $configValue = ['key' => 'value'];
+
+ $this->assertTrue($this->dummyDataProvider->setConfigData($configValue));
+ $this->assertSame($configValue, $this->dummyDataProvider->getConfigData());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetMeta()
+ {
+ $this->assertSame([], $this->dummyDataProvider->getMeta());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetFieldMetaInfo()
+ {
+ $this->assertSame([], $this->dummyDataProvider->getFieldMetaInfo('', ''));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetFieldSetMetaInfo()
+ {
+ $this->assertSame([], $this->dummyDataProvider->getFieldSetMetaInfo(''));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetFieldsMetaInfo()
+ {
+ $this->assertSame([], $this->dummyDataProvider->getFieldsMetaInfo(''));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetPrimaryFieldName()
+ {
+ $this->assertSame('', $this->dummyDataProvider->getPrimaryFieldName());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetRequestFieldName()
+ {
+ $this->assertSame('', $this->dummyDataProvider->getRequestFieldName());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetData()
+ {
+ $this->dataCollectionMock
+ ->expects($this->once())
+ ->method('toArray')
+ ->willReturn([]);
+ $this->assertSame([], $this->dummyDataProvider->getData());
+ }
+
+ /**
+ * @return void
+ */
+ public function testAddFilter()
+ {
+ $this->assertNull($this->dummyDataProvider->addFilter($this->filterMock));
+ }
+
+ /**
+ * @return void
+ */
+ public function testAddOrder()
+ {
+ $this->assertNull($this->dummyDataProvider->addOrder('', ''));
+ }
+
+ /**
+ * @return void
+ */
+ public function testSetLimit()
+ {
+ $this->assertNull($this->dummyDataProvider->setLimit(1, 1));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSearchCriteria()
+ {
+ $this->assertSame($this->searchCriteriaMock, $this->dummyDataProvider->getSearchCriteria());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSearchResult()
+ {
+ $this->assertSame($this->searchResultMock, $this->dummyDataProvider->getSearchResult());
+ }
+}
diff --git a/app/code/Magento/Analytics/Ui/DataProvider/DummyDataProvider.php b/app/code/Magento/Analytics/Ui/DataProvider/DummyDataProvider.php
new file mode 100644
index 0000000000000..e842b6572b8c2
--- /dev/null
+++ b/app/code/Magento/Analytics/Ui/DataProvider/DummyDataProvider.php
@@ -0,0 +1,237 @@
+name = $name;
+ $this->searchResult = $searchResult;
+ $this->searchCriteria = $searchCriteria;
+ $this->collection = $collection;
+ $this->data = $data;
+ }
+
+ /**
+ * Get Data Provider name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get config data
+ *
+ * @return mixed
+ */
+ public function getConfigData()
+ {
+ return isset($this->data['config']) ? $this->data['config'] : [];
+ }
+
+ /**
+ * Set config data
+ *
+ * @param mixed $config
+ *
+ * @return bool
+ */
+ public function setConfigData($config)
+ {
+ $this->data['config'] = $config;
+
+ return true;
+ }
+
+ /**
+ * @return array
+ */
+ public function getMeta()
+ {
+ return [];
+ }
+
+ /**
+ * @param string $fieldSetName
+ * @param string $fieldName
+ *
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getFieldMetaInfo($fieldSetName, $fieldName)
+ {
+ return [];
+ }
+
+ /**
+ * Get field set meta info
+ *
+ * @param string $fieldSetName
+ *
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getFieldSetMetaInfo($fieldSetName)
+ {
+ return [];
+ }
+
+ /**
+ * @param string $fieldSetName
+ *
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getFieldsMetaInfo($fieldSetName)
+ {
+ return [];
+ }
+
+ /**
+ * Get primary field name
+ *
+ * @return string
+ */
+ public function getPrimaryFieldName()
+ {
+ return '';
+ }
+
+ /**
+ * Get field name in request
+ *
+ * @return string
+ */
+ public function getRequestFieldName()
+ {
+ return '';
+ }
+
+ /**
+ * Get data
+ *
+ * @return mixed
+ */
+ public function getData()
+ {
+ return $this->collection->toArray();
+ }
+
+ /**
+ * Add field filter to collection
+ *
+ * @param \Magento\Framework\Api\Filter $filter
+ *
+ * @return void
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function addFilter(\Magento\Framework\Api\Filter $filter)
+ {
+ }
+
+ /**
+ * Add ORDER BY to the end or to the beginning
+ *
+ * @param string $field
+ * @param string $direction
+ *
+ * @return void
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function addOrder($field, $direction)
+ {
+ }
+
+ /**
+ * Set Query limit
+ *
+ * @param int $offset
+ * @param int $size
+ *
+ * @return void
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function setLimit($offset, $size)
+ {
+ }
+
+ /**
+ * Returns search criteria
+ *
+ * @return SearchCriteriaInterface
+ */
+ public function getSearchCriteria()
+ {
+ return $this->searchCriteria;
+ }
+
+ /**
+ * @return SearchResultInterface
+ */
+ public function getSearchResult()
+ {
+ return $this->searchResult;
+ }
+}
diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json
new file mode 100644
index 0000000000000..b8d05bbd08a52
--- /dev/null
+++ b/app/code/Magento/Analytics/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "magento/module-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "~5.6.5|7.0.2|7.0.4|~7.0.6",
+ "magento/module-backend": "100.2.*",
+ "magento/module-admin-notification": "100.2.*",
+ "magento/module-config": "100.2.*",
+ "magento/module-integration": "100.2.*",
+ "magento/module-store": "100.2.*",
+ "magento/framework": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0-dev",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\Analytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/etc/acl.xml b/app/code/Magento/Analytics/etc/acl.xml
new file mode 100644
index 0000000000000..9dffb87bbabc0
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/acl.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..0d874688521da
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
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..232ebe94d314f
--- /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..c3e15c0ba3c20
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ Business Intelligence
+ general
+ Magento_Analytics::analytics_settings
+
+ General
+
+ Send the system and transaction data to Magento Analytics service
+ Magento\Config\Model\Config\Source\Yesno
+ Magento\Analytics\Model\Config\Backend\Enabled
+ analytics/subscription/enabled
+
+
+ Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel
+
+
+ Vertical
+ Magento\Analytics\Model\Config\Source\Vertical
+ Magento\Analytics\Model\Config\Backend\Vertical
+
+
+ Cron configuration
+
+ Time
+ Magento\Analytics\Model\Config\Backend\CollectionTime
+
+
+ 1
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xml b/app/code/Magento/Analytics/etc/analytics.xml
new file mode 100644
index 0000000000000..d35df54bffe7e
--- /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..da3357f8eb75a
--- /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..8a55adbdf730e
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/config.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ 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
+
+ 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..ae74693df98ae
--- /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..4fe2693afcda8
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/di.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\SignUpCommand
+ - Magento\Analytics\Model\Connector\UpdateCommand
+
+
+
+
+
+
+ - Magento\Analytics\Model\Condition\CanViewNotification
+
+
+
+
+
+ Magento\Config\Model\ResourceModel\Config\Data
+
+
+
+
+
+ - Magento\Analytics\ReportXml\Config\Reader\Xml
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\DB\Assembler\FromAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler
+
+
+
+
+
+
+ - Magento\Analytics\Model\Config\Reader\Xml
+
+
+
+
+
+ Magento\Analytics\Model\Config\Data
+
+
+
+
+ Magento\Analytics\Model\Config\SchemaLocator
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+
+
+
+
+ Magento\Analytics\Model\Config\Reader
+
+
+
+
+
+ - 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
+
+
+
+
+
+
+ - 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
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/module.xml b/app/code/Magento/Analytics/etc/module.xml
new file mode 100644
index 0000000000000..958d43cd6ee53
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/module.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/reports.xml b/app/code/Magento/Analytics/etc/reports.xml
new file mode 100644
index 0000000000000..1c9f4a3d62700
--- /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..e7e341f5d9a36
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xsd
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/webapi.xml b/app/code/Magento/Analytics/etc/webapi.xml
new file mode 100644
index 0000000000000..401d09b06563d
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/webapi.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/registration.php b/app/code/Magento/Analytics/registration.php
new file mode 100644
index 0000000000000..d07fe8ffb5e56
--- /dev/null
+++ b/app/code/Magento/Analytics/registration.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/link.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/link.phtml
new file mode 100644
index 0000000000000..6f7874b5c001e
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/link.phtml
@@ -0,0 +1,13 @@
+
+
+ escapeHtml(__('Free Tier')) ?>
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/ui_component/analytics_subscription_form.xml b/app/code/Magento/Analytics/view/adminhtml/ui_component/analytics_subscription_form.xml
new file mode 100644
index 0000000000000..60f3751d74bd4
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/ui_component/analytics_subscription_form.xml
@@ -0,0 +1,114 @@
+
+
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/web/js/modal/modal-component.js b/app/code/Magento/Analytics/view/adminhtml/web/js/modal/modal-component.js
new file mode 100644
index 0000000000000..4b215db24857a
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/web/js/modal/modal-component.js
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+define([
+ 'jquery',
+ 'Magento_Ui/js/modal/modal-component',
+ 'Magento_Ui/js/modal/alert',
+ 'mage/translate'
+], function ($, Modal, alert, $t) {
+ 'use strict';
+
+ return Modal.extend({
+ defaults: {
+ postponeOptions: {},
+ imports: {
+ postponeUrl: '${ $.provider }:postpone_url'
+ },
+ modules: {
+ form: '${ $.parentName }'
+ }
+ },
+
+ /**
+ * Send request to postpone modal appearance for a certain time.
+ *
+ * @param {Object} options - additional request options.
+ */
+ sendPostponeRequest: function (options) {
+ var self = this,
+ data = $.extend(this.form().source.data, options);
+
+ $.ajax({
+ type: 'POST',
+ url: this.postponeUrl,
+ data: data,
+ showLoader: true
+ }).done(function (xhr) {
+ if (xhr.error) {
+ self.onError(xhr);
+ }
+ }).fail(this.onError);
+ },
+
+ /**
+ * Error handler.
+ *
+ * @param {Object} xhr - request result.
+ */
+ onError: function (xhr) {
+ if (xhr.statusText === 'abort') {
+ return;
+ }
+
+ alert({
+ content: xhr.message || $t('An error occurred while subscription process.')
+ });
+ },
+
+ /** @inheritdoc */
+ actionCancel: function () {
+ this.sendPostponeRequest(this.postponeOptions);
+ this.closeModal();
+ }
+ });
+});
diff --git a/app/code/Magento/Authorization/Model/Acl/AclRetriever.php b/app/code/Magento/Authorization/Model/Acl/AclRetriever.php
index d3a5714c93743..5b67748cd2e41 100644
--- a/app/code/Magento/Authorization/Model/Acl/AclRetriever.php
+++ b/app/code/Magento/Authorization/Model/Acl/AclRetriever.php
@@ -1,6 +1,6 @@
_resource = $resource;
$this->_groupFactory = $groupFactory;
$this->_roleFactory = $roleFactory;
+ $this->aclDataCache = $aclDataCache ?: ObjectManager::getInstance()->get(
+ \Magento\Framework\Acl\Data\CacheInterface::class
+ );
+ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+ $this->cacheKey = $cacheKey;
}
/**
@@ -49,12 +81,7 @@ public function __construct(
*/
public function populateAcl(\Magento\Framework\Acl $acl)
{
- $roleTableName = $this->_resource->getTableName('authorization_role');
- $connection = $this->_resource->getConnection();
-
- $select = $connection->select()->from($roleTableName)->order('tree_level');
-
- foreach ($connection->fetchAll($select) as $role) {
+ foreach ($this->getRolesArray() as $role) {
$parent = $role['parent_id'] > 0 ? $role['parent_id'] : null;
switch ($role['role_type']) {
case RoleGroup::ROLE_TYPE:
@@ -71,4 +98,28 @@ public function populateAcl(\Magento\Framework\Acl $acl)
}
}
}
+
+ /**
+ * Get application ACL roles array
+ *
+ * @return array
+ */
+ private function getRolesArray()
+ {
+ $rolesCachedData = $this->aclDataCache->load($this->cacheKey);
+ if ($rolesCachedData) {
+ return $this->serializer->unserialize($rolesCachedData);
+ }
+
+ $roleTableName = $this->_resource->getTableName('authorization_role');
+ $connection = $this->_resource->getConnection();
+
+ $select = $connection->select()
+ ->from($roleTableName)
+ ->order('tree_level');
+
+ $rolesArray = $connection->fetchAll($select);
+ $this->aclDataCache->save($this->serializer->serialize($rolesArray), $this->cacheKey);
+ return $rolesArray;
+ }
}
diff --git a/app/code/Magento/Authorization/Model/Acl/Loader/Rule.php b/app/code/Magento/Authorization/Model/Acl/Loader/Rule.php
index 2fa86b63ae504..8f89e00cfb22e 100644
--- a/app/code/Magento/Authorization/Model/Acl/Loader/Rule.php
+++ b/app/code/Magento/Authorization/Model/Acl/Loader/Rule.php
@@ -1,32 +1,69 @@
_resource = $resource;
$this->_rootResource = $rootResource;
+ $this->aclDataCache = $aclDataCache ?: ObjectManager::getInstance()->get(
+ \Magento\Framework\Acl\Data\CacheInterface::class
+ );
+ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
+ $this->cacheKey = $cacheKey;
}
/**
@@ -37,15 +74,7 @@ public function __construct(
*/
public function populateAcl(\Magento\Framework\Acl $acl)
{
- $ruleTable = $this->_resource->getTableName("authorization_rule");
-
- $connection = $this->_resource->getConnection();
-
- $select = $connection->select()->from(['r' => $ruleTable]);
-
- $rulesArr = $connection->fetchAll($select);
-
- foreach ($rulesArr as $rule) {
+ foreach ($this->getRulesArray() as $rule) {
$role = $rule['role_id'];
$resource = $rule['resource_id'];
$privileges = !empty($rule['privileges']) ? explode(',', $rule['privileges']) : null;
@@ -62,4 +91,28 @@ public function populateAcl(\Magento\Framework\Acl $acl)
}
}
}
+
+ /**
+ * Get application ACL rules array.
+ *
+ * @return array
+ */
+ private function getRulesArray()
+ {
+ $rulesCachedData = $this->aclDataCache->load($this->cacheKey);
+ if ($rulesCachedData) {
+ return $this->serializer->unserialize($rulesCachedData);
+ }
+
+ $ruleTable = $this->_resource->getTableName("authorization_rule");
+ $connection = $this->_resource->getConnection();
+ $select = $connection->select()
+ ->from(['r' => $ruleTable]);
+
+ $rulesArr = $connection->fetchAll($select);
+
+ $this->aclDataCache->save($this->serializer->serialize($rulesArr), $this->cacheKey);
+
+ return $rulesArr;
+ }
}
diff --git a/app/code/Magento/Authorization/Model/Acl/Role/Generic.php b/app/code/Magento/Authorization/Model/Acl/Role/Generic.php
index f35a755b74045..b6a92d5ccae27 100644
--- a/app/code/Magento/Authorization/Model/Acl/Role/Generic.php
+++ b/app/code/Magento/Authorization/Model/Acl/Role/Generic.php
@@ -1,6 +1,6 @@
_aclBuilder = $aclBuilder;
parent::__construct($context, $connectionName);
$this->_rootResource = $rootResource;
$this->_aclCache = $aclCache;
$this->_logger = $logger;
+ $this->aclDataCache = $aclDataCache ?: ObjectManager::getInstance()->get(
+ \Magento\Framework\Acl\Data\CacheInterface::class
+ );
}
/**
@@ -79,8 +92,8 @@ protected function _construct()
*/
public function saveRel(\Magento\Authorization\Model\Rules $rule)
{
+ $connection = $this->getConnection();
try {
- $connection = $this->getConnection();
$connection->beginTransaction();
$roleId = $rule->getRoleId();
@@ -118,7 +131,7 @@ public function saveRel(\Magento\Authorization\Model\Rules $rule)
}
$connection->commit();
- $this->_aclCache->clean();
+ $this->aclDataCache->clean();
} catch (\Magento\Framework\Exception\LocalizedException $e) {
$connection->rollBack();
throw $e;
diff --git a/app/code/Magento/Authorization/Model/ResourceModel/Rules/Collection.php b/app/code/Magento/Authorization/Model/ResourceModel/Rules/Collection.php
index e4bb8f1195322..9aec756931868 100644
--- a/app/code/Magento/Authorization/Model/ResourceModel/Rules/Collection.php
+++ b/app/code/Magento/Authorization/Model/ResourceModel/Rules/Collection.php
@@ -1,6 +1,6 @@
_resourceMock = $this->getMock(
@@ -57,51 +72,82 @@ protected function setUp()
false
);
- $this->_resourceMock->expects(
- $this->once()
- )->method(
- 'getTableName'
- )->with(
- $this->equalTo('authorization_role')
- )->will(
- $this->returnArgument(1)
- );
-
- $selectMock = $this->getMock(\Magento\Framework\DB\Select::class, [], [], '', false);
- $selectMock->expects($this->any())->method('from')->will($this->returnValue($selectMock));
+ $this->selectMock = $this->getMock(\Magento\Framework\DB\Select::class, [], [], '', false);
+ $this->selectMock->expects($this->any())
+ ->method('from')
+ ->will($this->returnValue($this->selectMock));
$this->_adapterMock = $this->getMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, [], [], '', false);
- $this->_adapterMock->expects($this->once())->method('select')->will($this->returnValue($selectMock));
-
- $this->_resourceMock->expects(
- $this->once()
- )->method(
- 'getConnection'
- )->will(
- $this->returnValue($this->_adapterMock)
+
+ $this->serializerMock = $this->getMock(
+ \Magento\Framework\Serialize\Serializer\Json::class,
+ ['serialize', 'unserialize'],
+ [],
+ '',
+ false
+ );
+ $this->serializerMock->expects($this->any())
+ ->method('serialize')
+ ->will(
+ $this->returnCallback(
+ function ($value) {
+ return json_encode($value);
+ }
+ )
+ );
+
+ $this->serializerMock->expects($this->any())
+ ->method('unserialize')
+ ->will(
+ $this->returnCallback(
+ function ($value) {
+ return json_decode($value, true);
+ }
+ )
+ );
+
+ $this->aclDataCacheMock = $this->getMock(
+ \Magento\Framework\Acl\Data\CacheInterface::class,
+ [],
+ [],
+ '',
+ false
);
$this->_model = new \Magento\Authorization\Model\Acl\Loader\Role(
$this->_groupFactoryMock,
$this->_roleFactoryMock,
- $this->_resourceMock
+ $this->_resourceMock,
+ $this->aclDataCacheMock,
+ $this->serializerMock
);
}
public function testPopulateAclAddsRolesAndTheirChildren()
{
- $this->_adapterMock->expects(
- $this->once()
- )->method(
- 'fetchAll'
- )->will(
- $this->returnValue(
- [
- ['role_id' => 1, 'role_type' => 'G', 'parent_id' => null],
- ['role_id' => 2, 'role_type' => 'U', 'parent_id' => 1, 'user_id' => 1],
- ]
- )
- );
+ $this->_resourceMock->expects($this->once())
+ ->method('getTableName')
+ ->with($this->equalTo('authorization_role'))
+ ->will($this->returnArgument(1));
+
+ $this->_adapterMock->expects($this->once())
+ ->method('select')
+ ->will($this->returnValue($this->selectMock));
+
+ $this->_resourceMock->expects($this->once())
+ ->method('getConnection')
+ ->will($this->returnValue($this->_adapterMock));
+
+ $this->_adapterMock->expects($this->once())
+ ->method('fetchAll')
+ ->will(
+ $this->returnValue(
+ [
+ ['role_id' => 1, 'role_type' => 'G', 'parent_id' => null],
+ ['role_id' => 2, 'role_type' => 'U', 'parent_id' => 1, 'user_id' => 1],
+ ]
+ )
+ );
$this->_groupFactoryMock->expects($this->once())->method('create')->with(['roleId' => '1']);
$this->_roleFactoryMock->expects($this->once())->method('create')->with(['roleId' => '2']);
@@ -115,13 +161,55 @@ public function testPopulateAclAddsRolesAndTheirChildren()
public function testPopulateAclAddsMultipleParents()
{
- $this->_adapterMock->expects(
- $this->once()
- )->method(
- 'fetchAll'
- )->will(
- $this->returnValue([['role_id' => 1, 'role_type' => 'U', 'parent_id' => 2, 'user_id' => 3]])
- );
+ $this->_resourceMock->expects($this->once())
+ ->method('getTableName')
+ ->with($this->equalTo('authorization_role'))
+ ->will($this->returnArgument(1));
+
+ $this->_adapterMock->expects($this->once())
+ ->method('select')
+ ->will($this->returnValue($this->selectMock));
+
+ $this->_resourceMock->expects($this->once())
+ ->method('getConnection')
+ ->will($this->returnValue($this->_adapterMock));
+
+ $this->_adapterMock->expects($this->once())
+ ->method('fetchAll')
+ ->will($this->returnValue([['role_id' => 1, 'role_type' => 'U', 'parent_id' => 2, 'user_id' => 3]]));
+
+ $this->_roleFactoryMock->expects($this->never())->method('getModelInstance');
+ $this->_groupFactoryMock->expects($this->never())->method('getModelInstance');
+
+ $aclMock = $this->getMock(\Magento\Framework\Acl::class);
+ $aclMock->expects($this->at(0))->method('hasRole')->with('1')->will($this->returnValue(true));
+ $aclMock->expects($this->at(1))->method('addRoleParent')->with('1', '2');
+
+ $this->_model->populateAcl($aclMock);
+ }
+
+ public function testPopulateAclFromCache()
+ {
+ $this->_resourceMock->expects($this->never())->method('getConnection');
+ $this->_resourceMock->expects($this->never())->method('getTableName');
+ $this->_adapterMock->expects($this->never())->method('fetchAll');
+ $this->aclDataCacheMock->expects($this->once())
+ ->method('load')
+ ->with(\Magento\Authorization\Model\Acl\Loader\Role::ACL_ROLES_CACHE_KEY)
+ ->will(
+ $this->returnValue(
+ json_encode(
+ [
+ [
+ 'role_id' => 1,
+ 'role_type' => 'U',
+ 'parent_id' => 2,
+ 'user_id' => 3
+ ]
+ ]
+ )
+ )
+ );
$this->_roleFactoryMock->expects($this->never())->method('getModelInstance');
$this->_groupFactoryMock->expects($this->never())->method('getModelInstance');
diff --git a/app/code/Magento/Authorization/Test/Unit/Model/Acl/Loader/RuleTest.php b/app/code/Magento/Authorization/Test/Unit/Model/Acl/Loader/RuleTest.php
index f5f3b9b6a4b9f..2cf4b6d997fee 100644
--- a/app/code/Magento/Authorization/Test/Unit/Model/Acl/Loader/RuleTest.php
+++ b/app/code/Magento/Authorization/Test/Unit/Model/Acl/Loader/RuleTest.php
@@ -1,6 +1,6 @@
_resourceMock = $this->getMock(
@@ -32,39 +42,71 @@ protected function setUp()
false,
false
);
+ $this->serializerMock = $this->getMock(
+ \Magento\Framework\Serialize\Serializer\Json::class,
+ ['serialize', 'unserialize'],
+ [],
+ '',
+ false
+ );
+ $this->serializerMock->expects($this->any())
+ ->method('serialize')
+ ->will(
+ $this->returnCallback(
+ function ($value) {
+ return json_encode($value);
+ }
+ )
+ );
+
+ $this->serializerMock->expects($this->any())
+ ->method('unserialize')
+ ->will(
+ $this->returnCallback(
+ function ($value) {
+ return json_decode($value, true);
+ }
+ )
+ );
+
+ $this->aclDataCacheMock = $this->getMock(
+ \Magento\Framework\Acl\Data\CacheInterface::class,
+ [],
+ [],
+ '',
+ false
+ );
+
$this->_rootResourceMock = new \Magento\Framework\Acl\RootResource('Magento_Backend::all');
$this->_model = new \Magento\Authorization\Model\Acl\Loader\Rule(
$this->_rootResourceMock,
- $this->_resourceMock
+ $this->_resourceMock,
+ [],
+ $this->aclDataCacheMock,
+ $this->serializerMock
);
}
- public function testPopulateAcl()
+ public function testPopulateAclFromCache()
{
- $this->_resourceMock->expects($this->any())->method('getTable')->will($this->returnArgument(1));
-
- $selectMock = $this->getMock(\Magento\Framework\DB\Select::class, [], [], '', false);
- $selectMock->expects($this->any())->method('from')->will($this->returnValue($selectMock));
-
- $connectionMock = $this->getMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, [], [], '', false);
- $connectionMock->expects($this->once())->method('select')->will($this->returnValue($selectMock));
- $connectionMock->expects(
- $this->once()
- )->method(
- 'fetchAll'
- )->will(
- $this->returnValue(
- [
- ['role_id' => 1, 'resource_id' => 'Magento_Backend::all', 'permission' => 'allow'],
- ['role_id' => 2, 'resource_id' => 1, 'permission' => 'allow'],
- ['role_id' => 3, 'resource_id' => 1, 'permission' => 'deny'],
- ]
- )
- );
+ $this->_resourceMock->expects($this->never())->method('getTable');
+ $this->_resourceMock->expects($this->never())
+ ->method('getConnection');
- $this->_resourceMock->expects($this->once())
- ->method('getConnection')
- ->will($this->returnValue($connectionMock));
+ $this->aclDataCacheMock->expects($this->once())
+ ->method('load')
+ ->with(\Magento\Authorization\Model\Acl\Loader\Rule::ACL_RULE_CACHE_KEY)
+ ->will(
+ $this->returnValue(
+ json_encode(
+ [
+ ['role_id' => 1, 'resource_id' => 'Magento_Backend::all', 'permission' => 'allow'],
+ ['role_id' => 2, 'resource_id' => 1, 'permission' => 'allow'],
+ ['role_id' => 3, 'resource_id' => 1, 'permission' => 'deny'],
+ ]
+ )
+ )
+ );
$aclMock = $this->getMock(\Magento\Framework\Acl::class);
$aclMock->expects($this->any())->method('has')->will($this->returnValue(true));
diff --git a/app/code/Magento/Authorization/Test/Unit/Model/CompositeUserContextTest.php b/app/code/Magento/Authorization/Test/Unit/Model/CompositeUserContextTest.php
index 3724a97a08cd2..d899194bc9e95 100644
--- a/app/code/Magento/Authorization/Test/Unit/Model/CompositeUserContextTest.php
+++ b/app/code/Magento/Authorization/Test/Unit/Model/CompositeUserContextTest.php
@@ -1,6 +1,6 @@
contextMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\Context::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getResources'])
+ ->getMock();
+
+ $this->resourceConnectionMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getConnection', 'getTableName'])
+ ->getMock();
+
+ $this->contextMock->expects($this->once())
+ ->method('getResources')
+ ->will($this->returnValue($this->resourceConnectionMock));
+
+ $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->with('connection')
+ ->will($this->returnValue($this->connectionMock));
+
+ $this->resourceConnectionMock->expects($this->any())
+ ->method('getTableName')
+ ->with('authorization_rule', 'connection')
+ ->will($this->returnArgument(0));
+
+ $this->aclBuilderMock = $this->getMockBuilder(\Magento\Framework\Acl\Builder::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getConfigCache'])
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->rootResourceMock = $this->getMockBuilder(\Magento\Framework\Acl\RootResource::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->aclCacheMock = $this->getMockBuilder(\Magento\Framework\Acl\CacheInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->aclDataCacheMock = $this->getMockBuilder(\Magento\Framework\Acl\Data\CacheInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->aclBuilderMock->expects($this->any())
+ ->method('getConfigCache')
+ ->will($this->returnValue($this->aclDataCacheMock));
+
+ $this->ruleMock = $this->getMockBuilder(\Magento\Authorization\Model\Rules::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getRoleId'])
+ ->getMock();
+
+ $this->ruleMock->expects($this->any())
+ ->method('getRoleId')
+ ->will($this->returnValue(self::TEST_ROLE_ID));
+
+ $this->model = new \Magento\Authorization\Model\ResourceModel\Rules(
+ $this->contextMock,
+ $this->aclBuilderMock,
+ $this->loggerMock,
+ $this->rootResourceMock,
+ $this->aclCacheMock,
+ 'connection',
+ $this->aclDataCacheMock
+ );
+ }
+
+ /**
+ * Test save with no resources posted.
+ */
+ public function testSaveRelNoResources()
+ {
+ $this->connectionMock->expects($this->once())
+ ->method('beginTransaction');
+ $this->connectionMock->expects($this->once())
+ ->method('delete')
+ ->with('authorization_rule', ['role_id = ?' => self::TEST_ROLE_ID]);
+ $this->connectionMock->expects($this->once())
+ ->method('commit');
+
+ $this->aclDataCacheMock->expects($this->once())
+ ->method('clean');
+
+ $this->model->saveRel($this->ruleMock);
+ }
+
+ /**
+ * Test LocalizedException throw case.
+ *
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage TestException
+ */
+ public function testLocalizedExceptionOccurance()
+ {
+ $exceptionPhrase = $this->getMockBuilder(\Magento\Framework\Phrase::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['render'])
+ ->getMock();
+
+ $exceptionPhrase->expects($this->any())->method('render')->will($this->returnValue('TestException'));
+
+ $exception = new \Magento\Framework\Exception\LocalizedException($exceptionPhrase);
+
+ $this->connectionMock->expects($this->once())
+ ->method('beginTransaction');
+
+ $this->connectionMock->expects($this->once())
+ ->method('delete')
+ ->with('authorization_rule', ['role_id = ?' => self::TEST_ROLE_ID])
+ ->will($this->throwException($exception));
+
+ $this->connectionMock->expects($this->once())->method('rollBack');
+
+ $this->model->saveRel($this->ruleMock);
+ }
+
+ /**
+ * Test generic exception throw case.
+ */
+ public function testGenericExceptionOccurance()
+ {
+ $exception = new \Exception('GenericException');
+
+ $this->connectionMock->expects($this->once())
+ ->method('beginTransaction');
+
+ $this->connectionMock->expects($this->once())
+ ->method('delete')
+ ->with('authorization_rule', ['role_id = ?' => self::TEST_ROLE_ID])
+ ->will($this->throwException($exception));
+
+ $this->connectionMock->expects($this->once())->method('rollBack');
+ $this->loggerMock->expects($this->once())->method('critical')->with($exception);
+
+ $this->model->saveRel($this->ruleMock);
+ }
+}
diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json
index 0ca367d4854df..af88e8376dc75 100644
--- a/app/code/Magento/Authorization/composer.json
+++ b/app/code/Magento/Authorization/composer.json
@@ -2,7 +2,7 @@
"name": "magento/module-authorization",
"description": "Authorization module provides access to Magento ACL functionality.",
"require": {
- "php": "~5.6.0|7.0.2|7.0.4|~7.0.6",
+ "php": "~5.6.5|7.0.2|7.0.4|~7.0.6",
"magento/module-backend": "100.2.*",
"magento/framework": "100.2.*"
},
diff --git a/app/code/Magento/Authorization/etc/di.xml b/app/code/Magento/Authorization/etc/di.xml
index 1159fb0a7606e..9370e14d81a35 100644
--- a/app/code/Magento/Authorization/etc/di.xml
+++ b/app/code/Magento/Authorization/etc/di.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorization/etc/module.xml b/app/code/Magento/Authorization/etc/module.xml
index f3fa8793f3e57..f27253870499d 100644
--- a/app/code/Magento/Authorization/etc/module.xml
+++ b/app/code/Magento/Authorization/etc/module.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorization/registration.php b/app/code/Magento/Authorization/registration.php
index 6aabf5a16bb91..f35b17a4d6710 100644
--- a/app/code/Magento/Authorization/registration.php
+++ b/app/code/Magento/Authorization/registration.php
@@ -1,6 +1,6 @@
dataFactory->create($area);
$params = [];
- $data = $this->getRequest()->getPostValue();
+ $data = $this->getRequest()->getParams();
+
/* @var $paymentMethod \Magento\Authorizenet\Model\DirectPost */
$paymentMethod = $this->_objectManager->create(\Magento\Authorizenet\Model\Directpost::class);
@@ -110,9 +111,8 @@ protected function _responseAction($area = 'frontend')
$params['redirect'] = $helper->getRedirectIframeUrl($result);
}
+ //registering parameter for iframe content
$this->_coreRegistry->register(Iframe::REGISTRY_KEY, $params);
- $this->_view->addPageLayoutHandles();
- $this->_view->loadLayout(false)->renderLayout();
}
/**
diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php
index c0ac07a11adb3..6e2401d930b16 100644
--- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php
+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php
@@ -1,7 +1,7 @@
_responseAction('adminhtml');
+ return $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);
}
}
diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php
index 2816a3bae78b9..bc327caffb262 100644
--- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php
+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php
@@ -1,6 +1,6 @@
eventManager = $context->getEventManager();
$this->cartManagement = $cartManagement;
$this->onepageCheckout = $onepageCheckout;
$this->jsonHelper = $jsonHelper;
+ $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class);
parent::__construct($context, $coreRegistry, $dataFactory);
}
@@ -127,9 +139,11 @@ protected function placeCheckoutOrder()
]
);
} catch (LocalizedException $exception) {
+ $this->logger->critical($exception);
$result->setData('error', true);
$result->setData('error_messages', $exception->getMessage());
} catch (\Exception $exception) {
+ $this->logger->critical($exception);
$result->setData('error', true);
$result->setData(
'error_messages',
diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php
index 8745737a19bd6..84d91dc89f155 100644
--- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php
+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php
@@ -1,7 +1,7 @@
_responseAction('frontend');
+ return $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);
}
}
diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php
index 5bfe916160967..c542c16e9fa7e 100644
--- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php
+++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php
@@ -1,7 +1,7 @@
setXCity($billing->getCity())
->setXState($billing->getRegion())
->setXZip($billing->getPostcode())
- ->setXCountry($billing->getCountry())
+ ->setXCountry($billing->getCountryId())
->setXPhone($billing->getTelephone())
->setXFax($billing->getFax())
->setXCustId($order->getCustomerId())
@@ -352,7 +352,7 @@ protected function buildRequest(\Magento\Framework\DataObject $payment)
->setXShipToCity($shipping->getCity())
->setXShipToState($shipping->getRegion())
->setXShipToZip($shipping->getPostcode())
- ->setXShipToCountry($shipping->getCountry());
+ ->setXShipToCountry($shipping->getCountryId());
}
$request->setXPoNum($payment->getPoNumber())
diff --git a/app/code/Magento/Authorizenet/Model/Debug.php b/app/code/Magento/Authorizenet/Model/Debug.php
index 3e46731197660..7605aa762a183 100644
--- a/app/code/Magento/Authorizenet/Model/Debug.php
+++ b/app/code/Magento/Authorizenet/Model/Debug.php
@@ -1,6 +1,6 @@
setXCity(strval($billing->getCity()))
->setXState(strval($billing->getRegion()))
->setXZip(strval($billing->getPostcode()))
- ->setXCountry(strval($billing->getCountry()))
+ ->setXCountry(strval($billing->getCountryId()))
->setXPhone(strval($billing->getTelephone()))
->setXFax(strval($billing->getFax()))
->setXCustId(strval($billing->getCustomerId()))
@@ -151,7 +151,7 @@ public function setDataFromOrder(
)->setXShipToZip(
strval($shipping->getPostcode())
)->setXShipToCountry(
- strval($shipping->getCountry())
+ strval($shipping->getCountryId())
);
}
diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php
index 7ef6647285616..d0f6ea6515d11 100644
--- a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php
+++ b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php
@@ -1,6 +1,6 @@
getMockBuilder(\Magento\Framework\App\Response\Http::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
+ $this->loggerMock = $this
+ ->getMockBuilder(\Psr\Log\LoggerInterface::class)
+ ->getMock();
$this->objectManager = new ObjectManager($this);
$this->placeOrderController = $this->objectManager->getObject(
@@ -165,6 +174,7 @@ protected function setUp()
'cartManagement' => $this->cartManagementMock,
'onepageCheckout' => $this->onepageCheckout,
'jsonHelper' => $this->jsonHelperMock,
+ 'logger' => $this->loggerMock,
]
);
}
@@ -214,13 +224,15 @@ public function testExecute(
* @param $controller
* @param $quoteId
* @param $result
+ * @param \Exception $exception Exception to check
* @dataProvider textExecuteFailedPlaceOrderDataProvider
*/
public function testExecuteFailedPlaceOrder(
$paymentMethod,
$controller,
$quoteId,
- $result
+ $result,
+ $exception
) {
$this->requestMock->expects($this->at(0))
->method('getParam')
@@ -238,7 +250,11 @@ public function testExecuteFailedPlaceOrder(
$this->cartManagementMock->expects($this->once())
->method('placeOrder')
- ->willThrowException(new \Exception());
+ ->willThrowException($exception);
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with($exception);
$this->jsonHelperMock->expects($this->any())
->method('jsonEncode')
@@ -278,11 +294,19 @@ public function textExecuteDataProvider()
*/
public function textExecuteFailedPlaceOrderDataProvider()
{
- $objectFailed = new \Magento\Framework\DataObject();
- $objectFailed->setData('error', true);
- $objectFailed->setData(
- 'error_messages',
- __('An error occurred on the server. Please try to place the order again.')
+ $objectFailed1 = new \Magento\Framework\DataObject(
+ [
+ 'error' => true,
+ 'error_messages' => __('An error occurred on the server. Please try to place the order again.')
+ ]
+ );
+ $generalException = new \Exception('Exception logging will save the world!');
+ $localizedException = new LocalizedException(__('Electronic payments save the trees.'));
+ $objectFailed2 = new \Magento\Framework\DataObject(
+ [
+ 'error' => true,
+ 'error_messages' => $localizedException->getMessage()
+ ]
);
return [
@@ -290,7 +314,15 @@ public function textExecuteFailedPlaceOrderDataProvider()
['method' => 'authorizenet_directpost'],
IframeConfigProvider::CHECKOUT_IDENTIFIER,
1,
- $objectFailed
+ $objectFailed1,
+ $generalException,
+ ],
+ [
+ ['method' => 'authorizenet_directpost'],
+ IframeConfigProvider::CHECKOUT_IDENTIFIER,
+ 1,
+ $objectFailed2,
+ $localizedException,
],
];
}
diff --git a/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/RedirectTest.php b/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/RedirectTest.php
index d11a7b8df3521..4ec5e401c9e96 100644
--- a/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/RedirectTest.php
+++ b/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/RedirectTest.php
@@ -1,6 +1,6 @@
diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/events.xml b/app/code/Magento/Authorizenet/etc/adminhtml/events.xml
index 4a669cd7d9cac..8114b8c8312cc 100644
--- a/app/code/Magento/Authorizenet/etc/adminhtml/events.xml
+++ b/app/code/Magento/Authorizenet/etc/adminhtml/events.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/routes.xml b/app/code/Magento/Authorizenet/etc/adminhtml/routes.xml
index ef3e63f2c9c90..028e1a8500d07 100644
--- a/app/code/Magento/Authorizenet/etc/adminhtml/routes.xml
+++ b/app/code/Magento/Authorizenet/etc/adminhtml/routes.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml
index fd2fb84f0a290..3d4cde185dcd1 100644
--- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml
+++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml
index 70b413b5c1a39..f5b053003f1f2 100644
--- a/app/code/Magento/Authorizenet/etc/config.xml
+++ b/app/code/Magento/Authorizenet/etc/config.xml
@@ -1,7 +1,7 @@
@@ -10,7 +10,7 @@
0
- AE,VI,MC,DI
+ AE,VI,MC,DI,JCB,DN
0
0
diff --git a/app/code/Magento/Authorizenet/etc/di.xml b/app/code/Magento/Authorizenet/etc/di.xml
index f5e595fb450e8..3bd70f25a3bf9 100644
--- a/app/code/Magento/Authorizenet/etc/di.xml
+++ b/app/code/Magento/Authorizenet/etc/di.xml
@@ -1,7 +1,7 @@
@@ -16,4 +16,14 @@
Magento\Authorizenet\Model\Directpost\Session\Storage
+
+
+
+ - 1
+ - 1
+ - 1
+ - 1
+
+
+
diff --git a/app/code/Magento/Authorizenet/etc/frontend/di.xml b/app/code/Magento/Authorizenet/etc/frontend/di.xml
index 7f20e067cc426..8dcf8ed700dcb 100644
--- a/app/code/Magento/Authorizenet/etc/frontend/di.xml
+++ b/app/code/Magento/Authorizenet/etc/frontend/di.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/frontend/events.xml b/app/code/Magento/Authorizenet/etc/frontend/events.xml
index bb11f4bca363a..2c6e3f12a9196 100644
--- a/app/code/Magento/Authorizenet/etc/frontend/events.xml
+++ b/app/code/Magento/Authorizenet/etc/frontend/events.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/frontend/page_types.xml b/app/code/Magento/Authorizenet/etc/frontend/page_types.xml
index 6dc50d5d28dd9..be4692b135955 100644
--- a/app/code/Magento/Authorizenet/etc/frontend/page_types.xml
+++ b/app/code/Magento/Authorizenet/etc/frontend/page_types.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/frontend/routes.xml b/app/code/Magento/Authorizenet/etc/frontend/routes.xml
index 7da347a2e6381..1bdcff9f1efe1 100644
--- a/app/code/Magento/Authorizenet/etc/frontend/routes.xml
+++ b/app/code/Magento/Authorizenet/etc/frontend/routes.xml
@@ -1,7 +1,7 @@
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Authorizenet/etc/frontend/sections.xml b/app/code/Magento/Authorizenet/etc/frontend/sections.xml
index 33c48e60e8dab..977a4b14e3e14 100644
--- a/app/code/Magento/Authorizenet/etc/frontend/sections.xml
+++ b/app/code/Magento/Authorizenet/etc/frontend/sections.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/etc/module.xml b/app/code/Magento/Authorizenet/etc/module.xml
index 75317e254efaa..91d93e56e0cad 100644
--- a/app/code/Magento/Authorizenet/etc/module.xml
+++ b/app/code/Magento/Authorizenet/etc/module.xml
@@ -1,7 +1,7 @@
diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv
index 7183c706dc0a2..45025304c4e44 100644
--- a/app/code/Magento/Authorizenet/i18n/en_US.csv
+++ b/app/code/Magento/Authorizenet/i18n/en_US.csv
@@ -64,3 +64,4 @@ Debug,Debug
"Minimum Order Total","Minimum Order Total"
"Maximum Order Total","Maximum Order Total"
"Sort Order","Sort Order"
+"Sorry, but something went wrong. Please contact the seller.","Sorry, but something went wrong. Please contact the seller."
diff --git a/app/code/Magento/Authorizenet/registration.php b/app/code/Magento/Authorizenet/registration.php
index a0eab4323313e..ad98beafa744d 100644
--- a/app/code/Magento/Authorizenet/registration.php
+++ b/app/code/Magento/Authorizenet/registration.php
@@ -1,6 +1,6 @@
diff --git a/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_index.xml
index aeed400c3c8fa..851cbf398750d 100644
--- a/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_index.xml
+++ b/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_index.xml
@@ -1,7 +1,7 @@
@@ -14,4 +14,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml b/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml
index 21e8f1d70fbb5..ec5ce845b8521 100644
--- a/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml
+++ b/app/code/Magento/Authorizenet/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml
@@ -1,7 +1,7 @@
@@ -14,4 +14,4 @@