diff --git a/.github/.htaccess b/.github/.htaccess index 93169e4eb44ff..76952375b2eba 100644 --- a/.github/.htaccess +++ b/.github/.htaccess @@ -1,2 +1,7 @@ -Order deny,allow -Deny from all + + Order deny,allow + Deny from all + += 2.4> + Require all denied + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2839ac5ee9d32..dae954a0970b7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,34 +1,34 @@ # Contributing to Magento 2 code Contributions to the Magento 2 codebase are done using the fork & pull model. -This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes (hence the phrase “pull request”). +This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes. For more information on pull requests please refer to [GitHub Help](https://help.github.com/articles/about-pull-requests/). -Contributions can take the form of new components/features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations or just good suggestions. +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 or optimizations. -The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor for two weeks, the issue is closed. +The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor within two weeks, the pull request will be closed. ## Contribution requirements -1. Contributions must adhere to [Magento coding standards](http://devdocs.magento.com/guides/v2.0/coding-standards/bk-coding-standards.html). -2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request to be merged quickly and without additional clarification requests. -3. Commits must be accompanied by meaningful commit messages. -4. PRs which include bug fixing, must be accompanied with step-by-step description of how to reproduce the bug. +1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.2/coding-standards/bk-coding-standards.html). +2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests. +3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/2.2-develop/.github/PULL_REQUEST_TEMPLATE.md) for more information. +4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. 3. PRs which include new logic or new features must be submitted along with: -* Unit/integration test coverage (we will be releasing more information on writing test coverage in the near future). -* Proposed [documentation](http://devdocs.magento.com) update. Documentation contributions can be submitted [here](https://github.com/magento/devdocs). -4. For large features or changes, please [open an issue](https://github.com/magento/magento2/issues) and discuss first. This may prevent duplicate or unnecessary effort, and it may gain you some additional contributors. -5. All automated tests are passed successfully (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). +* Unit/integration test coverage +* Proposed [documentation](http://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). +4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. +5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). ## Contribution process -If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). By doing that, you will be able to collaborate with the Magento 2 development team, “fork” the Magento 2 project and be able to easily send “pull requests”. +If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). This will allow you to collaborate with the Magento 2 development team, fork the Magento 2 project and send pull requests. 1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. 2. Review the [Contributor License Agreement](https://magento.com/legaldocuments/mca) if this is your first time contributing. 3. Create and test your work. -4. Fork the Magento 2 repository according to [Fork a repository instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow [Create a pull request instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#pull_request). -5. Once your contribution is received, Magento 2 development team will review the contribution and collaborate with you as needed to improve the quality of the contribution. +4. Fork the Magento 2 repository according to the [Fork A Repository instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#pull_request). +5. Once your contribution is received the Magento 2 development team will review the contribution and collaborate with you as needed. ## Code of Conduct diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3ac68076d4353..12ad4e452b1c7 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,24 +1,35 @@ - - + ### Preconditions - - + 1. 2. ### Steps to reproduce - + 1. 2. 3. ### Expected result -1. +1. [Screenshots, logs or description] ### Actual result -1. [Screenshot, logs] - - +1. [Screenshots, logs or description] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1f01ba9f2640..5b0b9d74e453b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,32 @@ - + + + ### Description - + ### Fixed Issues (if relevant) - + 1. magento/magento2#: Issue title 2. ... ### Manual testing scenarios - + 1. ... 2. ... diff --git a/.gitignore b/.gitignore index 94c3bf76a2bd1..8831ad0a17c39 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ atlassian* /.php_cs.cache /grunt-config.json /dev/tools/grunt/configs/local-themes.js - /pub/media/*.* !/pub/media/.htaccess /pub/media/attribute/* diff --git a/.htaccess b/.htaccess index 3e6eb31938e46..d22b5a1395cae 100644 --- a/.htaccess +++ b/.htaccess @@ -36,7 +36,7 @@ ############################################ ## adjust memory limit - php_value memory_limit 768M + php_value memory_limit 756M php_value max_execution_time 18000 ############################################ @@ -59,7 +59,7 @@ ############################################ ## adjust memory limit - php_value memory_limit 768M + php_value memory_limit 756M php_value max_execution_time 18000 ############################################ @@ -203,72 +203,166 @@ RedirectMatch 403 /\.git - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + + + + + order allow,deny + deny from all + + = 2.4> + Require all denied + # For 404s and 403s that aren't handled by the application, show plain 404 response diff --git a/.htaccess.sample b/.htaccess.sample index e70eb9d022412..c9ddff2cca4cf 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -35,7 +35,7 @@ ############################################ ## adjust memory limit - php_value memory_limit 768M + php_value memory_limit 756M php_value max_execution_time 18000 ############################################ @@ -111,7 +111,8 @@ ############################################ ## enable rewrites - Options +FollowSymLinks + # The following line has better security but add some performance overhead - see https://httpd.apache.org/docs/2.4/en/misc/perf-tuning.html + Options -FollowSymLinks +SymLinksIfOwnerMatch RewriteEngine on ############################################ @@ -179,72 +180,166 @@ RedirectMatch 403 /\.git - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + - order allow,deny - deny from all + + order allow,deny + deny from all + + = 2.4> + Require all denied + + + + + order allow,deny + deny from all + + = 2.4> + Require all denied + # For 404s and 403s that aren't handled by the application, show plain 404 response diff --git a/.php_cs.dist b/.php_cs.dist index 0f254c63283bd..84a5f88bf4355 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -5,9 +5,9 @@ */ /** - * Pre-commit hook installation: - * vendor/bin/static-review.php hook:install dev/tools/Magento/Tools/StaticReview/pre-commit .git/hooks/pre-commit + * PHP Coding Standards fixer configuration */ + $finder = PhpCsFixer\Finder::create() ->name('*.phtml') ->exclude('dev/tests/functional/generated') diff --git a/.travis.yml b/.travis.yml index dcd00f39bb810..cc730ca5a2cd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,15 +11,18 @@ addons: firefox: "46.0" hosts: - magento2.travis +services: + - rabbitmq + - elasticsearch language: php php: - - 7.0 - 7.1 + - 7.2 env: global: - COMPOSER_BIN_DIR=~/bin - INTEGRATION_SETS=3 - - NODE_JS_VERSION=6 + - NODE_JS_VERSION=8 - MAGENTO_HOST_NAME="magento2.travis" matrix: - TEST_SUITE=unit @@ -32,13 +35,13 @@ env: - TEST_SUITE=functional matrix: exclude: - - php: 7.0 + - php: 7.1 env: TEST_SUITE=static - - php: 7.0 + - php: 7.1 env: TEST_SUITE=js GRUNT_COMMAND=spec - - php: 7.0 + - php: 7.1 env: TEST_SUITE=js GRUNT_COMMAND=static - - php: 7.0 + - php: 7.1 env: TEST_SUITE=functional cache: apt: true @@ -47,7 +50,9 @@ cache: - $HOME/.nvm - $HOME/node_modules - $HOME/yarn.lock -before_install: ./dev/travis/before_install.sh +before_install: + - curl -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.3.0/elasticsearch-2.3.0.deb && sudo dpkg -i --force-confnew elasticsearch-2.3.0.deb && sudo service elasticsearch restart + - ./dev/travis/before_install.sh install: composer install --no-interaction before_script: ./dev/travis/before_script.sh script: diff --git a/.user.ini b/.user.ini index 8c0b765e0551c..bfc3a86d88e20 100644 --- a/.user.ini +++ b/.user.ini @@ -1,4 +1,4 @@ -memory_limit = 768M +memory_limit = 756M max_execution_time = 18000 session.auto_start = off suhosin.session.cryptua = off \ No newline at end of file diff --git a/COPYING.txt b/COPYING.txt index d2cbcd01539dd..040bdd5f3ce72 100644 --- a/COPYING.txt +++ b/COPYING.txt @@ -1,4 +1,4 @@ -Copyright © 2013-2017 Magento, Inc. +Copyright © 2013-present Magento, Inc. Each Magento source file included in this distribution is licensed under OSL 3.0 or the Magento Enterprise Edition (MEE) license diff --git a/README.md b/README.md index 9b1aa1b7b3e28..a2cf536bb6520 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -[![Build Status](https://travis-ci.org/magento/magento2.svg?branch=develop)](https://travis-ci.org/magento/magento2) +[![Build Status](https://travis-ci.org/magento/magento2.svg?branch=2.3-develop)](https://travis-ci.org/magento/magento2) +[![Open Source Helpers](https://www.codetriage.com/magento/magento2/badges/users.svg)](https://www.codetriage.com/magento/magento2) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/magento/magento2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/magento-2/localized.png)](https://crowdin.com/project/magento-2)

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. +Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting-edge, feature-rich eCommerce solution that gets results. ## Magento system requirements -[Magento system requirements](http://devdocs.magento.com/magento-system-requirements.html) +[Magento system requirements](http://devdocs.magento.com/guides/v2.3/install-gde/system-requirements2.html) ## Install Magento To install Magento, see either: -* [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) +* [Installation guide](http://devdocs.magento.com/guides/v2.3/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. @@ -22,11 +22,24 @@ To learn about issues, click [here][2]. To open an issue, click [here][3]. To suggest documentation improvements, click [here][4]. -[1]: -[2]: +[1]: +[2]: [3]: [4]: +

Community Maintainers

+The members of this team have been recognized for their outstanding commitment to maintaining and improving Magento. Magento has granted them permission to accept, merge, and reject pull requests, as well as review issues, and thanks these Community Maintainers for their valuable contributions. + + + + + +

Top Contributors

+Magento is thankful for any contribution that can improve our code base, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform. + + + +

Labels applied by the Magento team

| Label | Description | @@ -38,8 +51,10 @@ To suggest documentation improvements, click [here][4]. | ![reject](http://devdocs.magento.com/common/images/github_reject.png) | The pull request has been rejected and will not be merged into mainline code. Possible reasons can include but are not limited to: issue has already been fixed in another code contribution, or there is an issue with the code contribution. | | ![bug report](http://devdocs.magento.com/common/images/github_bug.png) | The Magento Team has confirmed that this issue contains the minimum required information to reproduce. | | ![acknowledged](http://devdocs.magento.com/common/images/gitHub_acknowledged.png) | The Magento Team has validated the issue and an internal ticket has been created. | -| ![acknowledged](http://devdocs.magento.com/common/images/github_inProgress.png) | The internal ticket is currently in progress, fix is scheduled to be delivered. | -| ![acknowledged](http://devdocs.magento.com/common/images/github_needsUpdate.png) | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. | +| ![in progress](http://devdocs.magento.com/common/images/github_inProgress.png) | The internal ticket is currently in progress, fix is scheduled to be delivered. | +| ![needs update](http://devdocs.magento.com/common/images/github_needsUpdate.png) | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. | + +To learn more about issue gate labels click [here](https://github.com/magento/magento2/wiki/Magento-Issue-Gates)

Reporting security issues

diff --git a/app/.htaccess b/app/.htaccess index 93169e4eb44ff..707c26b075e16 100644 --- a/app/.htaccess +++ b/app/.htaccess @@ -1,2 +1,8 @@ -Order deny,allow -Deny from all + + order allow,deny + deny from all + += 2.4> + Require all denied + + diff --git a/app/bootstrap.php b/app/bootstrap.php index 6701a9f4dd51e..ba62b296bd49c 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -14,12 +14,12 @@ if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) { if (PHP_SAPI == 'cli') { echo 'Magento supports 7.0.2, 7.0.4, and 7.0.6 or later. ' . - 'Please read http://devdocs.magento.com/guides/v1.0/install-gde/system-requirements.html'; + 'Please read http://devdocs.magento.com/guides/v2.2/install-gde/system-requirements.html'; } else { echo <<

Magento supports PHP 7.0.2, 7.0.4, and 7.0.6 or later. Please read - + Magento System Requirements. HTML; @@ -49,12 +49,13 @@ unset($_SERVER['ORIG_PATH_INFO']); } -if (!empty($_SERVER['MAGE_PROFILER']) +if ( + (!empty($_SERVER['MAGE_PROFILER']) || file_exists(BP . '/var/profiler.flag')) && isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false ) { \Magento\Framework\Profiler::applyConfig( - $_SERVER['MAGE_PROFILER'], + (isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER'])) ? $_SERVER['MAGE_PROFILER'] : trim(file_get_contents(BP . '/var/profiler.flag')), BP, !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' ); diff --git a/app/code/Magento/AdminNotification/Block/System/Messages.php b/app/code/Magento/AdminNotification/Block/System/Messages.php index e95d68663bf04..b950f5583e599 100644 --- a/app/code/Magento/AdminNotification/Block/System/Messages.php +++ b/app/code/Magento/AdminNotification/Block/System/Messages.php @@ -16,24 +16,34 @@ class Messages extends \Magento\Backend\Block\Template /** * @var \Magento\Framework\Json\Helper\Data + * @deprecated */ protected $jsonHelper; + /** + * @var \Magento\Framework\Serialize\Serializer\Json + */ + private $serializer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\AdminNotification\Model\ResourceModel\System\Message\Collection\Synchronized $messages * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param array $data + * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\AdminNotification\Model\ResourceModel\System\Message\Collection\Synchronized $messages, \Magento\Framework\Json\Helper\Data $jsonHelper, - array $data = [] + array $data = [], + \Magento\Framework\Serialize\Serializer\Json $serializer = null ) { $this->jsonHelper = $jsonHelper; parent::__construct($context, $data); $this->_messages = $messages; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\Json::class); } /** @@ -117,7 +127,7 @@ protected function _getMessagesUrl() */ public function getSystemMessageDialogJson() { - return $this->jsonHelper->jsonEncode( + return $this->serializer->serialize( [ 'systemMessageDialog' => [ 'buttons' => [], diff --git a/app/code/Magento/AdminNotification/Block/System/Messages/UnreadMessagePopup.php b/app/code/Magento/AdminNotification/Block/System/Messages/UnreadMessagePopup.php index 7ea0062581467..2d4c7f279f707 100644 --- a/app/code/Magento/AdminNotification/Block/System/Messages/UnreadMessagePopup.php +++ b/app/code/Magento/AdminNotification/Block/System/Messages/UnreadMessagePopup.php @@ -77,9 +77,8 @@ public function getPopupTitle() $messageCount = count($this->_messages->getUnread()); if ($messageCount > 1) { return __('You have %1 new system messages', $messageCount); - } else { - return __('You have %1 new system message', $messageCount); } + return __('You have %1 new system message', $messageCount); } /** diff --git a/app/code/Magento/AdminNotification/Block/Window.php b/app/code/Magento/AdminNotification/Block/Window.php index b80e12a8674db..9563626ee2577 100644 --- a/app/code/Magento/AdminNotification/Block/Window.php +++ b/app/code/Magento/AdminNotification/Block/Window.php @@ -98,10 +98,9 @@ protected function _getLatestItem() { if ($this->_latestItem == null) { $items = array_values($this->_criticalCollection->getItems()); + $this->_latestItem = false; if (count($items)) { $this->_latestItem = $items[0]; - } else { - $this->_latestItem = false; } } return $this->_latestItem; diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php index 79f69ab5da88d..6b5e0681139cf 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php @@ -28,11 +28,11 @@ public function execute() )->markAsRead( $notificationId ); - $this->messageManager->addSuccess(__('The message has been marked as Read.')); + $this->messageManager->addSuccessMessage(__('The message has been marked as Read.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("We couldn't mark the notification as Read because of an error.") ); diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php index 9e61b8ff4b83c..9ae4a7cdac0b9 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php @@ -23,7 +23,7 @@ public function execute() { $ids = $this->getRequest()->getParam('notification'); if (!is_array($ids)) { - $this->messageManager->addError(__('Please select messages.')); + $this->messageManager->addErrorMessage(__('Please select messages.')); } else { try { foreach ($ids as $id) { @@ -32,13 +32,13 @@ public function execute() $model->setIsRead(1)->save(); } } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been marked as Read.', count($ids)) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("We couldn't mark the notification as Read because of an error.") ); diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php index 6c0dfd1db7d16..06659b8452cab 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php @@ -23,7 +23,7 @@ public function execute() { $ids = $this->getRequest()->getParam('notification'); if (!is_array($ids)) { - $this->messageManager->addError(__('Please select messages.')); + $this->messageManager->addErrorMessage(__('Please select messages.')); } else { try { foreach ($ids as $id) { @@ -32,13 +32,16 @@ public function execute() $model->setIsRemove(1)->save(); } } - $this->messageManager->addSuccess(__('Total of %1 record(s) have been removed.', count($ids))); + $this->messageManager->addSuccessMessage(__('Total of %1 record(s) have been removed.', count($ids))); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __("We couldn't remove the messages because of an error.")); + $this->messageManager->addExceptionMessage( + $e, + __("We couldn't remove the messages because of an error.") + ); } } - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); + $this->_redirect('adminhtml/*/'); } } diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php index 17f911339cb61..f0724a9587c50 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php @@ -31,11 +31,14 @@ public function execute() try { $model->setIsRemove(1)->save(); - $this->messageManager->addSuccess(__('The message has been removed.')); + $this->messageManager->addSuccessMessage(__('The message has been removed.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __("We couldn't remove the messages because of an error.")); + $this->messageManager->addExceptionMessage( + $e, + __("We couldn't remove the messages because of an error.") + ); } $this->_redirect('adminhtml/*/'); diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php index c332440276083..d58a7ec31f77d 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php @@ -6,6 +6,8 @@ */ namespace Magento\AdminNotification\Controller\Adminhtml\System\Message; +use Magento\Framework\Controller\ResultFactory; + class ListAction extends \Magento\Backend\App\AbstractAction { /** @@ -15,6 +17,7 @@ class ListAction extends \Magento\Backend\App\AbstractAction /** * @var \Magento\Framework\Json\Helper\Data + * @deprecated */ protected $jsonHelper; @@ -41,7 +44,7 @@ public function __construct( } /** - * @return void + * @return \Magento\Framework\Controller\Result\Json */ public function execute() { @@ -59,10 +62,15 @@ public function execute() if (empty($result)) { $result[] = [ 'severity' => (string)\Magento\Framework\Notification\MessageInterface::SEVERITY_NOTICE, - 'text' => 'You have viewed and resolved all recent system notices. ' - . 'Please refresh the web page to clear the notice alert.', + 'text' => __( + 'You have viewed and resolved all recent system notices. ' + . 'Please refresh the web page to clear the notice alert.' + ) ]; } - $this->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setData($result); + return $resultJson; } } diff --git a/app/code/Magento/AdminNotification/Model/Feed.php b/app/code/Magento/AdminNotification/Model/Feed.php index 1766425fb19b1..d3b0b8501c864 100644 --- a/app/code/Magento/AdminNotification/Model/Feed.php +++ b/app/code/Magento/AdminNotification/Model/Feed.php @@ -214,9 +214,6 @@ public function getFeedData() ); $curl->write(\Zend_Http_Client::GET, $this->getFeedUrl(), '1.0'); $data = $curl->read(); - if ($data === false) { - return false; - } $data = preg_split('/^\r?$/m', $data, 2); $data = trim($data[1]); $curl->close(); diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/System/Message.php b/app/code/Magento/AdminNotification/Model/ResourceModel/System/Message.php index c7e9d348f3aca..9d830274b004e 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/System/Message.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/System/Message.php @@ -12,7 +12,7 @@ class Message extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { /** - * Flag that notifies whether Primary key of table is auto-incremeted + * Flag that notifies whether Primary key of table is auto-incremented * * @var bool */ diff --git a/app/code/Magento/AdminNotification/Setup/InstallSchema.php b/app/code/Magento/AdminNotification/Setup/InstallSchema.php deleted file mode 100644 index 081be974dc809..0000000000000 --- a/app/code/Magento/AdminNotification/Setup/InstallSchema.php +++ /dev/null @@ -1,124 +0,0 @@ -startSetup(); - /** - * Create table 'adminnotification_inbox' - */ - $table = $installer->getConnection()->newTable( - $installer->getTable('adminnotification_inbox') - )->addColumn( - 'notification_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], - 'Notification id' - )->addColumn( - 'severity', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Problem type' - )->addColumn( - 'date_added', - \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, - null, - ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT], - 'Create date' - )->addColumn( - 'title', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 255, - ['nullable' => false], - 'Title' - )->addColumn( - 'description', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - '64k', - [], - 'Description' - )->addColumn( - 'url', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 255, - [], - 'Url' - )->addColumn( - 'is_read', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Flag if notification read' - )->addColumn( - 'is_remove', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Flag if notification might be removed' - )->addIndex( - $installer->getIdxName('adminnotification_inbox', ['severity']), - ['severity'] - )->addIndex( - $installer->getIdxName('adminnotification_inbox', ['is_read']), - ['is_read'] - )->addIndex( - $installer->getIdxName('adminnotification_inbox', ['is_remove']), - ['is_remove'] - )->setComment( - 'Adminnotification Inbox' - ); - $installer->getConnection()->createTable($table); - - /** - * Create table 'admin_system_messages' - */ - $table = $installer->getConnection()->newTable( - $installer->getTable('admin_system_messages') - )->addColumn( - 'identity', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 100, - ['nullable' => false, 'primary' => true], - 'Message id' - )->addColumn( - 'severity', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Problem type' - )->addColumn( - 'created_at', - \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, - null, - ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT], - 'Create date' - )->setComment( - 'Admin System Messages' - ); - $installer->getConnection()->createTable($table); - - $installer->endSetup(); - } -} diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json index b8dba6f899645..e5cf487908cd7 100644 --- a/app/code/Magento/AdminNotification/composer.json +++ b/app/code/Magento/AdminNotification/composer.json @@ -5,16 +5,15 @@ "sort-packages": true }, "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.1.3||~7.2.0", "lib-libxml": "*", - "magento/framework": "100.3.*", - "magento/module-backend": "100.3.*", - "magento/module-media-storage": "100.3.*", - "magento/module-store": "100.3.*", - "magento/module-ui": "100.3.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/AdminNotification/etc/db_schema.xml b/app/code/Magento/AdminNotification/etc/db_schema.xml new file mode 100644 index 0000000000000..35e6045b607d1 --- /dev/null +++ b/app/code/Magento/AdminNotification/etc/db_schema.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+
diff --git a/app/code/Magento/AdminNotification/etc/db_schema_whitelist.json b/app/code/Magento/AdminNotification/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..df5e14e066636 --- /dev/null +++ b/app/code/Magento/AdminNotification/etc/db_schema_whitelist.json @@ -0,0 +1,32 @@ +{ + "adminnotification_inbox": { + "column": { + "notification_id": true, + "severity": true, + "date_added": true, + "title": true, + "description": true, + "url": true, + "is_read": true, + "is_remove": true + }, + "index": { + "ADMINNOTIFICATION_INBOX_SEVERITY": true, + "ADMINNOTIFICATION_INBOX_IS_READ": true, + "ADMINNOTIFICATION_INBOX_IS_REMOVE": true + }, + "constraint": { + "PRIMARY": true + } + }, + "admin_system_messages": { + "column": { + "identity": true, + "severity": true, + "created_at": true + }, + "constraint": { + "PRIMARY": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/AdminNotification/etc/module.xml b/app/code/Magento/AdminNotification/etc/module.xml index 8a792ee8453ce..607ecbde10a26 100644 --- a/app/code/Magento/AdminNotification/etc/module.xml +++ b/app/code/Magento/AdminNotification/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/AdminNotification/i18n/en_US.csv b/app/code/Magento/AdminNotification/i18n/en_US.csv index 16c5abb9db0d2..db5a4c9254814 100644 --- a/app/code/Magento/AdminNotification/i18n/en_US.csv +++ b/app/code/Magento/AdminNotification/i18n/en_US.csv @@ -48,3 +48,4 @@ Severity,Severity "Date Added","Date Added" Message,Message Actions,Actions +"You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert.","You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert." diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index 7ddd5e3bb2a36..a92df095036f3 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -5,6 +5,7 @@ */ namespace Magento\AdvancedPricingImportExport\Model\Export; +use Magento\ImportExport\Model\Export; use Magento\Store\Model\Store; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing as ImportAdvancedPricing; @@ -79,6 +80,11 @@ class AdvancedPricing extends \Magento\CatalogImportExport\Model\Export\Product ImportAdvancedPricing::COL_TIER_PRICE_TYPE => '' ]; + /** + * @var string[] + */ + private $websiteCodesMap = []; + /** * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Eav\Model\Config $config @@ -255,36 +261,131 @@ public function filterAttributeCollection(\Magento\Eav\Model\ResourceModel\Entit */ protected function getExportData() { + if ($this->_passTierPrice) { + return []; + } + $exportData = []; try { - $rawData = $this->collectRawData(); - $productIds = array_keys($rawData); - if (isset($productIds)) { - if (!$this->_passTierPrice) { - $exportData = array_merge( - $exportData, - $this->getTierPrices($productIds, ImportAdvancedPricing::TABLE_TIER_PRICE) - ); + $productsByStores = $this->loadCollection(); + if (!empty($productsByStores)) { + $linkField = $this->getProductEntityLinkField(); + $productLinkIds = []; + + foreach ($productsByStores as $product) { + $productLinkIds[array_pop($product)[$linkField]] = true; + } + $productLinkIds = array_keys($productLinkIds); + $tierPricesData = $this->fetchTierPrices($productLinkIds); + $exportData = $this->prepareExportData( + $productsByStores, + $tierPricesData + ); + if (!empty($exportData)) { + asort($exportData); } } - if ($exportData) { - $exportData = $this->correctExportData($exportData); - } - if (isset($exportData)) { - asort($exportData); - } - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->_logger->critical($e); } + return $exportData; } + /** + * Creating export-formatted row from tier price. + * + * @param array $tierPriceData Tier price information. + * + * @return array Formatted for export tier price information. + */ + private function createExportRow(array $tierPriceData): array + { + //List of columns to display in export row. + $exportRow = $this->templateExportData; + + foreach (array_keys($exportRow) as $keyTemplate) { + if (array_key_exists($keyTemplate, $tierPriceData)) { + if (in_array($keyTemplate, $this->_priceWebsite)) { + //If it's website column then getting website code. + $exportRow[$keyTemplate] = $this->_getWebsiteCode( + $tierPriceData[$keyTemplate] + ); + } elseif (in_array($keyTemplate, $this->_priceCustomerGroup)) { + //If it's customer group column then getting customer + //group name by ID. + $exportRow[$keyTemplate] = $this->_getCustomerGroupById( + $tierPriceData[$keyTemplate], + $tierPriceData[ImportAdvancedPricing::VALUE_ALL_GROUPS] + ); + unset($exportRow[ImportAdvancedPricing::VALUE_ALL_GROUPS]); + } elseif ($keyTemplate + === ImportAdvancedPricing::COL_TIER_PRICE + ) { + //If it's price column then getting value and type + //of tier price. + $exportRow[$keyTemplate] + = $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE] + ? $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE] + : $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE]; + $exportRow[ImportAdvancedPricing::COL_TIER_PRICE_TYPE] + = $this->tierPriceTypeValue($tierPriceData); + } else { + //Any other column just goes as is. + $exportRow[$keyTemplate] = $tierPriceData[$keyTemplate]; + } + } + } + + return $exportRow; + } + + /** + * Prepare data for export. + * + * @param array $productsData Products to export. + * @param array $tierPricesData Their tier prices. + * + * @return array Export rows to display. + */ + private function prepareExportData( + array $productsData, + array $tierPricesData + ): array { + //Assigning SKUs to tier prices data. + $productLinkIdToSkuMap = []; + foreach ($productsData as $productData) { + $productLinkIdToSkuMap[$productData[Store::DEFAULT_STORE_ID][$this->getProductEntityLinkField()]] + = $productData[Store::DEFAULT_STORE_ID]['sku']; + } + + //Adding products' SKUs to tier price data. + $linkedTierPricesData = []; + foreach ($tierPricesData as $tierPriceData) { + $sku = $productLinkIdToSkuMap[$tierPriceData['product_link_id']]; + $linkedTierPricesData[] = array_merge( + $tierPriceData, + [ImportAdvancedPricing::COL_SKU => $sku] + ); + } + + //Formatting data for export. + $customExportData = []; + foreach ($linkedTierPricesData as $row) { + $customExportData[] = $this->createExportRow($row); + } + + return $customExportData; + } + /** * Correct export data. * * @param array $exportData * @return array * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @deprecated + * @see prepareExportData */ protected function correctExportData($exportData) { @@ -327,16 +428,83 @@ protected function correctExportData($exportData) /** * Check type for tier price. * - * @param string $tierPricePercentage + * @param array $tierPriceData * @return string */ - private function tierPriceTypeValue($tierPricePercentage) + private function tierPriceTypeValue(array $tierPriceData): string { - return $tierPricePercentage + return $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE] ? ImportAdvancedPricing::TIER_PRICE_TYPE_PERCENT : ImportAdvancedPricing::TIER_PRICE_TYPE_FIXED; } + /** + * Load tier prices for given products. + * + * @param string[] $productIds Link IDs of products to find tier prices for. + * + * @return array Tier prices data. + * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function fetchTierPrices(array $productIds): array + { + if (empty($productIds)) { + throw new \InvalidArgumentException( + 'Can only load tier prices for specific products' + ); + } + + $pricesTable = ImportAdvancedPricing::TABLE_TIER_PRICE; + $exportFilter = null; + $priceFromFilter = null; + $priceToFilter = null; + if (isset($this->_parameters[Export::FILTER_ELEMENT_GROUP])) { + $exportFilter = $this->_parameters[Export::FILTER_ELEMENT_GROUP]; + } + $productEntityLinkField = $this->getProductEntityLinkField(); + $selectFields = [ + ImportAdvancedPricing::COL_TIER_PRICE_WEBSITE => 'ap.website_id', + ImportAdvancedPricing::VALUE_ALL_GROUPS => 'ap.all_groups', + ImportAdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'ap.customer_group_id', + ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty', + ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value', + ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE => 'ap.percentage_value', + 'product_link_id' => 'ap.' .$productEntityLinkField, + ]; + if ($exportFilter && array_key_exists('tier_price', $exportFilter)) { + if (!empty($exportFilter['tier_price'][0])) { + $priceFromFilter = $exportFilter['tier_price'][0]; + } + if (!empty($exportFilter['tier_price'][1])) { + $priceToFilter = $exportFilter['tier_price'][1]; + } + } + + $select = $this->_connection->select() + ->from( + ['ap' => $this->_resource->getTableName($pricesTable)], + $selectFields + ) + ->where( + 'ap.'.$productEntityLinkField.' IN (?)', + $productIds + ); + + if ($priceFromFilter !== null) { + $select->where('ap.value >= ?', $priceFromFilter); + } + if ($priceToFilter !== null) { + $select->where('ap.value <= ?', $priceToFilter); + } + if ($priceFromFilter || $priceToFilter) { + $select->orWhere('ap.percentage_value IS NOT NULL'); + } + + return $this->_connection->fetchAll($select); + } + /** * Get tier prices. * @@ -345,6 +513,8 @@ private function tierPriceTypeValue($tierPricePercentage) * @return array|bool * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @deprecated + * @see fetchTierPrices */ protected function getTierPrices(array $listSku, $table) { @@ -413,41 +583,51 @@ protected function getTierPrices(array $listSku, $table) } /** - * Get Website code + * Get Website code. * * @param int $websiteId + * * @return string */ - protected function _getWebsiteCode($websiteId) + protected function _getWebsiteCode(int $websiteId): string { - $storeName = ($websiteId == 0) - ? ImportAdvancedPricing::VALUE_ALL_WEBSITES - : $this->_storeManager->getWebsite($websiteId)->getCode(); - $currencyCode = ''; - if ($websiteId == 0) { - $currencyCode = $this->_storeManager->getWebsite($websiteId)->getBaseCurrencyCode(); - } - if ($storeName && $currencyCode) { - return $storeName . ' [' . $currencyCode . ']'; - } else { - return $storeName; + if (!array_key_exists($websiteId, $this->websiteCodesMap)) { + $storeName = ($websiteId == 0) + ? ImportAdvancedPricing::VALUE_ALL_WEBSITES + : $this->_storeManager->getWebsite($websiteId)->getCode(); + $currencyCode = ''; + if ($websiteId == 0) { + $currencyCode = $this->_storeManager->getWebsite($websiteId) + ->getBaseCurrencyCode(); + } + + if ($storeName && $currencyCode) { + $code = $storeName.' ['.$currencyCode.']'; + } else { + $code = $storeName; + } + $this->websiteCodesMap[$websiteId] = $code; } + + return $this->websiteCodesMap[$websiteId]; } /** - * Get Customer Group By Id + * Get Customer Group By Id. + * + * @param int $groupId + * @param int $allGroups * - * @param int $customerGroupId - * @param null $allGroups * @return string */ - protected function _getCustomerGroupById($customerGroupId, $allGroups = null) - { - if ($allGroups) { + protected function _getCustomerGroupById( + int $groupId, + int $allGroups = 0 + ): string { + if ($allGroups !== 0) { return ImportAdvancedPricing::VALUE_ALL_GROUPS; - } else { - return $this->_groupRepository->getById($customerGroupId)->getCode(); } + return $this->_groupRepository->getById($groupId)->getCode(); } /** diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 23829d3725119..4663aea7a7dfc 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -394,7 +394,7 @@ protected function saveAndReplaceAdvancedPrices() ? $rowData[self::COL_TIER_PRICE] : 0, 'percentage_value' => $rowData[self::COL_TIER_PRICE_TYPE] === self::TIER_PRICE_TYPE_PERCENT ? $rowData[self::COL_TIER_PRICE] : null, - 'website_id' => $this->getWebsiteId($rowData[self::COL_TIER_PRICE_WEBSITE]) + 'website_id' => $this->getWebSiteId($rowData[self::COL_TIER_PRICE_WEBSITE]) ]; } } @@ -482,9 +482,8 @@ protected function deleteProductTierPrices(array $listSku, $table) $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, 0); return false; } - } else { - return false; } + return 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 5111b4932d7a8..9a380ff75da24 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php @@ -27,7 +27,7 @@ class WebsiteTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\WebSite::class) + $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\Website::class) ->setMethods(['getBaseCurrency']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json index 1660104953504..12e1d9938f4bd 100644 --- a/app/code/Magento/AdvancedPricingImportExport/composer.json +++ b/app/code/Magento/AdvancedPricingImportExport/composer.json @@ -5,18 +5,17 @@ "sort-packages": true }, "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", - "magento/framework": "100.3.*", - "magento/module-catalog": "101.2.*", - "magento/module-catalog-import-export": "100.3.*", - "magento/module-catalog-inventory": "100.3.*", - "magento/module-customer": "100.3.*", - "magento/module-eav": "100.3.*", - "magento/module-import-export": "100.3.*", - "magento/module-store": "100.3.*" + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-catalog-inventory": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-store": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/AdvancedPricingImportExport/etc/module.xml b/app/code/Magento/AdvancedPricingImportExport/etc/module.xml index ac4b8dafd0183..230fb17ae5544 100644 --- a/app/code/Magento/AdvancedPricingImportExport/etc/module.xml +++ b/app/code/Magento/AdvancedPricingImportExport/etc/module.xml @@ -6,6 +6,6 @@ */ --> - + diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php new file mode 100644 index 0000000000000..403a4d12cc17b --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php @@ -0,0 +1,31 @@ + + * @since 100.0.2 + */ +class Edit extends \Magento\Backend\Block\Widget\Grid\Container +{ + /** + * Enable grid container + * + * @return void + */ + protected function _construct() + { + $this->_blockGroup = 'Magento_AdvancedSearch'; + $this->_controller = 'adminhtml_search'; + $this->_headerText = __('Related Search Terms'); + $this->_addButtonLabel = __('Add New Search Term'); + parent::_construct(); + $this->buttonList->remove('add'); + } +} diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php new file mode 100644 index 0000000000000..6bdfd3b0dd143 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php @@ -0,0 +1,113 @@ + + * @since 100.0.2 + */ +class Grid extends \Magento\Backend\Block\Widget\Grid +{ + /** + * @var \Magento\AdvancedSearch\Model\Adminhtml\Search\Grid\Options + */ + protected $_options; + + /** + * @var \Magento\Framework\Registry + */ + protected $_registryManager; + + /** + * @var \Magento\Framework\Json\Helper\Data + */ + protected $jsonHelper; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Helper\Data $backendHelper + * @param \Magento\AdvancedSearch\Model\Adminhtml\Search\Grid\Options $options + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Json\Helper\Data $jsonHelper + * @param array $data + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Helper\Data $backendHelper, + \Magento\AdvancedSearch\Model\Adminhtml\Search\Grid\Options $options, + \Magento\Framework\Registry $registry, + \Magento\Framework\Json\Helper\Data $jsonHelper, + array $data = [] + ) { + $this->jsonHelper = $jsonHelper; + parent::__construct($context, $backendHelper, $data); + $this->_options = $options; + $this->_registryManager = $registry; + $this->setDefaultFilter(['query_id_selected' => 1]); + } + + /** + * Retrieve a value from registry by a key + * + * @return mixed + */ + public function getQuery() + { + return $this->_registryManager->registry('current_catalog_search'); + } + + /** + * Add column filter to collection + * + * @param \Magento\Backend\Block\Widget\Grid\Column $column + * @return $this + */ + protected function _addColumnFilterToCollection($column) + { + // Set custom filter for query selected flag + if ($column->getId() == 'query_id_selected' && $this->getQuery()->getId()) { + $selectedIds = $this->getSelectedQueries(); + if (empty($selectedIds)) { + $selectedIds = 0; + } + if ($column->getFilter()->getValue()) { + $this->getCollection()->addFieldToFilter('query_id', ['in' => $selectedIds]); + } elseif (!empty($selectedIds)) { + $this->getCollection()->addFieldToFilter('query_id', ['nin' => $selectedIds]); + } + } else { + parent::_addColumnFilterToCollection($column); + } + return $this; + } + + /** + * Retrieve selected related queries from grid + * + * @return array + */ + public function getSelectedQueries() + { + return $this->_options->toOptionArray(); + } + + /** + * Get queries json + * + * @return string + */ + public function getQueriesJson() + { + $queries = array_flip($this->getSelectedQueries()); + if (!empty($queries)) { + return $this->jsonHelper->jsonEncode($queries); + } + return '{}'; + } +} diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/System/Config/TestConnection.php new file mode 100644 index 0000000000000..a546cfb126ba7 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/System/Config/TestConnection.php @@ -0,0 +1,74 @@ +setTemplate('Magento_AdvancedSearch::system/config/testconnection.phtml'); + return $this; + } + + /** + * Unset some non-related element parameters + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + * @since 100.1.0 + */ + public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) + { + $element = clone $element; + $element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); + return parent::render($element); + } + + /** + * Get the button and scripts contents + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + * @since 100.1.0 + */ + protected function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) + { + $originalData = $element->getOriginalData(); + $this->addData( + [ + 'button_label' => __($originalData['button_label']), + 'html_id' => $element->getHtmlId(), + 'ajax_url' => $this->_urlBuilder->getUrl('catalog/search_system_config/testconnection'), + 'field_mapping' => str_replace('"', '\\"', json_encode($this->_getFieldMapping())) + ] + ); + + return $this->_toHtml(); + } + + /** + * Returns configuration fields required to perform the ping request + * + * @return array + * @since 100.1.0 + */ + protected function _getFieldMapping() + { + return ['engine' => 'catalog_search_engine']; + } +} diff --git a/app/code/Magento/AdvancedSearch/Block/Recommendations.php b/app/code/Magento/AdvancedSearch/Block/Recommendations.php new file mode 100644 index 0000000000000..1a23ea554bd91 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Block/Recommendations.php @@ -0,0 +1,14 @@ +searchDataProvider = $searchDataProvider; + $this->query = $queryFactory->get(); + $this->title = $title; + parent::__construct($context, $data); + } + + /** + * {@inheritdoc} + */ + public function getItems() + { + return $this->searchDataProvider->getItems($this->query); + } + + /** + * {@inheritdoc} + */ + public function isShowResultsCount() + { + return $this->searchDataProvider->isResultsCountEnabled(); + } + + /** + * {@inheritdoc} + */ + public function getLink($queryText) + { + return $this->getUrl('*/*/') . '?q=' . urlencode($queryText); + } + + /** + * {@inheritdoc} + */ + public function getTitle() + { + return __($this->title); + } +} diff --git a/app/code/Magento/AdvancedSearch/Block/SearchDataInterface.php b/app/code/Magento/AdvancedSearch/Block/SearchDataInterface.php new file mode 100644 index 0000000000000..299e68e558ad5 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Block/SearchDataInterface.php @@ -0,0 +1,36 @@ +clientResolver = $clientResolver; + $this->resultJsonFactory = $resultJsonFactory; + $this->tagFilter = $tagFilter; + } + + /** + * Check for connection to server + * + * @return \Magento\Framework\Controller\Result\Json + */ + public function execute() + { + $result = [ + 'success' => false, + 'errorMessage' => '', + ]; + $options = $this->getRequest()->getParams(); + + try { + if (empty($options['engine'])) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Missing search engine parameter.') + ); + } + $response = $this->clientResolver->create($options['engine'], $options)->testConnection(); + if ($response) { + $result['success'] = true; + } + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $result['errorMessage'] = $e->getMessage(); + } catch (\Exception $e) { + $message = __($e->getMessage()); + $result['errorMessage'] = $this->tagFilter->filter($message); + } + + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($result); + } +} diff --git a/app/code/Magento/AdvancedSearch/LICENSE.txt b/app/code/Magento/AdvancedSearch/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AdvancedSearch/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/AdvancedSearch/LICENSE_AFL.txt b/app/code/Magento/AdvancedSearch/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/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/AdvancedSearch/Model/Adapter/DataMapper/AdditionalFieldsProvider.php b/app/code/Magento/AdvancedSearch/Model/Adapter/DataMapper/AdditionalFieldsProvider.php new file mode 100644 index 0000000000000..ef1f9890e02d1 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Adapter/DataMapper/AdditionalFieldsProvider.php @@ -0,0 +1,39 @@ + [field name1 => value1, ...], ...] + */ +class AdditionalFieldsProvider implements AdditionalFieldsProviderInterface +{ + /** + * @var AdditionalFieldsProviderInterface[] + */ + private $fieldsProviders; + + /** + * @param AdditionalFieldsProviderInterface[] $fieldsProviders + */ + public function __construct(array $fieldsProviders) + { + $this->fieldsProviders = $fieldsProviders; + } + + /** + * {@inheritdoc} + */ + public function getFields(array $productIds, $storeId) + { + $fields = []; + foreach ($this->fieldsProviders as $fieldsProvider) { + $fields[] = $fieldsProvider->getFields($productIds, $storeId); + } + + return array_replace_recursive(...$fields); + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/Adapter/DataMapper/AdditionalFieldsProviderInterface.php b/app/code/Magento/AdvancedSearch/Model/Adapter/DataMapper/AdditionalFieldsProviderInterface.php new file mode 100644 index 0000000000000..d7151236c6170 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Adapter/DataMapper/AdditionalFieldsProviderInterface.php @@ -0,0 +1,25 @@ + [field name1 => value1, ...], ...] + * @api + * @since 100.2.0 + */ +interface AdditionalFieldsProviderInterface +{ + /** + * Get additional fields for data mapper during search indexer based on product ids and store id. + * + * @param array $productIds + * @param int $storeId + * @return array + * @since 100.2.0 + */ + public function getFields(array $productIds, $storeId); +} diff --git a/app/code/Magento/AdvancedSearch/Model/Adminhtml/Search/Grid/Options.php b/app/code/Magento/AdvancedSearch/Model/Adminhtml/Search/Grid/Options.php new file mode 100644 index 0000000000000..b139689dbc234 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Adminhtml/Search/Grid/Options.php @@ -0,0 +1,60 @@ +_request = $request; + $this->_registryManager = $registry; + $this->_searchResourceModel = $searchResourceModel; + } + + /** + * {@inheritdoc} + */ + public function toOptionArray() + { + $queries = $this->_request->getPost('selected_queries'); + + $currentQueryId = $this->_registryManager->registry('current_catalog_search')->getId(); + $queryIds = []; + if ($queries === null && !empty($currentQueryId)) { + $queryIds = $this->_searchResourceModel->getRelatedQueries($currentQueryId); + } + return $queryIds; + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php b/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php new file mode 100644 index 0000000000000..05eb513d68399 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Client/ClientFactory.php @@ -0,0 +1,47 @@ +objectManager = $objectManager; + $this->clientClass = $clientClass; + } + + /** + * Return search client + * + * @param array $options + * @return ClientInterface + */ + public function create(array $options = []) + { + return $this->objectManager->create( + $this->clientClass, + ['options' => $options] + ); + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/Client/ClientFactoryInterface.php b/app/code/Magento/AdvancedSearch/Model/Client/ClientFactoryInterface.php new file mode 100644 index 0000000000000..acacbb1c093fa --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Client/ClientFactoryInterface.php @@ -0,0 +1,22 @@ +objectManager = $objectManager; + $this->clientFactoryPool = $clientFactories; + $this->clientOptionsPool = $clientOptions; + $this->engineResolver = $engineResolver; + } + + /** + * Returns configured search engine + * + * @return string + * @since 100.1.0 + */ + public function getCurrentEngine() + { + return $this->engineResolver->getCurrentSearchEngine(); + } + + /** + * Create client instance + * + * @param string $engine + * @param array $data + * @return ClientInterface + * @since 100.1.0 + */ + public function create($engine = '', array $data = []) + { + $engine = $engine ?: $this->getCurrentEngine(); + + if (!isset($this->clientFactoryPool[$engine])) { + throw new \LogicException( + 'There is no such client factory: ' . $engine + ); + } + $factoryClass = $this->clientFactoryPool[$engine]; + $factory = $this->objectManager->create($factoryClass); + if (!($factory instanceof ClientFactoryInterface)) { + throw new \InvalidArgumentException( + 'Client factory must implement \Magento\AdvancedSearch\Model\Client\ClientFactoryInterface' + ); + } + + $optionsClass = $this->clientOptionsPool[$engine]; + $clientOptions = $this->objectManager->create($optionsClass); + if (!($clientOptions instanceof ClientOptionsInterface)) { + throw new \InvalidArgumentException( + 'Client options must implement \Magento\AdvancedSearch\Model\Client\ClientInterface' + ); + } + + $client = $factory->create($clientOptions->prepareClientOptions($data)); + + return $client; + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/DataProvider/Suggestions.php b/app/code/Magento/AdvancedSearch/Model/DataProvider/Suggestions.php new file mode 100644 index 0000000000000..c76811c854514 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/DataProvider/Suggestions.php @@ -0,0 +1,28 @@ +clientOptions = $clientOptions; + $this->engineResolver = $engineResolver; + } + + /** + * Invalidate indexer on customer group save + * + * @param Group $subject + * @param \Closure $proceed + * @param AbstractModel $group + * @return Attribute + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundSave( + Group $subject, + \Closure $proceed, + AbstractModel $group + ) { + $needInvalidation = + ($this->engineResolver->getCurrentSearchEngine() != EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) + && ($group->isObjectNew() || $group->dataHasChangedFor('tax_class_id')); + $result = $proceed($group); + if ($needInvalidation) { + $this->indexerRegistry->get(Fulltext::INDEXER_ID)->invalidate(); + } + return $result; + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php new file mode 100644 index 0000000000000..546983bb5e5a8 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php @@ -0,0 +1,149 @@ +scopeConfig = $scopeConfig; + $this->searchLayer = $layerResolver->get(); + $this->recommendationsFactory = $recommendationsFactory; + $this->queryResultFactory = $queryResultFactory; + } + + /** + * @return bool + */ + public function isResultsCountEnabled() + { + return (bool)$this->scopeConfig->getValue( + self::CONFIG_RESULTS_COUNT_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * {@inheritdoc} + */ + public function getItems(QueryInterface $query) + { + $recommendations = []; + + if (!$this->isSearchRecommendationsEnabled()) { + return []; + } + + foreach ($this->getSearchRecommendations($query) as $recommendation) { + $recommendations[] = $this->queryResultFactory->create( + [ + 'queryText' => $recommendation['query_text'], + 'resultsCount' => $recommendation['num_results'], + ] + ); + } + return $recommendations; + } + + /** + * @param QueryInterface $query + * @return array + */ + private function getSearchRecommendations(\Magento\Search\Model\QueryInterface $query) + { + $recommendations = []; + + if ($this->isSearchRecommendationsEnabled()) { + $productCollection = $this->searchLayer->getProductCollection(); + $params = ['store_id' => $productCollection->getStoreId()]; + + /** @var \Magento\AdvancedSearch\Model\ResourceModel\Recommendations $recommendationsResource */ + $recommendationsResource = $this->recommendationsFactory->create(); + $recommendations = $recommendationsResource->getRecommendationsByQuery( + $query->getQueryText(), + $params, + $this->getSearchRecommendationsCount() + ); + } + + return $recommendations; + } + + /** + * @return bool + */ + private function isSearchRecommendationsEnabled() + { + return (bool)$this->scopeConfig->getValue( + self::CONFIG_IS_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @return int + */ + private function getSearchRecommendationsCount() + { + return (int)$this->scopeConfig->getValue( + self::CONFIG_RESULTS_COUNT, + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/Recommendations/SaveSearchQueryRelationsObserver.php b/app/code/Magento/AdvancedSearch/Model/Recommendations/SaveSearchQueryRelationsObserver.php new file mode 100644 index 0000000000000..5f5d4122d97a5 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Recommendations/SaveSearchQueryRelationsObserver.php @@ -0,0 +1,48 @@ +recommendationsFactory = $recommendationsFactory; + } + + /** + * Save search query relations after save search query + * + * @param EventObserver $observer + * @return void + */ + public function execute(EventObserver $observer) + { + $searchQueryModel = $observer->getEvent()->getDataObject(); + $queryId = $searchQueryModel->getId(); + $relatedQueries = $searchQueryModel->getSelectedQueriesGrid(); + + if (strlen($relatedQueries) == 0) { + $relatedQueries = []; + } else { + $relatedQueries = explode('&', $relatedQueries); + } + + $this->recommendationsFactory->create()->saveRelatedQueries($queryId, $relatedQueries); + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php new file mode 100644 index 0000000000000..c2379e9dff062 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php @@ -0,0 +1,189 @@ +storeManager = $storeManager; + $this->metadataPool = $metadataPool; + $this->tableResolver = $tableResolver ?: ObjectManager::getInstance()->get(TableResolver::class); + } + + /** + * Implementation of abstract construct + * @return void + * @since 100.1.0 + */ + protected function _construct() + { + } + + /** + * Return array of price data per customer and website by products + * + * @param null|array $productIds + * @return array + * @since 100.1.0 + */ + protected function _getCatalogProductPriceData($productIds = null) + { + $connection = $this->getConnection(); + + $select = $connection->select()->from( + $this->getTable('catalog_product_index_price'), + ['entity_id', 'customer_group_id', 'website_id', 'min_price'] + ); + + if ($productIds) { + $select->where('entity_id IN (?)', $productIds); + } + + $result = []; + foreach ($connection->fetchAll($select) as $row) { + $result[$row['website_id']][$row['entity_id']][$row['customer_group_id']] = round($row['min_price'], 2); + } + + return $result; + } + + /** + * Retrieve price data for product + * + * @param null|array $productIds + * @param int $storeId + * @return array + * @since 100.1.0 + */ + public function getPriceIndexData($productIds, $storeId) + { + $priceProductsIndexData = $this->_getCatalogProductPriceData($productIds); + + $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + if (!isset($priceProductsIndexData[$websiteId])) { + return []; + } + + return $priceProductsIndexData[$websiteId]; + } + + /** + * Prepare system index data for products. + * + * @param int $storeId + * @param null|array $productIds + * @return array + * @since 100.1.0 + */ + public function getCategoryProductIndexData($storeId = null, $productIds = null) + { + $connection = $this->getConnection(); + + $catalogCategoryProductDimension = new Dimension(\Magento\Store\Model\Store::ENTITY, $storeId); + + $catalogCategoryProductTableName = $this->tableResolver->resolve( + AbstractAction::MAIN_INDEX_TABLE, + [ + $catalogCategoryProductDimension + ] + ); + + $select = $connection->select()->from( + [$catalogCategoryProductTableName], + ['category_id', 'product_id', 'position', 'store_id'] + )->where( + 'store_id = ?', + $storeId + ); + + if ($productIds) { + $select->where('product_id IN (?)', $productIds); + } + + $result = []; + foreach ($connection->fetchAll($select) as $row) { + $result[$row['product_id']][$row['category_id']] = $row['position']; + } + + return $result; + } + + /** + * Retrieve moved categories product ids + * + * @param int $categoryId + * @return array + * @since 100.1.0 + */ + public function getMovedCategoryProductIds($categoryId) + { + $connection = $this->getConnection(); + + $identifierField = $this->metadataPool->getMetadata(CategoryInterface::class)->getIdentifierField(); + + $select = $connection->select()->distinct()->from( + ['c_p' => $this->getTable('catalog_category_product')], + ['product_id'] + )->join( + ['c_e' => $this->getTable('catalog_category_entity')], + 'c_p.category_id = c_e.' . $identifierField, + [] + )->where( + $connection->quoteInto('c_e.path LIKE ?', '%/' . $categoryId . '/%') + )->orWhere( + 'c_p.category_id = ?', + $categoryId + ); + + return $connection->fetchCol($select); + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php new file mode 100644 index 0000000000000..c19c1d67d81f7 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php @@ -0,0 +1,227 @@ + + * @api + * @since 100.0.2 + */ +class Recommendations extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +{ + + /** + * Search query model + * + * @var \Magento\Search\Model\Query + */ + protected $_searchQueryModel; + + /** + * Construct + * + * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @param \Magento\Search\Model\QueryFactory $queryFactory + * @param string $connectionName + */ + public function __construct( + \Magento\Framework\Model\ResourceModel\Db\Context $context, + \Magento\Search\Model\QueryFactory $queryFactory, + $connectionName = null + ) { + parent::__construct($context, $connectionName); + $this->_searchQueryModel = $queryFactory->create(); + } + + /** + * Init main table + * + * @return void + */ + protected function _construct() + { + $this->_init('catalogsearch_recommendations', 'id'); + } + + /** + * Save search relations + * + * @param int $queryId + * @param array $relatedQueries + * @return $this + */ + public function saveRelatedQueries($queryId, $relatedQueries = []) + { + $connection = $this->getConnection(); + $whereOr = []; + if (count($relatedQueries) > 0) { + $whereOr[] = implode( + ' AND ', + [ + $connection->quoteInto('query_id=?', $queryId), + $connection->quoteInto('relation_id NOT IN(?)', $relatedQueries) + ] + ); + $whereOr[] = implode( + ' AND ', + [ + $connection->quoteInto('relation_id = ?', $queryId), + $connection->quoteInto('query_id NOT IN(?)', $relatedQueries) + ] + ); + } else { + $whereOr[] = $connection->quoteInto('query_id = ?', $queryId); + $whereOr[] = $connection->quoteInto('relation_id = ?', $queryId); + } + $whereCond = '(' . implode(') OR (', $whereOr) . ')'; + $connection->delete($this->getMainTable(), $whereCond); + + $existsRelatedQueries = $this->getRelatedQueries($queryId); + $neededRelatedQueries = array_diff($relatedQueries, $existsRelatedQueries); + foreach ($neededRelatedQueries as $relationId) { + $connection->insert($this->getMainTable(), ["query_id" => $queryId, "relation_id" => $relationId]); + } + return $this; + } + + /** + * Retrieve related search queries + * + * @param int|array $queryId + * @param bool $limit + * @param bool $order + * @return array + */ + public function getRelatedQueries($queryId, $limit = false, $order = false) + { + $collection = $this->_searchQueryModel->getResourceCollection(); + $connection = $this->getConnection(); + + $queryIdCond = $connection->quoteInto('main_table.query_id IN (?)', $queryId); + + $collection->getSelect()->join( + ['sr' => $collection->getTable('catalogsearch_recommendations')], + '(sr.query_id=main_table.query_id OR sr.relation_id=main_table.query_id) AND ' . $queryIdCond + )->reset( + \Magento\Framework\DB\Select::COLUMNS + )->columns( + [ + 'rel_id' => $connection->getCheckSql( + 'main_table.query_id=sr.query_id', + 'sr.relation_id', + 'sr.query_id' + ), + ] + ); + if (!empty($limit)) { + $collection->getSelect()->limit($limit); + } + if (!empty($order)) { + $collection->getSelect()->order($order); + } + + $queryIds = $connection->fetchCol($collection->getSelect()); + return $queryIds; + } + + /** + * Retrieve related search queries by single query + * + * @param string $query + * @param array $params + * @param int $searchRecommendationsCount + * @return array + */ + public function getRecommendationsByQuery($query, $params, $searchRecommendationsCount) + { + $this->_searchQueryModel->loadByQueryText($query); + + if (isset($params['store_id'])) { + $this->_searchQueryModel->setStoreId($params['store_id']); + } + $relatedQueriesIds = $this->loadByQuery($query, $searchRecommendationsCount); + $relatedQueries = []; + if (count($relatedQueriesIds)) { + $connection = $this->getConnection(); + $mainTable = $this->_searchQueryModel->getResourceCollection()->getMainTable(); + $select = $connection->select()->from( + ['main_table' => $mainTable], + ['query_text', 'num_results'] + )->where( + 'query_id IN(?)', + $relatedQueriesIds + )->where( + 'num_results > 0' + ); + $relatedQueries = $connection->fetchAll($select); + } + + return $relatedQueries; + } + + /** + * Retrieve search terms which are started with $queryWords + * + * @param string $query + * @param int $searchRecommendationsCount + * @return array + */ + protected function loadByQuery($query, $searchRecommendationsCount) + { + $connection = $this->getConnection(); + $queryId = $this->_searchQueryModel->getId(); + $relatedQueries = $this->getRelatedQueries($queryId, $searchRecommendationsCount, 'num_results DESC'); + if ($searchRecommendationsCount - count($relatedQueries) < 1) { + return $relatedQueries; + } + + $queryWords = [$query]; + if (strpos($query, ' ') !== false) { + $queryWords = array_unique(array_merge($queryWords, explode(' ', $query))); + foreach ($queryWords as $key => $word) { + $queryWords[$key] = trim($word); + if (strlen($word) < 3) { + unset($queryWords[$key]); + } + } + } + + $likeCondition = []; + foreach ($queryWords as $word) { + $likeCondition[] = $connection->quoteInto('query_text LIKE ?', $word . '%'); + } + $likeCondition = implode(' OR ', $likeCondition); + + $select = $connection->select()->from( + $this->_searchQueryModel->getResource()->getMainTable(), + ['query_id'] + )->where( + new \Zend_Db_Expr($likeCondition) + )->where( + 'store_id=?', + $this->_searchQueryModel->getStoreId() + )->order( + 'num_results DESC' + )->limit( + $searchRecommendationsCount + 1 + ); + $ids = $connection->fetchCol($select); + + if (!is_array($ids)) { + $ids = []; + } + + $key = array_search($queryId, $ids); + if ($key !== false) { + unset($ids[$key]); + } + $ids = array_unique(array_merge($relatedQueries, $ids)); + $ids = array_slice($ids, 0, $searchRecommendationsCount); + return $ids; + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Search/Grid/Collection.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Search/Grid/Collection.php new file mode 100644 index 0000000000000..59263f308117c --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Search/Grid/Collection.php @@ -0,0 +1,80 @@ +_registryManager = $registry; + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $storeManager, + $resourceHelper, + $connection, + $resource + ); + } + + /** + * Initialize select + * + * @return $this + */ + protected function _initSelect() + { + parent::_initSelect(); + $queryId = $this->getQuery()->getId(); + if ($queryId) { + $this->addFieldToFilter('query_id', ['nin' => $queryId]); + } + return $this; + } + + /** + * Retrieve a value from registry by a key + * + * @return \Magento\Search\Model\Query + */ + public function getQuery() + { + return $this->_registryManager->registry('current_catalog_search'); + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/SuggestedQueries.php b/app/code/Magento/AdvancedSearch/Model/SuggestedQueries.php new file mode 100644 index 0000000000000..60f76682fc164 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/SuggestedQueries.php @@ -0,0 +1,88 @@ +engineResolver = $engineResolver; + $this->objectManager = $objectManager; + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function isResultsCountEnabled() + { + return $this->getDataProvider()->isResultsCountEnabled(); + } + + /** + * {@inheritdoc} + */ + public function getItems(QueryInterface $query) + { + return $this->getDataProvider()->getItems($query); + } + + /** + * Returns DataProvider for SuggestedQueries + * + * @return SuggestedQueriesInterface|SuggestedQueriesInterface[] + * @throws \Exception + */ + private function getDataProvider() + { + if (empty($this->dataProvider)) { + $currentEngine = $this->engineResolver->getCurrentSearchEngine(); + $this->dataProvider = $this->objectManager->create($this->data[$currentEngine]); + if (!$this->dataProvider instanceof SuggestedQueriesInterface) { + throw new \InvalidArgumentException( + 'Data provider must implement \Magento\AdvancedSearch\Model\SuggestedQueriesInterface' + ); + } + } + return $this->dataProvider; + } +} diff --git a/app/code/Magento/AdvancedSearch/Model/SuggestedQueriesInterface.php b/app/code/Magento/AdvancedSearch/Model/SuggestedQueriesInterface.php new file mode 100644 index 0000000000000..64ab45ceb145e --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/SuggestedQueriesInterface.php @@ -0,0 +1,42 @@ +dataProvider = $this->getMockBuilder(\Magento\AdvancedSearch\Model\SuggestedQueriesInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getItems', 'isResultsCountEnabled']) + ->getMockForAbstractClass(); + + $this->searchQuery = $this->getMockBuilder(\Magento\Search\Model\QueryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getQueryText']) + ->getMockForAbstractClass(); + $this->queryFactory = $this->getMockBuilder(\Magento\Search\Model\QueryFactoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + $this->queryFactory->expects($this->once()) + ->method('get') + ->will($this->returnValue($this->searchQuery)); + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\Template\Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->block = $this->getMockBuilder(\Magento\AdvancedSearch\Block\SearchData::class)->setConstructorArgs( + [ + $this->context, + $this->dataProvider, + $this->queryFactory, + 'Test Title', + [], + ] + ) + ->setMethods(['getUrl']) + ->getMockForAbstractClass(); + } + + public function testGetSuggestions() + { + $value = [1, 2, 3, 100500]; + + $this->dataProvider->expects($this->once()) + ->method('getItems') + ->with($this->searchQuery) + ->will($this->returnValue($value)); + $actualValue = $this->block->getItems(); + $this->assertEquals($value, $actualValue); + } + + public function testGetLink() + { + $searchQuery = 'Some test search query'; + $expectedResult = '?q=Some+test+search+query'; + $actualResult = $this->block->getLink($searchQuery); + $this->assertEquals($expectedResult, $actualResult); + } + + public function testIsShowResultsCount() + { + $value = 'qwertyasdfzxcv'; + $this->dataProvider->expects($this->once()) + ->method('isResultsCountEnabled') + ->will($this->returnValue($value)); + $this->assertEquals($value, $this->block->isShowResultsCount()); + } +} diff --git a/app/code/Magento/AdvancedSearch/Test/Unit/Controller/Adminhtml/Search/System/Config/TestConnectionTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Controller/Adminhtml/Search/System/Config/TestConnectionTest.php new file mode 100644 index 0000000000000..6215d79fc41ee --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Controller/Adminhtml/Search/System/Config/TestConnectionTest.php @@ -0,0 +1,169 @@ +requestMock = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParams']); + $responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); + + $context = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) + ->setMethods(['getRequest', 'getResponse', 'getMessageManager', 'getSession']) + ->setConstructorArgs( + $helper->getConstructArguments( + \Magento\Backend\App\Action\Context::class, + [ + 'request' => $this->requestMock + ] + ) + ) + ->getMock(); + $context->expects($this->once())->method('getRequest')->will($this->returnValue($this->requestMock)); + $context->expects($this->once())->method('getResponse')->will($this->returnValue($responseMock)); + + $this->clientResolverMock = $this->getMockBuilder(\Magento\AdvancedSearch\Model\Client\ClientResolver::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->clientMock = $this->createMock(\Magento\AdvancedSearch\Model\Client\ClientInterface::class); + + $this->resultJson = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->tagFilterMock = $this->getMockBuilder(\Magento\Framework\Filter\StripTags::class) + ->disableOriginalConstructor() + ->setMethods(['filter']) + ->getMock(); + + $this->controller = new TestConnection( + $context, + $this->clientResolverMock, + $this->resultJsonFactory, + $this->tagFilterMock + ); + } + + public function testExecuteEmptyEngine() + { + $this->requestMock->expects($this->once())->method('getParams') + ->will($this->returnValue(['engine' => ''])); + + $this->resultJsonFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->resultJson)); + + $result = ['success' => false, 'errorMessage' => 'Missing search engine parameter.']; + + $this->resultJson->expects($this->once())->method('setData') + ->with($this->equalTo($result)); + + $this->controller->execute(); + } + + public function testExecute() + { + $this->requestMock->expects($this->once())->method('getParams') + ->will($this->returnValue(['engine' => 'engineName'])); + + $this->clientResolverMock->expects($this->once())->method('create') + ->with($this->equalTo('engineName')) + ->will($this->returnValue($this->clientMock)); + + $this->clientMock->expects($this->once())->method('testConnection') + ->will($this->returnValue(true)); + + $this->resultJsonFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->resultJson)); + + $result = ['success' => true, 'errorMessage' => '']; + + $this->resultJson->expects($this->once())->method('setData') + ->with($this->equalTo($result)); + + $this->controller->execute(); + } + + public function testExecutePingFailed() + { + $this->requestMock->expects($this->once())->method('getParams') + ->will($this->returnValue(['engine' => 'engineName'])); + + $this->clientResolverMock->expects($this->once())->method('create') + ->with($this->equalTo('engineName')) + ->will($this->returnValue($this->clientMock)); + + $this->clientMock->expects($this->once())->method('testConnection') + ->will($this->returnValue(false)); + + $this->resultJsonFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->resultJson)); + + $result = ['success' => false, 'errorMessage' => '']; + + $this->resultJson->expects($this->once())->method('setData') + ->with($this->equalTo($result)); + + $this->controller->execute(); + } +} diff --git a/app/code/Magento/AdvancedSearch/Test/Unit/Model/Client/ClientResolverTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Model/Client/ClientResolverTest.php new file mode 100644 index 0000000000000..0cad0a2e8301c --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Model/Client/ClientResolverTest.php @@ -0,0 +1,108 @@ +engineResolverMock = $this->getMockBuilder(EngineResolverInterface::class) + ->getMockForAbstractClass(); + + $this->objectManager = $this->createMock(ObjectManagerInterface::class); + + $this->model = new ClientResolver( + $this->objectManager, + ['engineName' => 'engineFactoryClass'], + ['engineName' => 'engineOptionClass'], + $this->engineResolverMock + ); + } + + public function testCreate() + { + $this->engineResolverMock->expects($this->once())->method('getCurrentSearchEngine') + ->will($this->returnValue('engineName')); + + $factoryMock = $this->createMock(ClientFactoryInterface::class); + + $clientMock = $this->createMock(ClientInterface::class); + + $clientOptionsMock = $this->createMock(ClientOptionsInterface::class); + + $this->objectManager->expects($this->exactly(2))->method('create') + ->withConsecutive( + [$this->equalTo('engineFactoryClass')], + [$this->equalTo('engineOptionClass')] + ) + ->willReturnOnConsecutiveCalls( + $factoryMock, + $clientOptionsMock + ); + + $clientOptionsMock->expects($this->once())->method('prepareClientOptions') + ->with([]) + ->will($this->returnValue(['parameters'])); + + $factoryMock->expects($this->once())->method('create') + ->with($this->equalTo(['parameters'])) + ->will($this->returnValue($clientMock)); + + $result = $this->model->create(); + $this->assertInstanceOf(ClientInterface::class, $result); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCreateExceptionThrown() + { + $this->objectManager->expects($this->once())->method('create') + ->with($this->equalTo('engineFactoryClass')) + ->will($this->returnValue('t')); + + $this->model->create('engineName'); + } + + /** + * @expectedException LogicException + */ + public function testCreateLogicException() + { + $this->model->create('input'); + } + + public function testGetCurrentEngine() + { + $this->engineResolverMock->expects($this->once())->method('getCurrentSearchEngine') + ->will($this->returnValue('engineName')); + + $this->assertEquals('engineName', $this->model->getCurrentEngine()); + } +} diff --git a/app/code/Magento/AdvancedSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/CustomerGroupTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/CustomerGroupTest.php new file mode 100644 index 0000000000000..e6de135aab473 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/CustomerGroupTest.php @@ -0,0 +1,131 @@ +subjectMock = $this->createMock(\Magento\Customer\Model\ResourceModel\Group::class); + $this->customerOptionsMock = $this->createMock( + \Magento\AdvancedSearch\Model\Client\ClientOptionsInterface::class + ); + $this->indexerMock = $this->getMockForAbstractClass( + \Magento\Framework\Indexer\IndexerInterface::class, + [], + '', + false, + false, + true, + ['getId', 'getState', '__wakeup'] + ); + $this->indexerRegistryMock = $this->createPartialMock( + \Magento\Framework\Indexer\IndexerRegistry::class, + ['get'] + ); + $this->engineResolverMock = $this->createPartialMock( + \Magento\Search\Model\EngineResolver::class, + ['getCurrentSearchEngine'] + ); + $this->model = new CustomerGroup( + $this->indexerRegistryMock, + $this->customerOptionsMock, + $this->engineResolverMock + ); + } + + /** + * @param string $searchEngine + * @param bool $isObjectNew + * @param bool $isTaxClassIdChanged + * @param int $invalidateCounter + * @return void + * @dataProvider aroundSaveDataProvider + */ + public function testAroundSave($searchEngine, $isObjectNew, $isTaxClassIdChanged, $invalidateCounter) + { + $this->engineResolverMock->expects($this->once()) + ->method('getCurrentSearchEngine') + ->will($this->returnValue($searchEngine)); + + $groupMock = $this->createPartialMock( + \Magento\Customer\Model\Group::class, + ['dataHasChangedFor', 'isObjectNew', '__wakeup'] + ); + $groupMock->expects($this->any())->method('isObjectNew')->will($this->returnValue($isObjectNew)); + $groupMock->expects($this->any()) + ->method('dataHasChangedFor') + ->with('tax_class_id') + ->will($this->returnValue($isTaxClassIdChanged)); + + $closureMock = function (\Magento\Customer\Model\Group $object) use ($groupMock) { + $this->assertEquals($object, $groupMock); + return $this->subjectMock; + }; + + $this->indexerMock->expects($this->exactly($invalidateCounter))->method('invalidate'); + $this->indexerRegistryMock->expects($this->exactly($invalidateCounter)) + ->method('get') + ->with(\Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID) + ->will($this->returnValue($this->indexerMock)); + + $this->assertEquals( + $this->subjectMock, + $this->model->aroundSave($this->subjectMock, $closureMock, $groupMock) + ); + } + + /** + * @return array + */ + public function aroundSaveDataProvider() + { + return [ + ['mysql', false, false, 0], + ['mysql', false, true, 0], + ['mysql', true, false, 0], + ['mysql', true, true, 0], + ['custom', false, false, 0], + ['custom', false, true, 1], + ['custom', true, false, 1], + ['custom', true, true, 1], + ]; + } +} diff --git a/app/code/Magento/AdvancedSearch/Test/Unit/Model/ResourceModel/IndexTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Model/ResourceModel/IndexTest.php new file mode 100644 index 0000000000000..185e932406e5b --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Model/ResourceModel/IndexTest.php @@ -0,0 +1,84 @@ +storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->resourceContextMock = $this->createMock(Context::class); + $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->resourceContextMock->expects($this->any()) + ->method('getResources') + ->willReturn($this->resourceConnectionMock); + $this->adapterMock = $this->createMock(AdapterInterface::class); + $this->resourceConnectionMock->expects($this->any())->method('getConnection')->willReturn($this->adapterMock); + $this->metadataPoolMock = $this->createMock(MetadataPool::class); + + $this->model = new Index( + $this->resourceContextMock, + $this->storeManagerMock, + $this->metadataPoolMock + ); + } + + public function testGetPriceIndexDataUsesFrontendPriceIndexerTable() + { + $storeId = 1; + $storeMock = $this->createMock(StoreInterface::class); + $storeMock->expects($this->any())->method('getId')->willReturn($storeId); + $this->storeManagerMock->expects($this->once())->method('getStore')->with($storeId)->willReturn($storeMock); + + $selectMock = $this->createMock(Select::class); + $selectMock->expects($this->any())->method('from')->willReturnSelf(); + $selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->adapterMock->expects($this->once())->method('select')->willReturn($selectMock); + $this->adapterMock->expects($this->once())->method('fetchAll')->with($selectMock)->willReturn([]); + + $this->assertEmpty($this->model->getPriceIndexData([1], $storeId)); + } +} diff --git a/app/code/Magento/AdvancedSearch/Test/Unit/Model/SuggestedQueriesTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Model/SuggestedQueriesTest.php new file mode 100644 index 0000000000000..d349ed3e3ce93 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Model/SuggestedQueriesTest.php @@ -0,0 +1,131 @@ +engineResolverMock = $this->getMockBuilder(\Magento\Search\Model\EngineResolver::class) + ->setMethods(['getCurrentSearchEngine']) + ->disableOriginalConstructor() + ->getMock(); + $this->engineResolverMock->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('my_engine'); + + /** + * @var \Magento\AdvancedSearch\Model\SuggestedQueriesInterface| + * \PHPUnit_Framework_MockObject_MockObject + */ + $suggestedQueriesMock = $this->createMock(\Magento\AdvancedSearch\Model\SuggestedQueriesInterface::class); + $suggestedQueriesMock->expects($this->any()) + ->method('isResultsCountEnabled') + ->willReturn(true); + $suggestedQueriesMock->expects($this->any()) + ->method('getItems') + ->willReturn([]); + + $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->with('search_engine') + ->willReturn($suggestedQueriesMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + \Magento\AdvancedSearch\Model\SuggestedQueries::class, + [ + 'engineResolver' => $this->engineResolverMock, + 'objectManager' => $this->objectManagerMock, + 'data' => ['my_engine' => 'search_engine'] + ] + ); + } + + /** + * Test isResultsCountEnabled method. + * + * @return void + */ + public function testIsResultsCountEnabled() + { + $result = $this->model->isResultsCountEnabled(); + $this->assertTrue($result); + } + + /** + * Test isResultsCountEnabled() method failure. + * @expectedException \InvalidArgumentException + * + * @return void + */ + public function testIsResultsCountEnabledException() + { + $objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn(null); + + $objectManagerHelper = new ObjectManagerHelper($this); + /* @var $model \Magento\AdvancedSearch\Model\SuggestedQueries */ + $model = $objectManagerHelper->getObject( + \Magento\AdvancedSearch\Model\SuggestedQueries::class, + [ + 'engineResolver' => $this->engineResolverMock, + 'objectManager' => $objectManagerMock, + 'data' => ['my_engine' => 'search_engine'] + ] + ); + $model->isResultsCountEnabled(); + } + + /** + * Test testGetItems() method. + * + * @return void + */ + public function testGetItems() + { + /** @var $queryInterfaceMock \Magento\Search\Model\QueryInterface */ + $queryInterfaceMock = $this->createMock(\Magento\Search\Model\QueryInterface::class); + $result = $this->model->getItems($queryInterfaceMock); + $this->assertEquals([], $result); + } +} diff --git a/app/code/Magento/AdvancedSearch/composer.json b/app/code/Magento/AdvancedSearch/composer.json new file mode 100644 index 0000000000000..a224a1001cd01 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-advanced-search", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-search": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-search": "*", + "magento/module-store": "*", + "php": "~7.1.3||~7.2.0" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AdvancedSearch\\": "" + } + } +} diff --git a/app/code/Magento/AdvancedSearch/etc/adminhtml/events.xml b/app/code/Magento/AdvancedSearch/etc/adminhtml/events.xml new file mode 100644 index 0000000000000..b4d0f63a2bab4 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/adminhtml/events.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/etc/adminhtml/routes.xml b/app/code/Magento/AdvancedSearch/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..286d1537d40cc --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..fa7774f5cec1d --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml @@ -0,0 +1,74 @@ + + + + +

+ + + + When you enable this option your site may slow down. + Magento\Config\Model\Config\Source\Yesno + + + + validate-digits + + 1 + + + + + Magento\Config\Model\Config\Source\Yesno + + 1 + + + + + + When you enable this option your site may slow down. + Magento\Config\Model\Config\Source\Yesno + + + + + 1 + + + + + Magento\Config\Model\Config\Source\Yesno + When you enable this option your site may slow down. + + 1 + + + + +
+ + diff --git a/app/code/Magento/AdvancedSearch/etc/config.xml b/app/code/Magento/AdvancedSearch/etc/config.xml new file mode 100644 index 0000000000000..a4affbccdbc4e --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/config.xml @@ -0,0 +1,21 @@ + + + + + + + 1 + 2 + 0 + 1 + 5 + 0 + + + + diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema.xml b/app/code/Magento/AdvancedSearch/etc/db_schema.xml new file mode 100644 index 0000000000000..9fae40411098c --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/db_schema.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + +
+
diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema_whitelist.json b/app/code/Magento/AdvancedSearch/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..eaf7f3d616736 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/db_schema_whitelist.json @@ -0,0 +1,14 @@ +{ + "catalogsearch_recommendations": { + "column": { + "id": true, + "query_id": true, + "relation_id": true + }, + "constraint": { + "PRIMARY": true, + "CATALOGSEARCH_RECOMMENDATIONS_QUERY_ID_SEARCH_QUERY_QUERY_ID": true, + "CATALOGSEARCH_RECOMMENDATIONS_RELATION_ID_SEARCH_QUERY_QUERY_ID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/AdvancedSearch/etc/di.xml b/app/code/Magento/AdvancedSearch/etc/di.xml new file mode 100644 index 0000000000000..9ec75f56bbf7b --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/di.xml @@ -0,0 +1,30 @@ + + + + + + Magento\AdvancedSearch\Model\Recommendations\DataProvider + Related search terms + + + + + Magento\AdvancedSearch\Model\SuggestedQueries + Did you mean + + + + + + Magento\AdvancedSearch\Model\DataProvider\Suggestions + + + + + diff --git a/app/code/Magento/AdvancedSearch/etc/module.xml b/app/code/Magento/AdvancedSearch/etc/module.xml new file mode 100644 index 0000000000000..cc0c97f43d542 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/i18n/en_US.csv b/app/code/Magento/AdvancedSearch/i18n/en_US.csv new file mode 100644 index 0000000000000..f8210d58888ce --- /dev/null +++ b/app/code/Magento/AdvancedSearch/i18n/en_US.csv @@ -0,0 +1,26 @@ +"Related Search Terms","Related Search Terms" +"Add New Search Term","Add New Search Term" +button_label,button_label +"Missing search engine parameter.","Missing search engine parameter." +"Successful! Test again?","Successful! Test again?" +"Connection failed! Test again?","Connection failed! Test again?" +"Enable Search Recommendations","Enable Search Recommendations" +"When you enable this option your site may slow down.","When you enable this option your site may slow down." +"Search Recommendations Count","Search Recommendations Count" +"Show Results Count for Each Recommendation","Show Results Count for Each Recommendation" +"Enable Search Suggestions","Enable Search Suggestions" +"Search Suggestions Count","Search Suggestions Count" +"Show Results Count for Each Suggestion","Show Results Count for Each Suggestion" +"Related search terms","Related search terms" +"Did you mean","Did you mean" +ID,ID +"Search Query","Search Query" +Store,Store +Results,Results +Uses,Uses +"Redirect URL","Redirect URL" +"Suggested Term","Suggested Term" +Yes,Yes +No,No +Action,Action +Edit,Edit diff --git a/app/code/Magento/AdvancedSearch/registration.php b/app/code/Magento/AdvancedSearch/registration.php new file mode 100644 index 0000000000000..c82ffa8e7e4d6 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/registration.php @@ -0,0 +1,9 @@ + + + + + + + + catalog_search_grid + Magento\AdvancedSearch\Model\ResourceModel\Search\Grid\Collection + name + ASC + 1 + 1 + + 1 + + + + + + */*/edit + + getId + + + + + + query_id + query_id_selected + checkbox + query_id_selected + + + + + + ID + query_id + col-id + col-id + + + + + Search Query + query_text + + + + + Store + store_id + store + 1 + 0 + + + + + Results + num_results + number + + + + + Uses + popularity + number + + + + + Redirect URL + redirect + + + + + Suggested Term + 1 + display_in_terms + options + + + 1 + Yes + + + 0 + No + + + + + + + Action + catalog + action + getId + 0 + 0 + + + Edit + + */*/edit + + id + + + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_edit.xml b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_edit.xml new file mode 100644 index 0000000000000..5e6774b1b5c6b --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_edit.xml @@ -0,0 +1,28 @@ + + + + + + + + + + search.edit.grid + getSelectedQueries + selected_queries_grid + selected_queries_grid + + + + edit_form + + + + + + diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_relatedgrid.xml b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_relatedgrid.xml new file mode 100644 index 0000000000000..4187ba9127369 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_relatedgrid.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/requirejs-config.js b/app/code/Magento/AdvancedSearch/view/adminhtml/requirejs-config.js new file mode 100644 index 0000000000000..80369c99b8995 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/adminhtml/requirejs-config.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + testConnection: 'Magento_AdvancedSearch/js/testconnection' + } + } +}; diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml b/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml new file mode 100644 index 0000000000000..ae202cbfaf442 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/adminhtml/templates/system/config/testconnection.phtml @@ -0,0 +1,15 @@ + + diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js b/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js new file mode 100644 index 0000000000000..e28f1b4d07d94 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/adminhtml/web/js/testconnection.js @@ -0,0 +1,73 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'jquery', + 'Magento_Ui/js/modal/alert', + 'jquery/ui' +], function ($, alert) { + 'use strict'; + + $.widget('mage.testConnection', { + options: { + url: '', + elementId: '', + successText: '', + failedText: '', + fieldMapping: '' + }, + + /** + * Bind handlers to events + */ + _create: function () { + this._on({ + 'click': $.proxy(this._connect, this) + }); + }, + + /** + * Method triggers an AJAX request to check search engine connection + * @private + */ + _connect: function () { + var result = this.options.failedText, + element = $('#' + this.options.elementId), + self = this, + params = {}, + msg = ''; + + element.removeClass('success').addClass('fail'); + $.each($.parseJSON(this.options.fieldMapping), function (key, el) { + params[key] = $('#' + el).val(); + }); + $.ajax({ + url: this.options.url, + showLoader: true, + data: params + }).done(function (response) { + if (response.success) { + element.removeClass('fail').addClass('success'); + result = self.options.successText; + } else { + msg = response.errorMessage; + + if (msg) { + alert({ + content: msg + }); + } + } + }).always(function () { + $('#' + self.options.elementId + '_result').text(result); + }); + } + }); + + return $.mage.testConnection; +}); diff --git a/app/code/Magento/AdvancedSearch/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/AdvancedSearch/view/frontend/layout/catalogsearch_result_index.xml new file mode 100644 index 0000000000000..bf27fe73711e3 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/frontend/layout/catalogsearch_result_index.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml b/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml new file mode 100644 index 0000000000000..6e660555053a1 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/view/frontend/templates/search_data.phtml @@ -0,0 +1,27 @@ + +getItems(); +if (count($data)):?> +
+
getTitle()) ?>
+ +
+ escapeHtml($additionalInfo->getQueryText()) ?> + isShowResultsCount()): ?> + getResultsCount() ?> + +
+ +
+ diff --git a/app/code/Magento/Amqp/LICENSE.txt b/app/code/Magento/Amqp/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Amqp/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/Amqp/LICENSE_AFL.txt b/app/code/Magento/Amqp/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Amqp/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/Amqp/Model/Config.php b/app/code/Magento/Amqp/Model/Config.php new file mode 100644 index 0000000000000..9ec9780317a9f --- /dev/null +++ b/app/code/Magento/Amqp/Model/Config.php @@ -0,0 +1,16 @@ +getPublisherConfig(), + $this->getResponseQueueNameBuilder(), + $communicationConfig, + $rpcConnectionTimeout + ); + } + + /** + * Get publisher config. + * + * @return PublisherConfig + * + * @deprecated 100.2.0 + */ + private function getPublisherConfig() + { + return \Magento\Framework\App\ObjectManager::getInstance()->get(PublisherConfig::class); + } + + /** + * Get response queue name builder. + * + * @return ResponseQueueNameBuilder + * + * @deprecated 100.2.0 + */ + private function getResponseQueueNameBuilder() + { + return \Magento\Framework\App\ObjectManager::getInstance()->get(ResponseQueueNameBuilder::class); + } +} diff --git a/app/code/Magento/Amqp/Model/Queue.php b/app/code/Magento/Amqp/Model/Queue.php new file mode 100644 index 0000000000000..ffef398352bc7 --- /dev/null +++ b/app/code/Magento/Amqp/Model/Queue.php @@ -0,0 +1,16 @@ +get(TopologyConfig::class), + \Magento\Framework\App\ObjectManager::getInstance()->get(ExchangeInstaller::class), + \Magento\Framework\App\ObjectManager::getInstance()->get(ConfigPool::class), + \Magento\Framework\App\ObjectManager::getInstance()->get(QueueInstaller::class), + \Magento\Framework\App\ObjectManager::getInstance()->get(ConnectionTypeResolver::class), + $logger + ); + } +} diff --git a/app/code/Magento/Amqp/README.md b/app/code/Magento/Amqp/README.md new file mode 100644 index 0000000000000..a21624031d619 --- /dev/null +++ b/app/code/Magento/Amqp/README.md @@ -0,0 +1,3 @@ +# Amqp + +**Amqp** provides functionality to publish/consume messages with Amqp. diff --git a/app/code/Magento/Amqp/Setup/ConfigOptionsList.php b/app/code/Magento/Amqp/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..7b857dc2bcc2d --- /dev/null +++ b/app/code/Magento/Amqp/Setup/ConfigOptionsList.php @@ -0,0 +1,228 @@ +connectionValidator = $connectionValidator; + } + + /** + * {@inheritdoc} + */ + public function getOptions() + { + return [ + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_QUEUE_AMQP_HOST, + 'Amqp server host', + self::DEFAULT_AMQP_HOST + ), + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_PORT, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_QUEUE_AMQP_PORT, + 'Amqp server port', + self::DEFAULT_AMQP_PORT + ), + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_USER, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_QUEUE_AMQP_USER, + 'Amqp server username', + self::DEFAULT_AMQP_USER + ), + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_PASSWORD, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_QUEUE_AMQP_PASSWORD, + 'Amqp server password', + self::DEFAULT_AMQP_PASSWORD + ), + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_QUEUE_AMQP_VIRTUAL_HOST, + 'Amqp virtualhost', + self::DEFAULT_AMQP_VIRTUAL_HOST + ), + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_SSL, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_QUEUE_AMQP_SSL, + 'Amqp SSL', + self::DEFAULT_AMQP_SSL + ), + new TextConfigOption( + self::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS, + TextConfigOption::FRONTEND_WIZARD_TEXTAREA, + self::CONFIG_PATH_QUEUE_AMQP_SSL_OPTIONS, + 'Amqp SSL Options (JSON)', + self::DEFAULT_AMQP_SSL + ), + ]; + } + + /** + * {@inheritdoc} + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function createConfig(array $data, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_AMQP_HOST)) { + $configData->set(self::CONFIG_PATH_QUEUE_AMQP_HOST, $data[self::INPUT_KEY_QUEUE_AMQP_HOST]); + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_AMQP_PORT)) { + $configData->set(self::CONFIG_PATH_QUEUE_AMQP_PORT, $data[self::INPUT_KEY_QUEUE_AMQP_PORT]); + } + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_AMQP_USER)) { + $configData->set(self::CONFIG_PATH_QUEUE_AMQP_USER, $data[self::INPUT_KEY_QUEUE_AMQP_USER]); + } + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_AMQP_PASSWORD)) { + $configData->set(self::CONFIG_PATH_QUEUE_AMQP_PASSWORD, $data[self::INPUT_KEY_QUEUE_AMQP_PASSWORD]); + } + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST)) { + $configData->set( + self::CONFIG_PATH_QUEUE_AMQP_VIRTUAL_HOST, + $data[self::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST] + ); + } + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_AMQP_SSL)) { + $configData->set(self::CONFIG_PATH_QUEUE_AMQP_SSL, $data[self::INPUT_KEY_QUEUE_AMQP_SSL]); + } + if (!$this->isDataEmpty( + $data, + self::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS + )) { + $options = json_decode( + $data[self::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS], + true + ); + if ($options !== null) { + $configData->set( + self::CONFIG_PATH_QUEUE_AMQP_SSL_OPTIONS, + $options + ); + } + } + } + + return [$configData]; + } + + /** + * {@inheritdoc} + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + $errors = []; + + if (isset($options[self::INPUT_KEY_QUEUE_AMQP_HOST]) + && $options[self::INPUT_KEY_QUEUE_AMQP_HOST] !== '') { + if (!$this->isDataEmpty( + $options, + self::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS + )) { + $sslOptions = json_decode( + $options[self::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS], + true + ); + } else { + $sslOptions = null; + } + $isSslEnabled = !empty($options[self::INPUT_KEY_QUEUE_AMQP_SSL]) + && $options[self::INPUT_KEY_QUEUE_AMQP_SSL] !== 'false'; + + $result = $this->connectionValidator->isConnectionValid( + $options[self::INPUT_KEY_QUEUE_AMQP_HOST], + $options[self::INPUT_KEY_QUEUE_AMQP_PORT], + $options[self::INPUT_KEY_QUEUE_AMQP_USER], + $options[self::INPUT_KEY_QUEUE_AMQP_PASSWORD], + $options[self::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST], + $isSslEnabled, + $sslOptions + ); + + if (!$result) { + $errors[] = "Could not connect to the Amqp Server."; + } + } + + return $errors; + } + + /** + * Check if data ($data) with key ($key) is empty + * + * @param array $data + * @param string $key + * @return bool + */ + private function isDataEmpty(array $data, $key) + { + if (isset($data[$key]) && $data[$key] !== '') { + return false; + } + + return true; + } +} diff --git a/app/code/Magento/Amqp/Setup/ConnectionValidator.php b/app/code/Magento/Amqp/Setup/ConnectionValidator.php new file mode 100644 index 0000000000000..55a11286c7c43 --- /dev/null +++ b/app/code/Magento/Amqp/Setup/ConnectionValidator.php @@ -0,0 +1,72 @@ +connectionFactory = $connectionFactory; + } + + /** + * Checks Amqp Connection + * + * @param string $host + * @param string $port + * @param string $user + * @param string $password + * @param string $virtualHost + * @param bool $ssl + * @param string[]|null $sslOptions + * @return bool true if the connection succeeded, false otherwise + */ + public function isConnectionValid( + $host, + $port, + $user, + $password = '', + $virtualHost = '', + bool $ssl = false, + array $sslOptions = null + ) { + try { + $options = new FactoryOptions(); + $options->setHost($host); + $options->setPort($port); + $options->setUsername($user); + $options->setPassword($password); + $options->setVirtualHost($virtualHost); + $options->setSslEnabled($ssl); + + if ($sslOptions) { + $options->setSslOptions($sslOptions); + } + + $connection = $this->connectionFactory->create($options); + + $connection->close(); + } catch (\Exception $e) { + return false; + } + + return true; + } +} diff --git a/app/code/Magento/Amqp/Setup/Recurring.php b/app/code/Magento/Amqp/Setup/Recurring.php new file mode 100644 index 0000000000000..cc1951d84e3d0 --- /dev/null +++ b/app/code/Magento/Amqp/Setup/Recurring.php @@ -0,0 +1,38 @@ +topologyInstaller = $topologyInstaller; + } + + /** + * {@inheritdoc} + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $this->topologyInstaller->install(); + } +} diff --git a/app/code/Magento/Amqp/Test/Unit/Setup/ConfigOptionsListTest.php b/app/code/Magento/Amqp/Test/Unit/Setup/ConfigOptionsListTest.php new file mode 100644 index 0000000000000..8db9ae73034a2 --- /dev/null +++ b/app/code/Magento/Amqp/Test/Unit/Setup/ConfigOptionsListTest.php @@ -0,0 +1,231 @@ +options = [ + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_HOST => 'host', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PORT => 'port', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_USER => 'user', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PASSWORD => 'password', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST => 'virtual host', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL => 'ssl', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS => '{"ssl_option":"test"}', + ]; + + $this->objectManager = new ObjectManager($this); + $this->connectionValidatorMock = $this->getMockBuilder(\Magento\Amqp\Setup\ConnectionValidator::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->deploymentConfigMock = $this->getMockBuilder(\Magento\Framework\App\DeploymentConfig::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->model = $this->objectManager->getObject( + \Magento\Amqp\Setup\ConfigOptionsList::class, + [ + 'connectionValidator' => $this->connectionValidatorMock, + ] + ); + } + + public function testGetOptions() + { + $expectedOptions = [ + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_HOST, + 'Amqp server host', + ConfigOptionsList::DEFAULT_AMQP_HOST + ), + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PORT, + TextConfigOption::FRONTEND_WIZARD_TEXT, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_PORT, + 'Amqp server port', + ConfigOptionsList::DEFAULT_AMQP_PORT + ), + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_USER, + TextConfigOption::FRONTEND_WIZARD_TEXT, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_USER, + 'Amqp server username', + ConfigOptionsList::DEFAULT_AMQP_USER + ), + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PASSWORD, + TextConfigOption::FRONTEND_WIZARD_TEXT, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_PASSWORD, + 'Amqp server password', + ConfigOptionsList::DEFAULT_AMQP_PASSWORD + ), + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_VIRTUAL_HOST, + 'Amqp virtualhost', + ConfigOptionsList::DEFAULT_AMQP_VIRTUAL_HOST + ), + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL, + TextConfigOption::FRONTEND_WIZARD_TEXT, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_SSL, + 'Amqp SSL', + ConfigOptionsList::DEFAULT_AMQP_SSL + ), + new TextConfigOption( + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS, + TextConfigOption::FRONTEND_WIZARD_TEXTAREA, + ConfigOptionsList::CONFIG_PATH_QUEUE_AMQP_SSL_OPTIONS, + 'Amqp SSL Options (JSON)', + ConfigOptionsList::DEFAULT_AMQP_SSL + ), + ]; + $this->assertEquals($expectedOptions, $this->model->getOptions()); + } + + /** + * @param array $options + * @param array $expectedConfigData + * @dataProvider getCreateConfigDataProvider + */ + public function testCreateConfig($options, $expectedConfigData) + { + $result = $this->model->createConfig($options, $this->deploymentConfigMock); + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + /** @var \Magento\Framework\Config\Data\ConfigData $configData */ + $configData = $result[0]; + $this->assertInstanceOf(\Magento\Framework\Config\Data\ConfigData::class, $configData); + $this->assertEquals($expectedConfigData, $configData->getData()); + } + + public function testValidateInvalidConnection() + { + $expectedResult = ['Could not connect to the Amqp Server.']; + $this->connectionValidatorMock->expects($this->once())->method('isConnectionValid')->willReturn(false); + $this->assertEquals($expectedResult, $this->model->validate($this->options, $this->deploymentConfigMock)); + } + + public function testValidateValidConnection() + { + $expectedResult = []; + $this->connectionValidatorMock->expects($this->once())->method('isConnectionValid')->willReturn(true); + $this->assertEquals($expectedResult, $this->model->validate($this->options, $this->deploymentConfigMock)); + } + + public function testValidateNoOptions() + { + $expectedResult = []; + $options = []; + $this->connectionValidatorMock->expects($this->never())->method('isConnectionValid'); + $this->assertEquals($expectedResult, $this->model->validate($options, $this->deploymentConfigMock)); + } + + public function getCreateConfigDataProvider() + { + return [ + [ + [ + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_HOST => 'host', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PORT => 'port', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_USER => 'user', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PASSWORD => 'password', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST => 'virtual host', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL => 'ssl', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS => '{"ssl_option":"test"}', + ], + ['queue' => + ['amqp' => + [ + 'host' => 'host', + 'port' => 'port', + 'user' => 'user', + 'password' => 'password', + 'virtualhost' => 'virtual host', + 'ssl' => 'ssl', + 'ssl_options' => ['ssl_option' => 'test'], + ] + ] + ], + ], + [ + [ + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_HOST => 'host', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PORT => ConfigOptionsList::DEFAULT_AMQP_PORT, + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_USER => 'user', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PASSWORD => 'password', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST => 'virtual host', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL => 'ssl', + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS => '{"ssl_option":"test"}', + ], + ['queue' => + ['amqp' => + [ + 'host' => 'host', + 'port' => ConfigOptionsList::DEFAULT_AMQP_PORT, + 'user' => 'user', + 'password' => 'password', + 'virtualhost' => 'virtual host', + 'ssl' => 'ssl', + 'ssl_options' => ['ssl_option' => 'test'], + ] + ] + ], + ], + [ + [ + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_HOST => ConfigOptionsList::DEFAULT_AMQP_HOST, + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PORT => ConfigOptionsList::DEFAULT_AMQP_PORT, + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_USER => ConfigOptionsList::DEFAULT_AMQP_USER, + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_PASSWORD => ConfigOptionsList::DEFAULT_AMQP_PASSWORD, + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST => + ConfigOptionsList::DEFAULT_AMQP_VIRTUAL_HOST, + ConfigOptionsList::INPUT_KEY_QUEUE_AMQP_SSL => ConfigOptionsList::DEFAULT_AMQP_SSL, + ], + [], + ], + ]; + } +} diff --git a/app/code/Magento/Amqp/composer.json b/app/code/Magento/Amqp/composer.json new file mode 100644 index 0000000000000..23130dfb01a4e --- /dev/null +++ b/app/code/Magento/Amqp/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-amqp", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "magento/framework": "*", + "magento/framework-amqp": "*", + "magento/framework-message-queue": "*", + "php": "~7.1.3||~7.2.0" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Amqp\\": "" + } + } +} diff --git a/app/code/Magento/Amqp/etc/di.xml b/app/code/Magento/Amqp/etc/di.xml new file mode 100644 index 0000000000000..920bb72261ef9 --- /dev/null +++ b/app/code/Magento/Amqp/etc/di.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + Magento\Framework\MessageQueue\Publisher + + + Magento\Framework\MessageQueue\Rpc\Publisher + + + + + + + + + Magento\Framework\MessageQueue\Bulk\Publisher + + + Magento\Framework\MessageQueue\Bulk\Rpc\Publisher + + + + + + + + Magento\Framework\Amqp\ConnectionTypeResolver + + + + + + + Magento\Framework\Amqp\ExchangeFactory + + + + + + + Magento\Framework\Amqp\Bulk\ExchangeFactory + + + + + + + Magento\Framework\Amqp\QueueFactory + + + + + + \Magento\Framework\Amqp\Bulk\Exchange + + + diff --git a/app/code/Magento/Amqp/etc/module.xml b/app/code/Magento/Amqp/etc/module.xml new file mode 100644 index 0000000000000..1768a9b121c81 --- /dev/null +++ b/app/code/Magento/Amqp/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/Amqp/registration.php b/app/code/Magento/Amqp/registration.php new file mode 100644 index 0000000000000..17d8382c698e8 --- /dev/null +++ b/app/code/Magento/Amqp/registration.php @@ -0,0 +1,9 @@ +' . $element->getLabel() . ''; + $html .= '
' . $element->getComment() . '
'; + return $this->decorateRowHtml($element, $html); + } + + /** + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @param string $html + * @return string + */ + private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html) + { + return sprintf( + '
%s
', + $element->getHtmlId(), + $html + ); + } +} diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php new file mode 100644 index 0000000000000..34f2b7d53d9be --- /dev/null +++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php @@ -0,0 +1,53 @@ +localeResolver = $localeResolver ?: + ObjectManager::getInstance()->get(\Magento\Framework\Locale\ResolverInterface::class); + parent::__construct($context, $data); + } + + /** + * Add current time zone to comment, properly translated according to locale + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + */ + public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) + { + $timeZoneCode = $this->_localeDate->getConfigTimezone(); + $locale = $this->localeResolver->getLocale(); + $getLongTimeZoneName = \IntlTimeZone::createTimeZone($timeZoneCode) + ->getDisplayName(false, \IntlTimeZone::DISPLAY_LONG, $locale); + $element->setData( + 'comment', + sprintf("%s (%s)", $getLongTimeZoneName, $timeZoneCode) + ); + return parent::render($element); + } +} diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php new file mode 100644 index 0000000000000..2b306003b0642 --- /dev/null +++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php @@ -0,0 +1,60 @@ +subscriptionStatusProvider = $labelStatusProvider; + } + + /** + * Add Subscription status to comment + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @return string + */ + public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) + { + $element->setData( + 'comment', + $this->prepareLabelValue() + ); + return parent::render($element); + } + + /** + * Prepare label for subscription status + * + * @return string + */ + private function prepareLabelValue() + { + return __('Subscription status') . ': ' . $this->subscriptionStatusProvider->getStatus(); + } +} diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php new file mode 100644 index 0000000000000..99606e10f99d9 --- /dev/null +++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php @@ -0,0 +1,41 @@ +' . $element->getHint() . ''; + $html .= '
' . $element->getComment() . '
'; + return $this->decorateRowHtml($element, $html); + } + + /** + * Decorates row HTML for custom element style + * + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element + * @param string $html + * @return string + */ + private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html) + { + $rowHtml = sprintf('%s', $html); + $rowHtml .= sprintf( + '%s%s', + $element->getHtmlId(), + $element->getLabelHtml($element->getHtmlId(), "[WEBSITE]"), + $element->getElementHtml() + ); + return $rowHtml; + } +} diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php new file mode 100644 index 0000000000000..ff9126a83d59f --- /dev/null +++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php @@ -0,0 +1,57 @@ +config = $config; + parent::__construct($context); + } + + /** + * Provides link to BI Essentials signup + * + * @return \Magento\Framework\Controller\AbstractResult + */ + public function execute() + { + return $this->resultRedirectFactory->create()->setUrl( + $this->config->getValue($this->urlBIEssentialsConfigPath) + ); + } +} diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php new file mode 100644 index 0000000000000..cec09377770b0 --- /dev/null +++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php @@ -0,0 +1,70 @@ +reportUrlProvider = $reportUrlProvider; + parent::__construct($context); + } + + /** + * Redirect to resource with reports. + * + * @return Redirect $resultRedirect + */ + public function execute() + { + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + try { + $resultRedirect->setUrl($this->reportUrlProvider->getUrl()); + } catch (SubscriptionUpdateException $e) { + $this->getMessageManager()->addNoticeMessage($e->getMessage()); + $resultRedirect->setPath('adminhtml'); + } catch (LocalizedException $e) { + $this->getMessageManager()->addExceptionMessage($e, $e->getMessage()); + $resultRedirect->setPath('adminhtml'); + } catch (\Exception $e) { + $this->getMessageManager()->addExceptionMessage( + $e, + __('Sorry, there has been an error processing your request. Please try again later.') + ); + $resultRedirect->setPath('adminhtml'); + } + + return $resultRedirect; + } +} diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php new file mode 100644 index 0000000000000..4466c2065ee86 --- /dev/null +++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php @@ -0,0 +1,68 @@ +subscriptionHandler = $subscriptionHandler; + parent::__construct($context); + } + + /** + * Retry process of subscription. + * + * @return Redirect + */ + public function execute() + { + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + try { + $resultRedirect->setPath('adminhtml'); + $this->subscriptionHandler->processEnabled(); + } catch (LocalizedException $e) { + $this->getMessageManager()->addExceptionMessage($e, $e->getMessage()); + } catch (\Exception $e) { + $this->getMessageManager()->addExceptionMessage( + $e, + __('Sorry, there has been an error processing your request. Please try again later.') + ); + } + + return $resultRedirect; + } +} diff --git a/app/code/Magento/Analytics/Cron/CollectData.php b/app/code/Magento/Analytics/Cron/CollectData.php new file mode 100644 index 0000000000000..ff0b3e4f67638 --- /dev/null +++ b/app/code/Magento/Analytics/Cron/CollectData.php @@ -0,0 +1,53 @@ +exportDataHandler = $exportDataHandler; + $this->subscriptionStatus = $subscriptionStatus; + } + + /** + * @return bool + */ + public function execute() + { + if ($this->subscriptionStatus->getStatus() === SubscriptionStatusProvider::ENABLED) { + $this->exportDataHandler->prepareExportData(); + } + + return true; + } +} diff --git a/app/code/Magento/Analytics/Cron/SignUp.php b/app/code/Magento/Analytics/Cron/SignUp.php new file mode 100644 index 0000000000000..8f97b839ec8ee --- /dev/null +++ b/app/code/Magento/Analytics/Cron/SignUp.php @@ -0,0 +1,101 @@ +connector = $connector; + $this->configWriter = $configWriter; + $this->flagManager = $flagManager; + $this->reinitableConfig = $reinitableConfig; + } + + /** + * Execute scheduled subscription operation + * In case of failure writes message to notifications inbox + * + * @return bool + */ + public function execute() + { + $attemptsCount = $this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE); + + if (($attemptsCount === null) || ($attemptsCount <= 0)) { + $this->deleteAnalyticsCronExpr(); + $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE); + return false; + } + + $attemptsCount -= 1; + $this->flagManager->saveFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount); + $signUpResult = $this->connector->execute('signUp'); + if ($signUpResult === false) { + return false; + } + + $this->deleteAnalyticsCronExpr(); + $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE); + return true; + } + + /** + * Delete cron schedule setting into config. + * + * Delete cron schedule setting for subscription handler into config and + * re-initialize config cache to avoid auto-generate new schedule items. + * + * @return bool + */ + private function deleteAnalyticsCronExpr() + { + $this->configWriter->delete(SubscriptionHandler::CRON_STRING_PATH); + $this->reinitableConfig->reinit(); + return true; + } +} diff --git a/app/code/Magento/Analytics/Cron/Update.php b/app/code/Magento/Analytics/Cron/Update.php new file mode 100644 index 0000000000000..9062a7bac7551 --- /dev/null +++ b/app/code/Magento/Analytics/Cron/Update.php @@ -0,0 +1,92 @@ +connector = $connector; + $this->configWriter = $configWriter; + $this->reinitableConfig = $reinitableConfig; + $this->flagManager = $flagManager; + $this->analyticsToken = $analyticsToken; + } + + /** + * Execute scheduled update operation + * + * @return bool + */ + public function execute() + { + $result = false; + $attemptsCount = $this->flagManager + ->getFlagData(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE); + + if ($attemptsCount) { + $attemptsCount -= 1; + $result = $this->connector->execute('update'); + } + + if ($result || ($attemptsCount <= 0) || (!$this->analyticsToken->isTokenExist())) { + $this->flagManager + ->deleteFlag(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE); + $this->flagManager->deleteFlag(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE); + $this->configWriter->delete(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH); + $this->reinitableConfig->reinit(); + } + + return $result; + } +} diff --git a/app/code/Magento/Analytics/LICENSE.txt b/app/code/Magento/Analytics/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Analytics/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/Analytics/LICENSE_AFL.txt b/app/code/Magento/Analytics/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Analytics/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/Analytics/Model/AnalyticsToken.php b/app/code/Magento/Analytics/Model/AnalyticsToken.php new file mode 100644 index 0000000000000..ccec4d1bbe958 --- /dev/null +++ b/app/code/Magento/Analytics/Model/AnalyticsToken.php @@ -0,0 +1,92 @@ +reinitableConfig = $reinitableConfig; + $this->config = $config; + $this->configWriter = $configWriter; + } + + /** + * Get Magento BI token value. + * + * @return string|null + */ + public function getToken() + { + return $this->config->getValue($this->tokenPath); + } + + /** + * Stores Magento BI token value. + * + * @param string $value + * + * @return bool + */ + public function storeToken($value) + { + $this->configWriter->save($this->tokenPath, $value); + $this->reinitableConfig->reinit(); + + return true; + } + + /** + * Check Magento BI token value exist. + * + * @return bool + */ + public function isTokenExist() + { + return (bool)$this->getToken(); + } +} diff --git a/app/code/Magento/Analytics/Model/Config.php b/app/code/Magento/Analytics/Model/Config.php new file mode 100644 index 0000000000000..ba508187b4b9f --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config.php @@ -0,0 +1,40 @@ +data = $data; + } + + /** + * Get config value by key. + * + * @param string|null $key + * @param string|null $default + * @return array + */ + public function get($key = null, $default = null) + { + return $this->data->get($key, $default); + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php new file mode 100644 index 0000000000000..6e6f008d49f7e --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php @@ -0,0 +1,107 @@ +analyticsToken = $analyticsToken; + $this->flagManager = $flagManager; + $this->reinitableConfig = $reinitableConfig; + $this->configWriter = $configWriter; + } + + /** + * Activate process of subscription update handling. + * + * @param string $url + * @return bool + */ + public function processUrlUpdate(string $url) + { + if ($this->analyticsToken->isTokenExist()) { + if (!$this->flagManager->getFlagData(self::PREVIOUS_BASE_URL_FLAG_CODE)) { + $this->flagManager->saveFlag(self::PREVIOUS_BASE_URL_FLAG_CODE, $url); + } + + $this->flagManager + ->saveFlag(self::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue); + $this->configWriter->save(self::UPDATE_CRON_STRING_PATH, $this->cronExpression); + $this->reinitableConfig->reinit(); + } + + return true; + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php new file mode 100644 index 0000000000000..524062eec35c6 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php @@ -0,0 +1,93 @@ +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( + __('The time value is using an unsupported format. Enter a supported format and try again.') + ); + } + + $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..a5d885c80c3fc --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php @@ -0,0 +1,79 @@ +subscriptionHandler = $subscriptionHandler; + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + } + + /** + * Add additional handling after config value was saved. + * + * @return Value + * @throws LocalizedException + */ + public function afterSave() + { + try { + if ($this->isValueChanged()) { + $enabled = $this->getData('value'); + $enabled ? $this->subscriptionHandler->processEnabled() : $this->subscriptionHandler->processDisabled(); + } + } catch (\Exception $e) { + $this->_logger->error($e->getMessage()); + throw new LocalizedException(__('There was an error save new configuration value.')); + } + + return parent::afterSave(); + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php new file mode 100644 index 0000000000000..4b125949948c6 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php @@ -0,0 +1,172 @@ +configWriter = $configWriter; + $this->flagManager = $flagManager; + $this->analyticsToken = $analyticsToken; + $this->reinitableConfig = $reinitableConfig; + } + + /** + * Processing of activation MBI subscription. + * + * Activate process of subscription handling if Analytics token is not received. + * + * @return bool + */ + public function processEnabled() + { + if (!$this->analyticsToken->isTokenExist()) { + $this->setCronSchedule(); + $this->setAttemptsFlag(); + $this->reinitableConfig->reinit(); + } + + return true; + } + + /** + * Set cron schedule setting into config for activation of subscription process. + * + * @return bool + */ + private function setCronSchedule() + { + $this->configWriter->save(self::CRON_STRING_PATH, join(' ', self::CRON_EXPR_ARRAY)); + return true; + } + + /** + * Set flag as reserve counter of attempts subscription operation. + * + * @return bool + */ + private function setAttemptsFlag() + { + return $this->flagManager + ->saveFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue); + } + + /** + * Processing of deactivation MBI subscription. + * + * Disable data collection + * and interrupt subscription handling if Analytics token is not received. + * + * @return bool + */ + public function processDisabled() + { + $this->disableCollectionData(); + + if (!$this->analyticsToken->isTokenExist()) { + $this->unsetAttemptsFlag(); + } + + return true; + } + + /** + * Unset flag of attempts subscription operation. + * + * @return bool + */ + private function unsetAttemptsFlag() + { + return $this->flagManager + ->deleteFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE); + } + + /** + * Unset schedule of collection data cron. + * + * @return bool + */ + private function disableCollectionData() + { + $this->configWriter->delete(CollectionTime::CRON_SCHEDULE_PATH); + + return true; + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php new file mode 100644 index 0000000000000..2c46222216d5a --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php @@ -0,0 +1,32 @@ +getValue())) { + throw new LocalizedException(__('Please select an industry.')); + } + + return $this; + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Mapper.php b/app/code/Magento/Analytics/Model/Config/Mapper.php new file mode 100644 index 0000000000000..504690b8e4763 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Mapper.php @@ -0,0 +1,66 @@ + [ + * 'name' => 'file_name', + * 'providers' => [ + * 'reportProvider' => [ + * 'name' => 'report_provider_name', + * 'class' => 'Magento\Analytics\ReportXml\ReportProvider', + * 'parameters' =>[ + * 'name' => 'report_name', + * ], + * ], + * 'customProvider' => [ + * 'name' => 'custom_provider_name', + * 'class' => 'Magento\Analytics\Model\CustomProvider', + * ], + * ], + * ] + * ]; + */ + public function execute($configData) + { + if (!isset($configData['config'][0]['file'])) { + return []; + } + + $files = []; + foreach ($configData['config'][0]['file'] as $fileData) { + /** just one set of providers is allowed by xsd */ + $providers = reset($fileData['providers']); + foreach ($providers as $providerType => $providerDataSet) { + /** just one set of provider data is allowed by xsd */ + $providerData = reset($providerDataSet); + /** just one set of parameters is allowed by xsd */ + $providerData['parameters'] = !empty($providerData['parameters']) + ? reset($providerData['parameters']) + : []; + $providerData['parameters'] = array_map( + 'reset', + $providerData['parameters'] + ); + $providers[$providerType] = $providerData; + } + $files[$fileData['name']] = $fileData; + $files[$fileData['name']]['providers'] = $providers; + } + return $files; + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Reader.php b/app/code/Magento/Analytics/Model/Config/Reader.php new file mode 100644 index 0000000000000..8980e31627717 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Reader.php @@ -0,0 +1,52 @@ +mapper = $mapper; + $this->readers = $readers; + } + + /** + * Read configuration scope. + * + * @param string|null $scope + * @return array + */ + public function read($scope = null) + { + $data = []; + foreach ($this->readers as $reader) { + $data = array_merge_recursive($data, $reader->read($scope)); + } + + return $this->mapper->execute($data); + } +} diff --git a/app/code/Magento/Analytics/Model/Config/Source/Vertical.php b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php new file mode 100644 index 0000000000000..c9d9582ea7c7a --- /dev/null +++ b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php @@ -0,0 +1,51 @@ +verticals = $verticals; + } + + /** + * {@inheritdoc} + */ + public function toOptionArray() + { + $result = [ + ['value' => '', 'label' => __('--Please Select--')] + ]; + + foreach ($this->verticals as $vertical) { + $result[] = ['value' => $vertical, 'label' => __($vertical)]; + } + + return $result; + } +} diff --git a/app/code/Magento/Analytics/Model/ConfigInterface.php b/app/code/Magento/Analytics/Model/ConfigInterface.php new file mode 100644 index 0000000000000..caaa2e100c1c7 --- /dev/null +++ b/app/code/Magento/Analytics/Model/ConfigInterface.php @@ -0,0 +1,22 @@ + 'command_class_name'. + * + * The list may be configured in each module via '/etc/di.xml'. + * + * @var string[] + */ + private $commands; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param array $commands + * @param ObjectManagerInterface $objectManager + */ + public function __construct( + array $commands, + ObjectManagerInterface $objectManager + ) { + $this->commands = $commands; + $this->objectManager = $objectManager; + } + + /** + * Executes a command in accordance with the given name. + * + * @param string $commandName + * @return bool + * @throws NotFoundException if the command is not found. + */ + public function execute($commandName) + { + if (!array_key_exists($commandName, $this->commands)) { + throw new NotFoundException(__('Command was not found.')); + } + + /** @var \Magento\Analytics\Model\Connector\CommandInterface $command */ + $command = $this->objectManager->create($this->commands[$commandName]); + + return $command->execute(); + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/CommandInterface.php b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php new file mode 100644 index 0000000000000..7a8774fe3dba9 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php @@ -0,0 +1,21 @@ +curlFactory = $curlFactory; + $this->responseFactory = $responseFactory; + $this->converter = $converter; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function request($method, $url, array $body = [], array $headers = [], $version = '1.1') + { + $response = new \Zend_Http_Response(0, []); + + try { + $curl = $this->curlFactory->create(); + $headers = $this->applyContentTypeHeaderFromConverter($headers); + + $curl->write($method, $url, $version, $headers, $this->converter->toBody($body)); + + $result = $curl->read(); + + if ($curl->getErrno()) { + $this->logger->critical( + new \Exception( + sprintf( + 'MBI service CURL connection error #%s: %s', + $curl->getErrno(), + $curl->getError() + ) + ) + ); + + return $response; + } + + $response = $this->responseFactory->create($result); + } catch (\Exception $e) { + $this->logger->critical($e); + } + + return $response; + } + + /** + * @param array $headers + * + * @return array + */ + private function applyContentTypeHeaderFromConverter(array $headers) + { + $contentTypeHeaderKey = array_search($this->converter->getContentTypeHeader(), $headers); + if ($contentTypeHeaderKey === false) { + $headers[] = $this->converter->getContentTypeHeader(); + } + + return $headers; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php new file mode 100644 index 0000000000000..a1e1f057684f6 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php @@ -0,0 +1,29 @@ +serializer = $serializer; + } + + /** + * @param string $body + * + * @return array + */ + public function fromBody($body) + { + $decodedBody = $this->serializer->unserialize($body); + return $decodedBody === null ? [$body] : $decodedBody; + } + + /**c + * @param array $data + * + * @return string + */ + public function toBody(array $data) + { + return $this->serializer->serialize($data); + } + + /** + * @return string + */ + public function getContentTypeHeader() + { + return self::CONTENT_TYPE_HEADER; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ResponseHandlerInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ResponseHandlerInterface.php new file mode 100644 index 0000000000000..4a6633f08da55 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/Http/ResponseHandlerInterface.php @@ -0,0 +1,18 @@ +converter = $converter; + $this->responseHandlers = $responseHandlers; + } + + /** + * @param \Zend_Http_Response $response + * + * @return bool|string + */ + public function getResult(\Zend_Http_Response $response) + { + $result = false; + $responseBody = $this->converter->fromBody($response->getBody()); + if (array_key_exists($response->getStatus(), $this->responseHandlers)) { + $result = $this->responseHandlers[$response->getStatus()]->handleResponse($responseBody); + } + + return $result; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php new file mode 100644 index 0000000000000..f1a8ea6460f9d --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php @@ -0,0 +1,93 @@ +analyticsToken = $analyticsToken; + $this->httpClient = $httpClient; + $this->config = $config; + $this->responseResolver = $responseResolver; + $this->logger = $logger; + } + + /** + * Notify MBI about that data collection was finished + * + * @return bool + */ + public function execute() + { + $result = false; + if ($this->analyticsToken->isTokenExist()) { + $response = $this->httpClient->request( + ZendClient::POST, + $this->config->getValue($this->notifyDataChangedUrlPath), + [ + "access-token" => $this->analyticsToken->getToken(), + "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL), + ] + ); + $result = $this->responseResolver->getResult($response); + } + return (bool)$result; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/OTPRequest.php b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php new file mode 100644 index 0000000000000..dfa283e10d070 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php @@ -0,0 +1,115 @@ +analyticsToken = $analyticsToken; + $this->httpClient = $httpClient; + $this->config = $config; + $this->responseResolver = $responseResolver; + $this->logger = $logger; + } + + /** + * Performs obtaining of an OTP from the MBI service. + * + * Returns received OTP or FALSE in case of failure. + * + * @return string|false + */ + public function call() + { + $result = false; + + if ($this->analyticsToken->isTokenExist()) { + $response = $this->httpClient->request( + ZendClient::POST, + $this->config->getValue($this->otpUrlConfigPath), + [ + "access-token" => $this->analyticsToken->getToken(), + "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL), + ] + ); + + $result = $this->responseResolver->getResult($response); + if (!$result) { + $this->logger->warning( + sprintf( + 'Obtaining of an OTP from the MBI service has been failed: %s', + !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.' + ) + ); + } + } + + return $result; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php new file mode 100644 index 0000000000000..d9a672e81f43d --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php @@ -0,0 +1,24 @@ +analyticsToken = $analyticsToken; + $this->subscriptionHandler = $subscriptionHandler; + $this->subscriptionStatusProvider = $subscriptionStatusProvider; + } + + /** + * @inheritdoc + */ + public function handleResponse(array $responseBody) + { + if ($this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::ENABLED) { + $this->analyticsToken->storeToken(null); + $this->subscriptionHandler->processEnabled(); + } + return false; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php new file mode 100644 index 0000000000000..db5f7be47cad7 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php @@ -0,0 +1,42 @@ +analyticsToken = $analyticsToken; + } + + /** + * @inheritdoc + */ + public function handleResponse(array $body) + { + if (isset($body['access-token']) && !empty($body['access-token'])) { + $this->analyticsToken->storeToken($body['access-token']); + return $body['access-token']; + } + + return false; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php new file mode 100644 index 0000000000000..73fc575ae2821 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php @@ -0,0 +1,24 @@ +analyticsToken = $analyticsToken; + $this->integrationManager = $integrationManager; + $this->config = $config; + $this->httpClient = $httpClient; + $this->logger = $logger; + $this->responseResolver = $responseResolver; + } + + /** + * Executes signUp command + * + * During this call Magento generates or retrieves access token for the integration user + * In case successful generation Magento activates user and sends access token to MA + * As the response, Magento receives a token to MA + * Magento stores this token in System Configuration + * + * This method returns true in case of success + * + * @return bool + */ + public function execute() + { + $result = false; + $integrationToken = $this->integrationManager->generateToken(); + if ($integrationToken) { + $this->integrationManager->activateIntegration(); + $response = $this->httpClient->request( + ZendClient::POST, + $this->config->getValue($this->signUpUrlPath), + [ + "token" => $integrationToken->getData('token'), + "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL), + ] + ); + + $result = $this->responseResolver->getResult($response); + if (!$result) { + $this->logger->warning( + sprintf( + 'Subscription for MBI service has been failed. An error occurred during token exchange: %s', + !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.' + ) + ); + } + } + + return (bool)$result; + } +} diff --git a/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php new file mode 100644 index 0000000000000..7b4e452a7b451 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php @@ -0,0 +1,113 @@ +analyticsToken = $analyticsToken; + $this->httpClient = $httpClient; + $this->config = $config; + $this->logger = $logger; + $this->flagManager = $flagManager; + $this->responseResolver = $responseResolver; + } + + /** + * Executes update request to MBI api in case store url was changed + * + * @return bool + */ + public function execute() + { + $result = false; + if ($this->analyticsToken->isTokenExist()) { + $response = $this->httpClient->request( + ZendClient::PUT, + $this->config->getValue($this->updateUrlPath), + [ + "url" => $this->flagManager + ->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE), + "new-url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL), + "access-token" => $this->analyticsToken->getToken(), + ] + ); + $result = $this->responseResolver->getResult($response); + if (!$result) { + $this->logger->warning( + sprintf( + 'Update of the subscription for MBI service has been failed: %s', + !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.' + ) + ); + } + } + + return (bool)$result; + } +} diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php new file mode 100644 index 0000000000000..665d564814b14 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Cryptographer.php @@ -0,0 +1,135 @@ +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( + __( + 'The data is invalid. ' + . 'Enter the data as a string or data that can be converted into a string and try again.' + ) + ); + } + } elseif (!$source) { + throw new LocalizedException(__('The data is invalid. Enter the data as a string and try again.')); + } + if (!$this->validateCipherMethod($this->cipherMethod)) { + throw new LocalizedException(__('The data is invalid. Use a valid cipher method and try again.')); + } + $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(__('Enter the encryption key and try again.')); + } + return hash('sha256', $token); + } + + /** + * Return established cipher method. + * + * @return string + */ + private function getCipherMethod() + { + return $this->cipherMethod; + } + + /** + * Return each time generated random initialization vector which depends on the cipher method. + * + * @return string + */ + private function getInitializationVector() + { + $ivSize = openssl_cipher_iv_length($this->getCipherMethod()); + return openssl_random_pseudo_bytes($ivSize); + } + + /** + * Check that cipher method is allowed for encryption. + * + * @param string $cipherMethod + * @return bool + */ + private function validateCipherMethod($cipherMethod) + { + $methods = openssl_get_cipher_methods(); + return (false !== array_search($cipherMethod, $methods)); + } +} diff --git a/app/code/Magento/Analytics/Model/EncodedContext.php b/app/code/Magento/Analytics/Model/EncodedContext.php new file mode 100644 index 0000000000000..5fb2d0c15aef7 --- /dev/null +++ b/app/code/Magento/Analytics/Model/EncodedContext.php @@ -0,0 +1,52 @@ +content = $content; + $this->initializationVector = $initializationVector; + } + + /** + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function getInitializationVector() + { + return $this->initializationVector; + } +} diff --git a/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php new file mode 100644 index 0000000000000..5d127037afea9 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php @@ -0,0 +1,17 @@ +filesystem = $filesystem; + $this->archive = $archive; + $this->reportWriter = $reportWriter; + $this->cryptographer = $cryptographer; + $this->fileRecorder = $fileRecorder; + } + + /** + * @inheritdoc + */ + public function prepareExportData() + { + try { + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + + $this->prepareDirectory($tmpDirectory, $this->getTmpFilesDirRelativePath()); + $this->reportWriter->write($tmpDirectory, $this->getTmpFilesDirRelativePath()); + + $tmpFilesDirectoryAbsolutePath = $this->validateSource($tmpDirectory, $this->getTmpFilesDirRelativePath()); + $archiveAbsolutePath = $this->prepareFileDirectory($tmpDirectory, $this->getArchiveRelativePath()); + $this->pack( + $tmpFilesDirectoryAbsolutePath, + $archiveAbsolutePath + ); + + $this->validateSource($tmpDirectory, $this->getArchiveRelativePath()); + $this->fileRecorder->recordNewFile( + $this->cryptographer->encode($tmpDirectory->readFile($this->getArchiveRelativePath())) + ); + } finally { + $tmpDirectory->delete($this->getTmpFilesDirRelativePath()); + $tmpDirectory->delete($this->getArchiveRelativePath()); + } + + return true; + } + + /** + * Return relative path to a directory for temporary files with reports data. + * + * @return string + */ + private function getTmpFilesDirRelativePath() + { + return $this->subdirectoryPath . 'tmp/'; + } + + /** + * Return relative path to a directory for an archive. + * + * @return string + */ + private function getArchiveRelativePath() + { + return $this->subdirectoryPath . $this->archiveName; + } + + /** + * Clean up a directory. + * + * @param WriteInterface $directory + * @param string $path + * @return string + */ + private function prepareDirectory(WriteInterface $directory, $path) + { + $directory->delete($path); + + return $directory->getAbsolutePath($path); + } + + /** + * Remove a file and a create parent directory a file. + * + * @param WriteInterface $directory + * @param string $path + * @return string + */ + private function prepareFileDirectory(WriteInterface $directory, $path) + { + $directory->delete($path); + if (dirname($path) !== '.') { + $directory->create(dirname($path)); + } + + return $directory->getAbsolutePath($path); + } + + /** + * Packing data into an archive. + * + * @param string $source + * @param string $destination + * @return bool + */ + private function pack($source, $destination) + { + $this->archive->pack( + $source, + $destination, + is_dir($source) ?: false + ); + + return true; + } + + /** + * Validate that data source exist. + * + * Return absolute path in a validated data source. + * + * @param WriteInterface $directory + * @param string $path + * @return string + * @throws LocalizedException If source is not exist. + */ + private function validateSource(WriteInterface $directory, $path) + { + if (!$directory->isExist($path)) { + throw new LocalizedException(__('The "%1" source doesn\'t exist.', $directory->getAbsolutePath($path))); + } + + return $directory->getAbsolutePath($path); + } +} diff --git a/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php new file mode 100644 index 0000000000000..65efb33659c89 --- /dev/null +++ b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php @@ -0,0 +1,19 @@ +exportDataHandler = $exportDataHandler; + $this->analyticsConnector = $connector; + } + + /** + * {@inheritdoc} + * Execute notification command. + * + * @return bool + */ + public function prepareExportData() + { + $result = $this->exportDataHandler->prepareExportData(); + $this->analyticsConnector->execute('notifyDataChanged'); + return $result; + } +} diff --git a/app/code/Magento/Analytics/Model/FileInfo.php b/app/code/Magento/Analytics/Model/FileInfo.php new file mode 100644 index 0000000000000..19bdaf21b2a20 --- /dev/null +++ b/app/code/Magento/Analytics/Model/FileInfo.php @@ -0,0 +1,52 @@ +path = $path; + $this->initializationVector = $initializationVector; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @return string + */ + public function getInitializationVector() + { + return $this->initializationVector; + } +} diff --git a/app/code/Magento/Analytics/Model/FileInfoManager.php b/app/code/Magento/Analytics/Model/FileInfoManager.php new file mode 100644 index 0000000000000..e37700e665420 --- /dev/null +++ b/app/code/Magento/Analytics/Model/FileInfoManager.php @@ -0,0 +1,123 @@ +flagManager = $flagManager; + $this->fileInfoFactory = $fileInfoFactory; + } + + /** + * Save FileInfo object. + * + * @param FileInfo $fileInfo + * @return bool + * @throws LocalizedException + */ + public function save(FileInfo $fileInfo) + { + $parameters = []; + $parameters['initializationVector'] = $fileInfo->getInitializationVector(); + $parameters['path'] = $fileInfo->getPath(); + + $emptyParameters = array_diff($parameters, array_filter($parameters)); + if ($emptyParameters) { + throw new LocalizedException( + __('These arguments can\'t be empty "%1"', implode(', ', array_keys($emptyParameters))) + ); + } + + foreach ($this->encodedParameters as $encodedParameter) { + $parameters[$encodedParameter] = $this->encodeValue($parameters[$encodedParameter]); + } + + $this->flagManager->saveFlag($this->flagCode, $parameters); + + return true; + } + + /** + * Load FileInfo object. + * + * @return FileInfo + */ + public function load() + { + $parameters = $this->flagManager->getFlagData($this->flagCode) ?: []; + + $encodedParameters = array_intersect($this->encodedParameters, array_keys($parameters)); + foreach ($encodedParameters as $encodedParameter) { + $parameters[$encodedParameter] = $this->decodeValue($parameters[$encodedParameter]); + } + + $fileInfo = $this->fileInfoFactory->create($parameters); + + return $fileInfo; + } + + /** + * Encode value. + * + * @param string $value + * @return string + */ + private function encodeValue($value) + { + return base64_encode($value); + } + + /** + * Decode value. + * + * @param string $value + * @return string + */ + private function decodeValue($value) + { + return base64_decode($value); + } +} diff --git a/app/code/Magento/Analytics/Model/FileRecorder.php b/app/code/Magento/Analytics/Model/FileRecorder.php new file mode 100644 index 0000000000000..70438a98d56f1 --- /dev/null +++ b/app/code/Magento/Analytics/Model/FileRecorder.php @@ -0,0 +1,136 @@ +fileInfoManager = $fileInfoManager; + $this->fileInfoFactory = $fileInfoFactory; + $this->filesystem = $filesystem; + } + + /** + * Save new encrypted file, register it and remove old registered file. + * + * @param EncodedContext $encodedContext + * @return bool + */ + public function recordNewFile(EncodedContext $encodedContext) + { + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + + $fileRelativePath = $this->getFileRelativePath(); + $directory->writeFile($fileRelativePath, $encodedContext->getContent()); + + $fileInfo = $this->fileInfoManager->load(); + $this->registerFile($encodedContext, $fileRelativePath); + $this->removeOldFile($fileInfo, $directory); + + return true; + } + + /** + * Return relative path to encoded file. + * + * @return string + */ + private function getFileRelativePath() + { + return $this->fileSubdirectoryPath . hash('sha256', time()) + . '/' . $this->encodedFileName; + } + + /** + * Register encoded file. + * + * @param EncodedContext $encodedContext + * @param string $fileRelativePath + * @return bool + */ + private function registerFile(EncodedContext $encodedContext, $fileRelativePath) + { + $newFileInfo = $this->fileInfoFactory->create( + [ + 'path' => $fileRelativePath, + 'initializationVector' => $encodedContext->getInitializationVector(), + ] + ); + $this->fileInfoManager->save($newFileInfo); + + return true; + } + + /** + * Remove previously registered file. + * + * @param FileInfo $fileInfo + * @param WriteInterface $directory + * @return bool + */ + private function removeOldFile(FileInfo $fileInfo, WriteInterface $directory) + { + if (!$fileInfo->getPath()) { + return true; + } + + $directory->delete($fileInfo->getPath()); + + $directoryName = dirname($fileInfo->getPath()); + if ($directoryName !== '.') { + $directory->delete($directoryName); + } + + return true; + } +} diff --git a/app/code/Magento/Analytics/Model/IntegrationManager.php b/app/code/Magento/Analytics/Model/IntegrationManager.php new file mode 100644 index 0000000000000..f0d7fe9994232 --- /dev/null +++ b/app/code/Magento/Analytics/Model/IntegrationManager.php @@ -0,0 +1,124 @@ +integrationService = $integrationService; + $this->config = $config; + $this->oauthService = $oauthService; + } + + /** + * Activate predefined integration user + * + * @return bool + * @throws NoSuchEntityException + */ + public function activateIntegration() + { + $integration = $this->integrationService->findByName( + $this->config->getConfigDataValue('analytics/integration_name') + ); + if (!$integration->getId()) { + throw new NoSuchEntityException(__('Cannot find predefined integration user!')); + } + $integrationData = $this->getIntegrationData(Integration::STATUS_ACTIVE); + $integrationData['integration_id'] = $integration->getId(); + $this->integrationService->update($integrationData); + return true; + } + + /** + * This method execute Generate Token command and enable integration + * + * @return bool|\Magento\Integration\Model\Oauth\Token + */ + public function generateToken() + { + $consumerId = $this->generateIntegration()->getConsumerId(); + $accessToken = $this->oauthService->getAccessToken($consumerId); + if (!$accessToken && $this->oauthService->createAccessToken($consumerId, true)) { + $accessToken = $this->oauthService->getAccessToken($consumerId); + } + return $accessToken; + } + + /** + * Returns consumer Id for MA integration user + * + * @return \Magento\Integration\Model\Integration + */ + private function generateIntegration() + { + $integration = $this->integrationService->findByName( + $this->config->getConfigDataValue('analytics/integration_name') + ); + if (!$integration->getId()) { + $integration = $this->integrationService->create($this->getIntegrationData()); + } + return $integration; + } + + /** + * Returns default attributes for MA integration user + * + * @param int $status + * @return array + */ + private function getIntegrationData($status = Integration::STATUS_INACTIVE) + { + $integrationData = [ + 'name' => $this->config->getConfigDataValue('analytics/integration_name'), + 'status' => $status, + 'all_resources' => false, + 'resource' => [ + 'Magento_Analytics::analytics', + 'Magento_Analytics::analytics_api' + ], + ]; + return $integrationData; + } +} diff --git a/app/code/Magento/Analytics/Model/Link.php b/app/code/Magento/Analytics/Model/Link.php new file mode 100644 index 0000000000000..7bb11eda2cc8b --- /dev/null +++ b/app/code/Magento/Analytics/Model/Link.php @@ -0,0 +1,50 @@ +url = $url; + $this->initializationVector = $initializationVector; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return string + */ + public function getInitializationVector() + { + return $this->initializationVector; + } +} diff --git a/app/code/Magento/Analytics/Model/LinkProvider.php b/app/code/Magento/Analytics/Model/LinkProvider.php new file mode 100644 index 0000000000000..2474653f4916c --- /dev/null +++ b/app/code/Magento/Analytics/Model/LinkProvider.php @@ -0,0 +1,87 @@ +linkFactory = $linkFactory; + $this->fileInfoManager = $fileInfoManager; + $this->storeManager = $storeManager; + } + + /** + * Returns base url to file according to store configuration + * + * @param FileInfo $fileInfo + * @return string + */ + private function getBaseUrl(FileInfo $fileInfo) + { + return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $fileInfo->getPath(); + } + + /** + * Verify is requested file ready + * + * @param FileInfo $fileInfo + * @return bool + */ + private function isFileReady(FileInfo $fileInfo) + { + return $fileInfo->getPath() && $fileInfo->getInitializationVector(); + } + + /** + * @inheritdoc + */ + public function get() + { + $fileInfo = $this->fileInfoManager->load(); + if (!$this->isFileReady($fileInfo)) { + throw new NoSuchEntityException(__('File is not ready yet.')); + } + return $this->linkFactory->create( + [ + 'url' => $this->getBaseUrl($fileInfo), + 'initializationVector' => base64_encode($fileInfo->getInitializationVector()) + ] + ); + } +} diff --git a/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php new file mode 100644 index 0000000000000..174272614fb19 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php @@ -0,0 +1,61 @@ +subscriptionUpdateHandler = $subscriptionUpdateHandler; + } + + /** + * Add additional handling after config value was saved. + * + * @param Value $subject + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterAfterSave( + Value $subject, + Value $result + ) { + if ($this->isPluginApplicable($result)) { + $this->subscriptionUpdateHandler->processUrlUpdate($result->getOldValue()); + } + + return $result; + } + + /** + * @param Value $result + * @return bool + */ + private function isPluginApplicable(Value $result) + { + return $result->isValueChanged() + && ($result->getPath() === Store::XML_PATH_SECURE_BASE_URL) + && ($result->getScope() === ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + } +} diff --git a/app/code/Magento/Analytics/Model/ProviderFactory.php b/app/code/Magento/Analytics/Model/ProviderFactory.php new file mode 100644 index 0000000000000..421b67ea2b2a2 --- /dev/null +++ b/app/code/Magento/Analytics/Model/ProviderFactory.php @@ -0,0 +1,37 @@ +objectManager = $objectManager; + } + + /** + * @param string $providerName + * @return object + */ + public function create($providerName) + { + return $this->objectManager->get($providerName); + } +} diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php new file mode 100644 index 0000000000000..e7fdf6f9e8132 --- /dev/null +++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php @@ -0,0 +1,94 @@ +analyticsToken = $analyticsToken; + $this->otpRequest = $otpRequest; + $this->config = $config; + $this->flagManager = $flagManager; + } + + /** + * Provide URL on resource with reports. + * + * @return string + * @throws SubscriptionUpdateException + */ + public function getUrl() + { + if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) { + throw new SubscriptionUpdateException(__( + 'Your Base URL has been changed and your reports are being updated. ' + . 'Advanced Reporting will be available once this change has been processed. Please try again later.' + )); + } + + $url = $this->config->getValue($this->urlReportConfigPath); + if ($this->analyticsToken->isTokenExist()) { + $otp = $this->otpRequest->call(); + if ($otp) { + $query = http_build_query(['otp' => $otp], '', '&'); + $url .= '?' . $query; + } + } + + return $url; + } +} diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php new file mode 100644 index 0000000000000..7128658947908 --- /dev/null +++ b/app/code/Magento/Analytics/Model/ReportWriter.php @@ -0,0 +1,101 @@ +config = $config; + $this->reportValidator = $reportValidator; + $this->providerFactory = $providerFactory; + } + + /** + * {@inheritdoc} + */ + public function write(WriteInterface $directory, $path) + { + $errorsList = []; + foreach ($this->config->get() as $file) { + $provider = reset($file['providers']); + if (isset($provider['parameters']['name'])) { + $error = $this->reportValidator->validate($provider['parameters']['name']); + if ($error) { + $errorsList[] = $error; + continue; + } + } + /** @var $providerObject */ + $providerObject = $this->providerFactory->create($provider['class']); + $fileName = $provider['parameters'] ? $provider['parameters']['name'] : $provider['name']; + $fileFullPath = $path . $fileName . '.csv'; + $fileData = $providerObject->getReport(...array_values($provider['parameters'])); + $stream = $directory->openFile($fileFullPath, 'w+'); + $stream->lock(); + $headers = []; + foreach ($fileData as $row) { + if (!$headers) { + $headers = array_keys($row); + $stream->writeCsv($headers); + } + $stream->writeCsv($row); + } + $stream->unlock(); + $stream->close(); + } + if ($errorsList) { + $errorStream = $directory->openFile($path . $this->errorsFileName, 'w+'); + foreach ($errorsList as $error) { + $errorStream->lock(); + $errorStream->writeCsv($error); + $errorStream->unlock(); + } + $errorStream->close(); + } + + return true; + } +} diff --git a/app/code/Magento/Analytics/Model/ReportWriterInterface.php b/app/code/Magento/Analytics/Model/ReportWriterInterface.php new file mode 100644 index 0000000000000..a611095a47ae4 --- /dev/null +++ b/app/code/Magento/Analytics/Model/ReportWriterInterface.php @@ -0,0 +1,28 @@ +moduleManager = $moduleManager; + } + + /** + * Returns module with module status + * + * @return array + */ + public function current() + { + $current = parent::current(); + if (is_array($current) && isset($current['module_name'])) { + $current['status'] = + $this->moduleManager->isEnabled($current['module_name']) == 1 ? 'Enabled' : "Disabled"; + } + return $current; + } +} diff --git a/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php new file mode 100644 index 0000000000000..d010aeb19106d --- /dev/null +++ b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php @@ -0,0 +1,101 @@ +scopeConfig = $scopeConfig; + $this->configPaths = $configPaths; + $this->storeManager = $storeManager; + } + + /** + * Generates report using config paths from di.xml + * For each website and store + * @return \IteratorIterator + */ + public function getReport() + { + $configReport = $this->generateReportForScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0); + + /** @var WebsiteInterface $website */ + foreach ($this->storeManager->getWebsites() as $website) { + $configReport = array_merge( + $this->generateReportForScope(ScopeInterface::SCOPE_WEBSITES, $website->getId()), + $configReport + ); + } + + /** @var StoreInterface $store */ + foreach ($this->storeManager->getStores() as $store) { + $configReport = array_merge( + $this->generateReportForScope(ScopeInterface::SCOPE_STORES, $store->getId()), + $configReport + ); + } + return new \IteratorIterator(new \ArrayIterator($configReport)); + } + + /** + * Creates report from config for scope type and scope id. + * + * @param string $scope + * @param int $scopeId + * @return array + */ + private function generateReportForScope($scope, $scopeId) + { + $report = []; + foreach ($this->configPaths as $configPath) { + $report[] = [ + "config_path" => $configPath, + "scope" => $scope, + "scope_id" => $scopeId, + "value" => $this->scopeConfig->getValue( + $configPath, + $scope, + $scopeId + ) + ]; + } + return $report; + } +} diff --git a/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php new file mode 100644 index 0000000000000..1dd831a672faa --- /dev/null +++ b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php @@ -0,0 +1,120 @@ +scopeConfig = $scopeConfig; + $this->analyticsToken = $analyticsToken; + $this->flagManager = $flagManager; + } + + /** + * Retrieve subscription status to Magento BI Advanced Reporting. + * + * Statuses: + * Enabled - if subscription is enabled and MA token was received; + * Pending - if subscription is enabled and MA token was not received; + * Disabled - if subscription is not enabled. + * Failed - if subscription is enabled and token was not received after attempts ended. + * + * @return string + */ + public function getStatus() + { + $isSubscriptionEnabledInConfig = $this->scopeConfig->getValue('analytics/subscription/enabled'); + if ($isSubscriptionEnabledInConfig) { + return $this->getStatusForEnabledSubscription(); + } + + return $this->getStatusForDisabledSubscription(); + } + + /** + * Retrieve status for subscription that enabled in config. + * + * @return string + */ + public function getStatusForEnabledSubscription() + { + $status = static::ENABLED; + if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) { + $status = self::PENDING; + } + + if (!$this->analyticsToken->isTokenExist()) { + $status = static::PENDING; + if ($this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) === null) { + $status = static::FAILED; + } + } + + return $status; + } + + /** + * Retrieve status for subscription that disabled in config. + * + * @return string + */ + public function getStatusForDisabledSubscription() + { + return static::DISABLED; + } +} diff --git a/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php new file mode 100644 index 0000000000000..a30168202cac7 --- /dev/null +++ b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php @@ -0,0 +1,78 @@ +subscriptionStatusProvider = $subscriptionStatusProvider; + $this->urlBuilder = $urlBuilder; + } + + /** + * @inheritdoc + * + * @codeCoverageIgnore + */ + public function getIdentity() + { + return hash('sha256', 'ANALYTICS_NOTIFICATION'); + } + + /** + * {@inheritdoc} + */ + public function isDisplayed() + { + return $this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::FAILED; + } + + /** + * {@inheritdoc} + */ + public function getText() + { + $messageDetails = ''; + + $messageDetails .= __('Failed to synchronize data to the Magento Business Intelligence service. '); + $messageDetails .= '' + . __('Retry Synchronization') . ''; + + return $messageDetails; + } + + /** + * @inheritdoc + * + * @codeCoverageIgnore + */ + public function getSeverity() + { + return self::SEVERITY_MAJOR; + } +} diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md new file mode 100644 index 0000000000000..7ec64abcd9b86 --- /dev/null +++ b/app/code/Magento/Analytics/README.md @@ -0,0 +1,41 @@ +# Magento_Analytics Module + +The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality. + +The module implements the following functionality: + +* enabling subscription to the MBI and automatic re-subscription +* changing the base URL with the same MBI account remained +* declaring the configuration schemas for report data collection +* collecting the Magento instance data as reports for the MBI +* introducing API that provides the collected data +* extending Magento configuration with the module parameters: + * subscription status (enabled/disabled) + * industry (a business area in which the instance website works) + * time of data collection (time of the day when the module collects data) + +## Structure + +Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`. +[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting. +The language declares SQL queries using XML declaration. + +## Subscription Process + +The subscription to the MBI service is enabled during the installation process of the Analytics module. Each administrator will be notified of these new features upon their initial login to the Admin Panel. + +## Analytics Settings + +Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab. + +The following options can be adjusted: +* Advanced Reporting Service (Enabled/Disabled) + * Alters the status of the Advanced Reporting subscription +* Time of day to send data (Hour/Minute/Second in the store's time zone) + * Defines when the data collection process for the Advanced Reporting service occurs +* Industry + * Defines the industry of the store in order to create a personalized Advanced Reporting experience + +## Extensibility + +We do not recommend to extend the Magento_Analytics module. It introduces an API that is purposed to transfer the collected data. Note that the API cannot be used for other needs. diff --git a/app/code/Magento/Analytics/ReportXml/Config.php b/app/code/Magento/Analytics/ReportXml/Config.php new file mode 100644 index 0000000000000..1edf4ef6e212e --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/Config.php @@ -0,0 +1,41 @@ +data = $data; + } + + /** + * Returns config value by name + * + * @param string $queryName + * @return array + */ + public function get($queryName) + { + return $this->data->get($queryName); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php new file mode 100644 index 0000000000000..9e0b20a6ad414 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php @@ -0,0 +1,61 @@ +hasAttributes()) { + $attrs = $source->attributes; + foreach ($attrs as $attr) { + $result[$attr->name] = $attr->value; + } + } + if ($source->hasChildNodes()) { + $children = $source->childNodes; + if ($children->length == 1) { + $child = $children->item(0); + if ($child->nodeType == XML_TEXT_NODE) { + $result['_value'] = $child->nodeValue; + return count($result) == 1 ? $result['_value'] : $result; + } + } + foreach ($children as $child) { + if ($child instanceof \DOMCharacterData) { + continue; + } + $result[$child->nodeName][] = $this->convertNode($child); + } + } + return $result; + } + + /** + * Converts XML document into corresponding array. + * + * @param \DOMDocument $source + * @return array + */ + public function convert($source) + { + return $this->convertNode($source); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/Config/Mapper.php b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php new file mode 100644 index 0000000000000..4dda8f3c733a6 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php @@ -0,0 +1,37 @@ +readers = $readers; + $this->mapper = $mapper; + } + + /** + * Reads configuration according to the given scope. + * + * @param string|null $scope + * @return array + */ + public function read($scope = null) + { + $data = []; + foreach ($this->readers as $reader) { + $data = array_merge_recursive($data, $reader->read($scope)); + } + return $this->mapper->execute($data); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/ConfigInterface.php b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php new file mode 100644 index 0000000000000..ec03ddf429c06 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php @@ -0,0 +1,23 @@ +resourceConnection = $resourceConnection; + $this->objectManager = $objectManager; + } + + /** + * Creates one-time connection for export + * + * @param string $connectionName + * @return AdapterInterface + */ + public function getConnection($connectionName) + { + $connection = $this->resourceConnection->getConnection($connectionName); + $connectionClassName = get_class($connection); + $configData = $connection->getConfig(); + $configData['use_buffered_query'] = false; + unset($configData['persistent']); + return $this->objectManager->create( + $connectionClassName, + [ + 'config' => $configData + ] + ); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php new file mode 100644 index 0000000000000..083b4843c185a --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php @@ -0,0 +1,27 @@ +conditionResolver = $conditionResolver; + $this->nameResolver = $nameResolver; + } + + /** + * Assembles WHERE conditions + * + * @param SelectBuilder $selectBuilder + * @param array $queryConfig + * @return SelectBuilder + */ + public function assemble(SelectBuilder $selectBuilder, $queryConfig) + { + if (!isset($queryConfig['source']['filter'])) { + return $selectBuilder; + } + $filters = $this->conditionResolver->getFilter( + $selectBuilder, + $queryConfig['source']['filter'], + $this->nameResolver->getAlias($queryConfig['source']) + ); + $selectBuilder->setFilters(array_merge_recursive($selectBuilder->getFilters(), [$filters])); + return $selectBuilder; + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php new file mode 100644 index 0000000000000..811119ace221b --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php @@ -0,0 +1,69 @@ +nameResolver = $nameResolver; + $this->columnsResolver = $columnsResolver; + $this->resourceConnection = $resourceConnection; + } + + /** + * Assembles FROM condition + * + * @param SelectBuilder $selectBuilder + * @param array $queryConfig + * @return SelectBuilder + */ + public function assemble(SelectBuilder $selectBuilder, $queryConfig) + { + $selectBuilder->setFrom( + [ + $this->nameResolver->getAlias($queryConfig['source']) => + $this->resourceConnection + ->getTableName($this->nameResolver->getName($queryConfig['source'])), + ] + ); + $columns = $this->columnsResolver->getColumns($selectBuilder, $queryConfig['source']); + $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns)); + return $selectBuilder; + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php new file mode 100644 index 0000000000000..82a06f824d468 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php @@ -0,0 +1,111 @@ +conditionResolver = $conditionResolver; + $this->nameResolver = $nameResolver; + $this->columnsResolver = $columnsResolver; + $this->resourceConnection = $resourceConnection; + } + + /** + * Assembles JOIN conditions + * + * @param SelectBuilder $selectBuilder + * @param array $queryConfig + * @return SelectBuilder + */ + public function assemble(SelectBuilder $selectBuilder, $queryConfig) + { + if (!isset($queryConfig['source']['link-source'])) { + return $selectBuilder; + } + $joins = []; + $filters = $selectBuilder->getFilters(); + + $sourceAlias = $this->nameResolver->getAlias($queryConfig['source']); + + foreach ($queryConfig['source']['link-source'] as $join) { + $joinAlias = $this->nameResolver->getAlias($join); + + $joins[$joinAlias] = [ + 'link-type' => isset($join['link-type']) ? $join['link-type'] : 'left', + 'table' => [ + $joinAlias => $this->resourceConnection + ->getTableName($this->nameResolver->getName($join)), + ], + 'condition' => $this->conditionResolver->getFilter( + $selectBuilder, + $join['using'], + $joinAlias, + $sourceAlias + ) + ]; + if (isset($join['filter'])) { + $filters = array_merge( + $filters, + [ + $this->conditionResolver->getFilter( + $selectBuilder, + $join['filter'], + $joinAlias, + $sourceAlias + ) + ] + ); + } + $columns = $this->columnsResolver->getColumns($selectBuilder, isset($join['attribute']) ? $join : []); + $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns)); + } + $selectBuilder->setFilters($filters); + $selectBuilder->setJoins(array_merge($selectBuilder->getJoins(), $joins)); + return $selectBuilder; + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php new file mode 100644 index 0000000000000..14b80c6814ba6 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php @@ -0,0 +1,98 @@ +nameResolver = $nameResolver; + $this->resourceConnection = $resourceConnection; + } + + /** + * Returns connection + * + * @return \Magento\Framework\DB\Adapter\AdapterInterface + */ + private function getConnection() + { + if (!$this->connection) { + $this->connection = $this->resourceConnection->getConnection(); + } + return $this->connection; + } + + /** + * Set columns list to SelectBuilder + * + * @param SelectBuilder $selectBuilder + * @param array $entityConfig + * @return array + */ + public function getColumns(SelectBuilder $selectBuilder, $entityConfig) + { + if (!isset($entityConfig['attribute'])) { + return []; + } + $group = []; + $columns = $selectBuilder->getColumns(); + foreach ($entityConfig['attribute'] as $attributeData) { + $columnAlias = $this->nameResolver->getAlias($attributeData); + $tableAlias = $this->nameResolver->getAlias($entityConfig); + $columnName = $this->nameResolver->getName($attributeData); + if (isset($attributeData['function'])) { + $prefix = ''; + if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) { + $prefix = ' DISTINCT '; + } + $expression = new ColumnValueExpression( + strtoupper($attributeData['function']) . '(' . $prefix + . $this->getConnection()->quoteIdentifier($tableAlias . '.' . $columnName) + . ')' + ); + } else { + $expression = $tableAlias . '.' . $columnName; + } + $columns[$columnAlias] = $expression; + if (isset($attributeData['group'])) { + $group[$columnAlias] = $expression; + } + } + $selectBuilder->setGroup(array_merge($selectBuilder->getGroup(), $group)); + return $columns; + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php new file mode 100644 index 0000000000000..8ead9ba8ae326 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php @@ -0,0 +1,164 @@ + '%1$s = %2$s', + 'neq' => '%1$s != %2$s', + 'like' => '%1$s LIKE %2$s', + 'nlike' => '%1$s NOT LIKE %2$s', + 'in' => '%1$s IN(%2$s)', + 'nin' => '%1$s NOT IN(%2$s)', + 'notnull' => '%1$s IS NOT NULL', + 'null' => '%1$s IS NULL', + 'gt' => '%1$s > %2$s', + 'lt' => '%1$s < %2$s', + 'gteq' => '%1$s >= %2$s', + 'lteq' => '%1$s <= %2$s', + 'finset' => 'FIND_IN_SET(%2$s, %1$s)' + ]; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + private $connection; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * ConditionResolver constructor. + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Returns connection + * + * @return \Magento\Framework\DB\Adapter\AdapterInterface + */ + private function getConnection() + { + if (!$this->connection) { + $this->connection = $this->resourceConnection->getConnection(); + } + return $this->connection; + } + + /** + * Returns value for condition + * + * @param string $condition + * @param string $referencedEntity + * @return mixed|null|string|\Zend_Db_Expr + */ + private function getValue($condition, $referencedEntity) + { + $value = null; + $argument = isset($condition['_value']) ? $condition['_value'] : null; + if (!isset($condition['type'])) { + $condition['type'] = 'value'; + } + + switch ($condition['type']) { + case "value": + $value = $this->getConnection()->quote($argument); + break; + case "variable": + $value = new Expression($argument); + break; + case "identifier": + $value = $this->getConnection()->quoteIdentifier( + $referencedEntity ? $referencedEntity . '.' . $argument : $argument + ); + break; + } + return $value; + } + + /** + * Returns condition for WHERE + * + * @param SelectBuilder $selectBuilder + * @param string $tableName + * @param array $condition + * @param null|string $referencedEntity + * @return string + */ + private function getCondition(SelectBuilder $selectBuilder, $tableName, $condition, $referencedEntity = null) + { + $columns = $selectBuilder->getColumns(); + if (isset($columns[$condition['attribute']]) + && $columns[$condition['attribute']] instanceof Expression + ) { + $expression = $columns[$condition['attribute']]; + } else { + $expression = $this->getConnection()->quoteIdentifier($tableName . '.' . $condition['attribute']); + } + return sprintf( + $this->conditionMap[$condition['operator']], + $expression, + $this->getValue($condition, $referencedEntity) + ); + } + + /** + * Build WHERE condition + * + * @param SelectBuilder $selectBuilder + * @param array $filterConfig + * @param string $aliasName + * @param null|string $referencedAlias + * @return array + */ + public function getFilter(SelectBuilder $selectBuilder, $filterConfig, $aliasName, $referencedAlias = null) + { + $filtersParts = []; + foreach ($filterConfig as $filter) { + $glue = $filter['glue']; + $parts = []; + foreach ($filter['condition'] as $condition) { + if (isset($condition['type']) && $condition['type'] == 'variable') { + $selectBuilder->setParams(array_merge($selectBuilder->getParams(), [$condition['_value']])); + } + $parts[] = $this->getCondition( + $selectBuilder, + $aliasName, + $condition, + $referencedAlias + ); + } + if (isset($filter['filter'])) { + $parts[] = '(' . $this->getFilter( + $selectBuilder, + $filter['filter'], + $aliasName, + $referencedAlias + ) . ')'; + } + $filtersParts[] = '(' . implode(' ' . strtoupper($glue) . ' ', $parts) . ')'; + } + return implode(' OR ', $filtersParts); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php new file mode 100644 index 0000000000000..dc09391b69b31 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php @@ -0,0 +1,38 @@ +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..4c8b9fa3c2e83 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php @@ -0,0 +1,62 @@ +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..81ee9b15781d1 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php @@ -0,0 +1,287 @@ +resourceConnection = $resourceConnection; + } + + /** + * Get join condition + * + * @return array + */ + public function getJoins() + { + return $this->joins; + } + + /** + * Set joins conditions + * + * @param array $joins + * @return void + */ + public function setJoins($joins) + { + $this->joins = $joins; + } + + /** + * Get connection name + * + * @return string + */ + public function getConnectionName() + { + return $this->connectionName; + } + + /** + * Set connection name + * + * @param string $connectionName + * @return void + */ + public function setConnectionName($connectionName) + { + $this->connectionName = $connectionName; + } + + /** + * Get columns + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Set columns + * + * @param array $columns + * @return void + */ + public function setColumns($columns) + { + $this->columns = $columns; + } + + /** + * Get filters + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Set filters + * + * @param array $filters + * @return void + */ + public function setFilters($filters) + { + $this->filters = $filters; + } + + /** + * Get from condition + * + * @return array + */ + public function getFrom() + { + return $this->from; + } + + /** + * Set from condition + * + * @param array $from + * @return void + */ + public function setFrom($from) + { + $this->from = $from; + } + + /** + * Process JOIN conditions + * + * @param Select $select + * @param array $joinConfig + * @return Select + */ + private function processJoin(Select $select, $joinConfig) + { + switch ($joinConfig['link-type']) { + case 'left': + $select->joinLeft($joinConfig['table'], $joinConfig['condition'], []); + break; + case 'inner': + $select->joinInner($joinConfig['table'], $joinConfig['condition'], []); + break; + case 'right': + $select->joinRight($joinConfig['table'], $joinConfig['condition'], []); + break; + } + return $select; + } + + /** + * Creates Select object + * + * @return Select + */ + public function create() + { + $connection = $this->resourceConnection->getConnection($this->getConnectionName()); + $select = $connection->select(); + $select->from($this->getFrom(), []); + $select->columns($this->getColumns()); + foreach ($this->getFilters() as $filter) { + $select->where($filter); + } + foreach ($this->getJoins() as $joinConfig) { + $select = $this->processJoin($select, $joinConfig); + } + if (!empty($this->getGroup())) { + $select->group(implode(', ', $this->getGroup())); + } + return $select; + } + + /** + * Returns group + * + * @return array + */ + public function getGroup() + { + return $this->group; + } + + /** + * Set group + * + * @param array $group + * @return void + */ + public function setGroup($group) + { + $this->group = $group; + } + + /** + * Get parameters + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Set parameters + * + * @param array $params + * @return void + */ + public function setParams($params) + { + $this->params = $params; + } + + /** + * Get having condition + * + * @return array + */ + public function getHaving() + { + return $this->having; + } + + /** + * Set having condition + * + * @param array $having + * @return void + */ + public function setHaving($having) + { + $this->having = $having; + } +} diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php new file mode 100644 index 0000000000000..1d88d4618efc5 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php @@ -0,0 +1,43 @@ +objectManager = $objectManager; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return SelectBuilder + */ + public function create(array $data = []) + { + return $this->objectManager->create(SelectBuilder::class, $data); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/IteratorFactory.php b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php new file mode 100644 index 0000000000000..0556cf4569dc4 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php @@ -0,0 +1,59 @@ +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..edf5ed08ee55f --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/Query.php @@ -0,0 +1,94 @@ +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..5da7adf794215 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/QueryFactory.php @@ -0,0 +1,140 @@ +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..60e722930c244 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php @@ -0,0 +1,74 @@ +queryFactory = $queryFactory; + $this->connectionFactory = $connectionFactory; + $this->iteratorFactory = $iteratorFactory; + } + + /** + * Returns custom iterator name for report + * Null for default + * + * @param Query $query + * @return string|null + */ + private function getIteratorName(Query $query) + { + $config = $query->getConfig(); + return isset($config['iterator']) ? $config['iterator'] : null; + } + + /** + * Returns report data by name and criteria + * + * @param string $name + * @return \IteratorIterator + */ + public function getReport($name) + { + $query = $this->queryFactory->create($name); + $connection = $this->connectionFactory->getConnection($query->getConnectionName()); + $statement = $connection->query($query->getSelect()); + return $this->iteratorFactory->create($statement, $this->getIteratorName($query)); + } +} diff --git a/app/code/Magento/Analytics/ReportXml/SelectHydrator.php b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php new file mode 100644 index 0000000000000..6dca7e0481e76 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php @@ -0,0 +1,143 @@ +resourceConnection = $resourceConnection; + $this->objectManager = $objectManager; + $this->selectParts = $selectParts; + } + + /** + * @return array + */ + private function getSelectParts() + { + return array_merge($this->predefinedSelectParts, $this->selectParts); + } + + /** + * Extracts Select metadata parts + * + * @param Select $select + * @return array + * @throws \Zend_Db_Select_Exception + */ + public function extract(Select $select) + { + $parts = []; + foreach ($this->getSelectParts() as $partName) { + $parts[$partName] = $select->getPart($partName); + } + return $parts; + } + + /** + * @param array $selectParts + * @return Select + */ + public function recreate(array $selectParts) + { + $select = $this->resourceConnection->getConnection()->select(); + + $select = $this->processColumns($select, $selectParts); + + foreach ($selectParts as $partName => $partValue) { + $select->setPart($partName, $partValue); + } + + return $select; + } + + /** + * Process COLUMNS part values and add this part into select. + * + * If each column contains information about select expression + * an object with the type of this expression going to be created and assigned to this column. + * + * @param Select $select + * @param array $selectParts + * @return Select + */ + private function processColumns(Select $select, array &$selectParts) + { + if (!empty($selectParts[Select::COLUMNS]) && is_array($selectParts[Select::COLUMNS])) { + $part = []; + + foreach ($selectParts[Select::COLUMNS] as $columnEntry) { + list($correlationName, $column, $alias) = $columnEntry; + if (is_array($column) && !empty($column['class'])) { + $expression = $this->objectManager->create( + $column['class'], + isset($column['arguments']) ? $column['arguments'] : [] + ); + $part[] = [$correlationName, $expression, $alias]; + } else { + $part[] = $columnEntry; + } + } + + $select->setPart(Select::COLUMNS, $part); + unset($selectParts[Select::COLUMNS]); + } + + return $select; + } +} diff --git a/app/code/Magento/Analytics/Setup/Patch/Data/PrepareInitialConfig.php b/app/code/Magento/Analytics/Setup/Patch/Data/PrepareInitialConfig.php new file mode 100644 index 0000000000000..a352854a8b77b --- /dev/null +++ b/app/code/Magento/Analytics/Setup/Patch/Data/PrepareInitialConfig.php @@ -0,0 +1,92 @@ +moduleDataSetup = $moduleDataSetup; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->insertMultiple( + $this->moduleDataSetup->getTable('core_config_data'), + [ + [ + 'scope' => 'default', + 'scope_id' => 0, + 'path' => 'analytics/subscription/enabled', + 'value' => 1 + ], + [ + 'scope' => 'default', + 'scope_id' => 0, + 'path' => SubscriptionHandler::CRON_STRING_PATH, + 'value' => join(' ', SubscriptionHandler::CRON_EXPR_ARRAY) + ] + ] + ); + + $this->moduleDataSetup->getConnection()->insert( + $this->moduleDataSetup->getTable('flag'), + [ + 'flag_code' => SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, + 'state' => 0, + 'flag_data' => 24, + ] + ); + } + + /** + * {@inheritdoc} + */ + public static function getDependencies() + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getVersion() + { + return '2.0.0'; + } + + /** + * {@inheritdoc} + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php new file mode 100644 index 0000000000000..cbf06264096ac --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php @@ -0,0 +1,77 @@ +abstractElementMock = $this->getMockBuilder(AbstractElement::class) + ->setMethods(['getComment', 'getLabel']) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->formMock = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManager($this); + $this->additionalComment = $objectManager->getObject( + AdditionalComment::class, + [ + 'context' => $this->contextMock + ] + ); + } + + public function testRender() + { + $this->abstractElementMock->setForm($this->formMock); + $this->abstractElementMock->expects($this->any()) + ->method('getComment') + ->willReturn('New comment'); + $this->abstractElementMock->expects($this->any()) + ->method('getLabel') + ->willReturn('Comment label'); + $html = $this->additionalComment->render($this->abstractElementMock); + $this->assertRegexp( + "/New comment/", + $html + ); + $this->assertRegexp( + "/Comment label/", + $html + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php new file mode 100644 index 0000000000000..462b3c909a7fd --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -0,0 +1,95 @@ +abstractElementMock = $this->getMockBuilder(AbstractElement::class) + ->setMethods(['getComment']) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->setMethods(['getLocaleDate']) + ->disableOriginalConstructor() + ->getMock(); + $this->formMock = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->getMock(); + $this->timeZoneMock = $this->getMockBuilder(TimezoneInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->any()) + ->method('getLocaleDate') + ->willReturn($this->timeZoneMock); + $this->localeResolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getLocale']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManager($this); + $this->collectionTimeLabel = $objectManager->getObject( + CollectionTimeLabel::class, + [ + 'context' => $this->contextMock, + 'localeResolver' => $this->localeResolver + ] + ); + } + + public function testRender() + { + $timeZone = "America/New_York"; + $this->abstractElementMock->setForm($this->formMock); + $this->timeZoneMock->expects($this->once()) + ->method('getConfigTimezone') + ->willReturn($timeZone); + $this->abstractElementMock->expects($this->any()) + ->method('getComment') + ->willReturn('Eastern Standard Time (America/New_York)'); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn('en_US'); + $this->assertRegexp( + "/Eastern Standard Time \(America\/New_York\)/", + $this->collectionTimeLabel->render($this->abstractElementMock) + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php new file mode 100644 index 0000000000000..d643bc05cc615 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -0,0 +1,82 @@ +subscriptionStatusProviderMock = $this->getMockBuilder(SubscriptionStatusProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) + ->setMethods(['getComment']) + ->disableOriginalConstructor() + ->getMock(); + $this->formMock = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManager($this); + $this->subscriptionStatusLabel = $objectManager->getObject( + SubscriptionStatusLabel::class, + [ + 'context' => $this->contextMock, + 'subscriptionStatusProvider' => $this->subscriptionStatusProviderMock + ] + ); + } + + public function testRender() + { + $this->abstractElementMock->setForm($this->formMock); + $this->subscriptionStatusProviderMock->expects($this->once()) + ->method('getStatus') + ->willReturn('Enabled'); + $this->abstractElementMock->expects($this->any()) + ->method('getComment') + ->willReturn('Subscription status: Enabled'); + $this->assertRegexp( + "/Subscription status: Enabled/", + $this->subscriptionStatusLabel->render($this->abstractElementMock) + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php new file mode 100644 index 0000000000000..abce48c36c86a --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -0,0 +1,77 @@ +abstractElementMock = $this->getMockBuilder(AbstractElement::class) + ->setMethods(['getComment', 'getLabel', 'getHint']) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->formMock = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManager($this); + $this->vertical = $objectManager->getObject( + Vertical::class, + [ + 'context' => $this->contextMock + ] + ); + } + + public function testRender() + { + $this->abstractElementMock->setForm($this->formMock); + $this->abstractElementMock->expects($this->any()) + ->method('getComment') + ->willReturn('New comment'); + $this->abstractElementMock->expects($this->any()) + ->method('getHint') + ->willReturn('New hint'); + $html = $this->vertical->render($this->abstractElementMock); + $this->assertRegexp( + "/New comment/", + $html + ); + $this->assertRegExp( + "/New hint/", + $html + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php new file mode 100644 index 0000000000000..4e79ca43327bf --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php @@ -0,0 +1,81 @@ +configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resultRedirectFactoryMock = $this->getMockBuilder(RedirectFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->redirectMock = $this->getMockBuilder(Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->signUpController = $this->objectManagerHelper->getObject( + SignUp::class, + [ + 'config' => $this->configMock, + 'resultRedirectFactory' => $this->resultRedirectFactoryMock + ] + ); + } + + /** + * @return void + */ + public function testExecute() + { + $urlBIEssentialsConfigPath = 'analytics/url/bi_essentials'; + $this->configMock->expects($this->once()) + ->method('getValue') + ->with($urlBIEssentialsConfigPath) + ->willReturn('value'); + $this->resultRedirectFactoryMock->expects($this->once())->method('create')->willReturn($this->redirectMock); + $this->redirectMock->expects($this->once())->method('setUrl')->with('value')->willReturnSelf(); + $this->assertEquals($this->redirectMock, $this->signUpController->execute()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php new file mode 100644 index 0000000000000..4f54ce5059965 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php @@ -0,0 +1,185 @@ +reportUrlProviderMock = $this->getMockBuilder(ReportUrlProvider::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->redirectMock = $this->getMockBuilder(Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->showController = $this->objectManagerHelper->getObject( + Show::class, + [ + 'reportUrlProvider' => $this->reportUrlProviderMock, + 'resultFactory' => $this->resultFactoryMock, + 'messageManager' => $this->messageManagerMock, + ] + ); + } + + /** + * @return void + */ + public function testExecute() + { + $otpUrl = 'http://example.com?otp=15vbjcfdvd15645'; + + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->redirectMock); + $this->reportUrlProviderMock + ->expects($this->once()) + ->method('getUrl') + ->with() + ->willReturn($otpUrl); + $this->redirectMock + ->expects($this->once()) + ->method('setUrl') + ->with($otpUrl) + ->willReturnSelf(); + $this->assertSame($this->redirectMock, $this->showController->execute()); + } + + /** + * @dataProvider executeWithExceptionDataProvider + * + * @param \Exception $exception + */ + public function testExecuteWithException(\Exception $exception) + { + + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->redirectMock); + $this->reportUrlProviderMock + ->expects($this->once()) + ->method('getUrl') + ->with() + ->willThrowException($exception); + if ($exception instanceof LocalizedException) { + $message = $exception->getMessage(); + } else { + $message = __('Sorry, there has been an error processing your request. Please try again later.'); + } + $this->messageManagerMock + ->expects($this->once()) + ->method('addExceptionMessage') + ->with($exception, $message) + ->willReturnSelf(); + $this->redirectMock + ->expects($this->once()) + ->method('setPath') + ->with('adminhtml') + ->willReturnSelf(); + $this->assertSame($this->redirectMock, $this->showController->execute()); + } + + /** + * @return array + */ + public function executeWithExceptionDataProvider() + { + return [ + 'ExecuteWithLocalizedException' => [new LocalizedException(__('TestMessage'))], + 'ExecuteWithException' => [new \Exception('TestMessage')], + ]; + } + + /** + * @return void + */ + public function testExecuteWithSubscriptionUpdateException() + { + $exception = new SubscriptionUpdateException(__('TestMessage')); + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->redirectMock); + $this->reportUrlProviderMock + ->expects($this->once()) + ->method('getUrl') + ->with() + ->willThrowException($exception); + $this->messageManagerMock + ->expects($this->once()) + ->method('addNoticeMessage') + ->with($exception->getMessage()) + ->willReturnSelf(); + $this->redirectMock + ->expects($this->once()) + ->method('setPath') + ->with('adminhtml') + ->willReturnSelf(); + $this->assertSame($this->redirectMock, $this->showController->execute()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php new file mode 100644 index 0000000000000..89107b8999ecd --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php @@ -0,0 +1,156 @@ +resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resultRedirectMock = $this->getMockBuilder(Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->retryController = $this->objectManagerHelper->getObject( + Retry::class, + [ + 'resultFactory' => $this->resultFactoryMock, + 'subscriptionHandler' => $this->subscriptionHandlerMock, + 'messageManager' => $this->messageManagerMock, + ] + ); + } + + /** + * @return void + */ + public function testExecute() + { + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); + $this->resultRedirectMock + ->expects($this->once()) + ->method('setPath') + ->with('adminhtml') + ->willReturnSelf(); + $this->subscriptionHandlerMock + ->expects($this->once()) + ->method('processEnabled') + ->with() + ->willReturn(true); + $this->assertSame( + $this->resultRedirectMock, + $this->retryController->execute() + ); + } + + /** + * @dataProvider executeExceptionsDataProvider + * + * @param \Exception $exception + * @param Phrase $message + */ + public function testExecuteWithException(\Exception $exception, Phrase $message) + { + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); + $this->resultRedirectMock + ->expects($this->once()) + ->method('setPath') + ->with('adminhtml') + ->willReturnSelf(); + $this->subscriptionHandlerMock + ->expects($this->once()) + ->method('processEnabled') + ->with() + ->willThrowException($exception); + $this->messageManagerMock + ->expects($this->once()) + ->method('addExceptionMessage') + ->with($exception, $message); + + $this->assertSame( + $this->resultRedirectMock, + $this->retryController->execute() + ); + } + + /** + * @return array + */ + public function executeExceptionsDataProvider() + { + return [ + [new LocalizedException(__('TestMessage')), __('TestMessage')], + [ + new \Exception('TestMessage'), + __('Sorry, there has been an error processing your request. Please try again later.') + ], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php new file mode 100644 index 0000000000000..66d1715de0321 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php @@ -0,0 +1,88 @@ +exportDataHandlerMock = $this->getMockBuilder(ExportDataHandlerInterface::class) + ->getMockForAbstractClass(); + + $this->subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->collectData = $this->objectManagerHelper->getObject( + CollectData::class, + [ + 'exportDataHandler' => $this->exportDataHandlerMock, + 'subscriptionStatus' => $this->subscriptionStatusMock, + ] + ); + } + + /** + * @param string $status + * @return void + * @dataProvider executeDataProvider + */ + public function testExecute($status) + { + $this->subscriptionStatusMock + ->expects($this->once()) + ->method('getStatus') + ->with() + ->willReturn($status); + $this->exportDataHandlerMock + ->expects(($status === SubscriptionStatusProvider::ENABLED) ? $this->once() : $this->never()) + ->method('prepareExportData') + ->with(); + + $this->assertTrue($this->collectData->execute()); + } + + /** + * @return array + */ + public function executeDataProvider() + { + return [ + 'Subscription is enabled' => [SubscriptionStatusProvider::ENABLED], + 'Subscription is disabled' => [SubscriptionStatusProvider::DISABLED], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php new file mode 100644 index 0000000000000..959a11f9e1058 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php @@ -0,0 +1,133 @@ +connectorMock = $this->getMockBuilder(Connector::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configWriterMock = $this->getMockBuilder(WriterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->signUp = new SignUp( + $this->connectorMock, + $this->configWriterMock, + $this->flagManagerMock, + $this->reinitableConfigMock + ); + } + + public function testExecute() + { + $attemptsCount = 10; + + $this->flagManagerMock->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) + ->willReturn($attemptsCount); + + $attemptsCount -= 1; + $this->flagManagerMock->expects($this->once()) + ->method('saveFlag') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount); + $this->connectorMock->expects($this->once()) + ->method('execute') + ->with('signUp') + ->willReturn(true); + $this->addDeleteAnalyticsCronExprAsserts(); + $this->flagManagerMock->expects($this->once()) + ->method('deleteFlag') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE); + $this->assertTrue($this->signUp->execute()); + } + + public function testExecuteFlagNotExist() + { + $this->flagManagerMock->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) + ->willReturn(null); + $this->addDeleteAnalyticsCronExprAsserts(); + $this->assertFalse($this->signUp->execute()); + } + + public function testExecuteZeroAttempts() + { + $attemptsCount = 0; + $this->flagManagerMock->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) + ->willReturn($attemptsCount); + $this->addDeleteAnalyticsCronExprAsserts(); + $this->flagManagerMock->expects($this->once()) + ->method('deleteFlag') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE); + $this->assertFalse($this->signUp->execute()); + } + + /** + * Add assertions for method deleteAnalyticsCronExpr. + * + * @return void + */ + private function addDeleteAnalyticsCronExprAsserts() + { + $this->configWriterMock + ->expects($this->once()) + ->method('delete') + ->with(SubscriptionHandler::CRON_STRING_PATH) + ->willReturn(true); + $this->reinitableConfigMock + ->expects($this->once()) + ->method('reinit') + ->willReturnSelf(); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php new file mode 100644 index 0000000000000..aa3011ffc94f6 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php @@ -0,0 +1,211 @@ +connectorMock = $this->getMockBuilder(Connector::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configWriterMock = $this->getMockBuilder(WriterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->update = new Update( + $this->connectorMock, + $this->configWriterMock, + $this->reinitableConfigMock, + $this->flagManagerMock, + $this->analyticsTokenMock + ); + } + + /** + * @return void + */ + public function testExecuteWithoutToken() + { + $this->flagManagerMock + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE) + ->willReturn(10); + $this->connectorMock + ->expects($this->once()) + ->method('execute') + ->with('update') + ->willReturn(false); + $this->analyticsTokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + $this->addFinalOutputAsserts(); + $this->assertFalse($this->update->execute()); + } + + /** + * @param bool $isExecuted + */ + private function addFinalOutputAsserts(bool $isExecuted = true) + { + $this->flagManagerMock + ->expects($this->exactly(2 * $isExecuted)) + ->method('deleteFlag') + ->withConsecutive( + [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE], + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE] + ); + $this->configWriterMock + ->expects($this->exactly((int)$isExecuted)) + ->method('delete') + ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH); + $this->reinitableConfigMock + ->expects($this->exactly((int)$isExecuted)) + ->method('reinit') + ->with(); + } + + /** + * @param $counterData + * @return void + * @dataProvider executeWithEmptyReverseCounterDataProvider + */ + public function testExecuteWithEmptyReverseCounter($counterData) + { + $this->flagManagerMock + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE) + ->willReturn($counterData); + $this->connectorMock + ->expects($this->never()) + ->method('execute') + ->with('update') + ->willReturn(false); + $this->analyticsTokenMock + ->method('isTokenExist') + ->willReturn(true); + $this->addFinalOutputAsserts(); + $this->assertFalse($this->update->execute()); + } + + /** + * Provides empty states of the reverse counter. + * + * @return array + */ + public function executeWithEmptyReverseCounterDataProvider() + { + return [ + [null], + [0] + ]; + } + + /** + * @param int $reverseCount + * @param bool $commandResult + * @param bool $finalConditionsIsExpected + * @param bool $functionResult + * @return void + * @dataProvider executeRegularScenarioDataProvider + */ + public function testExecuteRegularScenario( + int $reverseCount, + bool $commandResult, + bool $finalConditionsIsExpected, + bool $functionResult + ) { + $this->flagManagerMock + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE) + ->willReturn($reverseCount); + $this->connectorMock + ->expects($this->once()) + ->method('execute') + ->with('update') + ->willReturn($commandResult); + $this->analyticsTokenMock + ->method('isTokenExist') + ->willReturn(true); + $this->addFinalOutputAsserts($finalConditionsIsExpected); + $this->assertSame($functionResult, $this->update->execute()); + } + + /** + * @return array + */ + public function executeRegularScenarioDataProvider() + { + return [ + 'The last attempt with command execution result False' => [ + 'Reverse count' => 1, + 'Command result' => false, + 'Executed final output conditions' => true, + 'Function result' => false, + ], + 'Not the last attempt with command execution result False' => [ + 'Reverse count' => 10, + 'Command result' => false, + 'Executed final output conditions' => false, + 'Function result' => false, + ], + 'Command execution result True' => [ + 'Reverse count' => 10, + 'Command result' => true, + 'Executed final output conditions' => true, + 'Function result' => true, + ], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php new file mode 100644 index 0000000000000..f4d17b3069229 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php @@ -0,0 +1,126 @@ +reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configWriterMock = $this->getMockBuilder(WriterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->tokenModel = $this->objectManagerHelper->getObject( + AnalyticsToken::class, + [ + 'reinitableConfig' => $this->reinitableConfigMock, + 'config' => $this->configMock, + 'configWriter' => $this->configWriterMock, + 'tokenPath' => $this->tokenPath, + ] + ); + } + + /** + * @return void + */ + public function testStoreToken() + { + $value = 'jjjj0000'; + + $this->configWriterMock + ->expects($this->once()) + ->method('save') + ->with($this->tokenPath, $value); + + $this->reinitableConfigMock + ->expects($this->once()) + ->method('reinit') + ->willReturnSelf(); + + $this->assertTrue($this->tokenModel->storeToken($value)); + } + + /** + * @return void + */ + public function testGetToken() + { + $value = 'jjjj0000'; + + $this->configMock + ->expects($this->once()) + ->method('getValue') + ->with($this->tokenPath) + ->willReturn($value); + + $this->assertSame($value, $this->tokenModel->getToken()); + } + + /** + * @return void + */ + public function testIsTokenExist() + { + $this->assertFalse($this->tokenModel->isTokenExist()); + + $this->configMock + ->expects($this->once()) + ->method('getValue') + ->with($this->tokenPath) + ->willReturn('0000'); + $this->assertTrue($this->tokenModel->isTokenExist()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php new file mode 100644 index 0000000000000..f5f721c038c57 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php @@ -0,0 +1,178 @@ +reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configWriterMock = $this->getMockBuilder(WriterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->subscriptionUpdateHandler = $this->objectManagerHelper->getObject( + SubscriptionUpdateHandler::class, + [ + 'reinitableConfig' => $this->reinitableConfigMock, + 'analyticsToken' => $this->analyticsTokenMock, + 'flagManager' => $this->flagManagerMock, + 'configWriter' => $this->configWriterMock, + ] + ); + } + + /** + * @return void + */ + public function testTokenDoesNotExist() + { + $this->analyticsTokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->with() + ->willReturn(false); + $this->flagManagerMock + ->expects($this->never()) + ->method('saveFlag'); + $this->configWriterMock + ->expects($this->never()) + ->method('save'); + $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate('http://store.com')); + } + + /** + * @return void + */ + public function testTokenAndPreviousBaseUrlExist() + { + $url = 'https://store.com'; + $this->analyticsTokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->with() + ->willReturn(true); + $this->flagManagerMock + ->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) + ->willReturn(true); + $this->flagManagerMock + ->expects($this->once()) + ->method('saveFlag') + ->withConsecutive( + [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue], + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url] + ); + $this->configWriterMock + ->expects($this->once()) + ->method('save') + ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression); + $this->reinitableConfigMock + ->expects($this->once()) + ->method('reinit') + ->with(); + $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url)); + } + + /** + * @return void + */ + public function testTokenExistAndWithoutPreviousBaseUrl() + { + $url = 'https://store.com'; + $this->analyticsTokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->with() + ->willReturn(true); + $this->flagManagerMock + ->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) + ->willReturn(false); + $this->flagManagerMock + ->expects($this->exactly(2)) + ->method('saveFlag') + ->withConsecutive( + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url], + [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue] + ); + $this->configWriterMock + ->expects($this->once()) + ->method('save') + ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression); + $this->reinitableConfigMock + ->expects($this->once()) + ->method('reinit') + ->with(); + $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php new file mode 100644 index 0000000000000..25f4008f9a6e4 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php @@ -0,0 +1,108 @@ +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..cf3e37ad89a31 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php @@ -0,0 +1,149 @@ +flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configWriterMock = $this->getMockBuilder(WriterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->tokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->subscriptionHandler = $this->objectManagerHelper->getObject( + SubscriptionHandler::class, + [ + 'flagManager' => $this->flagManagerMock, + 'configWriter' => $this->configWriterMock, + 'attemptsInitValue' => $this->attemptsInitValue, + 'analyticsToken' => $this->tokenMock, + ] + ); + } + + public function testProcessEnabledTokenExist() + { + $this->tokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + $this->configWriterMock + ->expects($this->never()) + ->method('save'); + $this->flagManagerMock + ->expects($this->never()) + ->method('saveFlag'); + $this->assertTrue( + $this->subscriptionHandler->processEnabled() + ); + } + + public function testProcessEnabledTokenDoesNotExist() + { + $this->tokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + $this->configWriterMock + ->expects($this->once()) + ->method('save') + ->with(SubscriptionHandler::CRON_STRING_PATH, "0 * * * *"); + $this->flagManagerMock + ->expects($this->once()) + ->method('saveFlag') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue) + ->willReturn(true); + $this->assertTrue( + $this->subscriptionHandler->processEnabled() + ); + } + + public function testProcessDisabledTokenDoesNotExist() + { + $this->configWriterMock + ->expects($this->once()) + ->method('delete') + ->with(CollectionTime::CRON_SCHEDULE_PATH); + $this->tokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + $this->flagManagerMock + ->expects($this->once()) + ->method('deleteFlag') + ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) + ->willReturn(true); + $this->assertTrue( + $this->subscriptionHandler->processDisabled() + ); + } + + public function testProcessDisabledTokenExists() + { + $this->configWriterMock + ->expects($this->once()) + ->method('delete') + ->with(CollectionTime::CRON_SCHEDULE_PATH); + $this->tokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + $this->flagManagerMock + ->expects($this->never()) + ->method('deleteFlag'); + $this->assertTrue( + $this->subscriptionHandler->processDisabled() + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php new file mode 100644 index 0000000000000..587f3599282f0 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php @@ -0,0 +1,181 @@ +subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->enabledModel = $this->objectManagerHelper->getObject( + Enabled::class, + [ + 'subscriptionHandler' => $this->subscriptionHandlerMock, + '_logger' => $this->loggerMock, + 'config' => $this->configMock, + ] + ); + } + + /** + * @return void + */ + public function testAfterSaveSuccessEnabled() + { + $this->enabledModel->setData('value', $this->valueEnabled); + + $this->configMock + ->expects($this->any()) + ->method('getValue') + ->willReturn(!$this->valueEnabled); + + $this->subscriptionHandlerMock + ->expects($this->once()) + ->method('processEnabled') + ->with() + ->willReturn(true); + + $this->assertInstanceOf( + Value::class, + $this->enabledModel->afterSave() + ); + } + + /** + * @return void + */ + public function testAfterSaveSuccessDisabled() + { + $this->enabledModel->setData('value', $this->valueDisabled); + + $this->configMock + ->expects($this->any()) + ->method('getValue') + ->willReturn(!$this->valueDisabled); + + $this->subscriptionHandlerMock + ->expects($this->once()) + ->method('processDisabled') + ->with() + ->willReturn(true); + + $this->assertInstanceOf( + Value::class, + $this->enabledModel->afterSave() + ); + } + + /** + * @return void + */ + public function testAfterSaveSuccessValueNotChanged() + { + $this->enabledModel->setData('value', null); + + $this->configMock + ->expects($this->any()) + ->method('getValue') + ->willReturn(null); + + $this->subscriptionHandlerMock + ->expects($this->never()) + ->method('processEnabled') + ->with() + ->willReturn(true); + $this->subscriptionHandlerMock + ->expects($this->never()) + ->method('processDisabled') + ->with() + ->willReturn(true); + + $this->assertInstanceOf( + Value::class, + $this->enabledModel->afterSave() + ); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testExecuteAfterSaveFailedWithLocalizedException() + { + $exception = new \Exception('Message'); + $this->enabledModel->setData('value', $this->valueEnabled); + + $this->subscriptionHandlerMock + ->expects($this->once()) + ->method('processEnabled') + ->with() + ->willThrowException($exception); + + $this->loggerMock + ->expects($this->once()) + ->method('error') + ->with($exception->getMessage()); + + $this->enabledModel->afterSave(); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php new file mode 100644 index 0000000000000..6fe7d0aa93998 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php @@ -0,0 +1,59 @@ +objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\Model\Config\Backend\Vertical::class + ); + } + + /** + * @return void + */ + public function testBeforeSaveSuccess() + { + $this->subject->setValue('Apps and Games'); + + $this->assertInstanceOf( + \Magento\Analytics\Model\Config\Backend\Vertical::class, + $this->subject->beforeSave() + ); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testBeforeSaveFailedWithLocalizedException() + { + $this->subject->setValue(''); + + $this->subject->beforeSave(); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php new file mode 100644 index 0000000000000..0b7f4870dbac8 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php @@ -0,0 +1,142 @@ +objectManagerHelper = new ObjectManagerHelper($this); + + $this->mapper = $this->objectManagerHelper->getObject(Mapper::class); + } + + /** + * @param array $configData + * @param array $resultData + * @return void + * + * @dataProvider executingDataProvider + */ + public function testExecution($configData, $resultData) + { + $this->assertSame($resultData, $this->mapper->execute($configData)); + } + + /** + * @return array + */ + public function executingDataProvider() + { + return [ + 'wrongConfig' => [ + ['config' => ['files']], + [] + ], + 'validConfigWithFileNodes' => [ + [ + 'config' => [ + 0 => [ + 'file' => [ + 0 => [ + 'name' => 'fileName', + 'providers' => [[]] + ] + ] + ] + ] + ], + [ + 'fileName' => [ + 'name' => 'fileName', + 'providers' => [] + ] + ], + ], + 'validConfigWithProvidersNode' => [ + [ + 'config' => [ + 0 => [ + 'file' => [ + 0 => [ + 'name' => 'fileName', + 'providers' => [ + 0 => [ + 'reportProvider' => [0 => []] + ] + ] + ] + ] + ] + ] + ], + [ + 'fileName' => [ + 'name' => 'fileName', + 'providers' => [ + 'reportProvider' => ['parameters' => []] + ] + ] + ], + ], + 'validConfigWithParametersNode' => [ + [ + 'config' => [ + 0 => [ + 'file' => [ + 0 => [ + 'name' => 'fileName', + 'providers' => [ + 0 => [ + 'reportProvider' => [ + 0 => [ + 'parameters' => [ + 0 => ['name' => ['reportName']] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ], + [ + 'fileName' => [ + 'name' => 'fileName', + 'providers' => [ + 'reportProvider' => [ + 'parameters' => [ + 'name' => 'reportName' + ] + ] + ] + ] + ], + ], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php new file mode 100644 index 0000000000000..6aa9c7ef3106c --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php @@ -0,0 +1,108 @@ +mapperMock = $this->getMockBuilder(Mapper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->readerXmlMock = $this->getMockBuilder(ReaderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->readerDbMock = $this->getMockBuilder(ReaderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->reader = $this->objectManagerHelper->getObject( + Reader::class, + [ + 'mapper' => $this->mapperMock, + 'readers' => [ + $this->readerXmlMock, + $this->readerDbMock, + ], + ] + ); + } + + /** + * @return void + */ + public function testRead() + { + $scope = 'store'; + $xmlReaderResult = [ + 'config' => ['node1' => ['node2' => 'node4']] + ]; + $dbReaderResult = [ + 'config' => ['node1' => ['node2' => 'node3']] + ]; + $mapperResult = ['node2' => ['node3', 'node4']]; + + $this->readerXmlMock + ->expects($this->once()) + ->method('read') + ->with($scope) + ->willReturn($xmlReaderResult); + + $this->readerDbMock + ->expects($this->once()) + ->method('read') + ->with($scope) + ->willReturn($dbReaderResult); + + $this->mapperMock + ->expects($this->once()) + ->method('execute') + ->with(array_merge_recursive($xmlReaderResult, $dbReaderResult)) + ->willReturn($mapperResult); + + $this->assertSame($mapperResult, $this->reader->read($scope)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php new file mode 100644 index 0000000000000..c13205d34f25b --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php @@ -0,0 +1,60 @@ +objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\Model\Config\Source\Vertical::class, + [ + 'verticals' => [ + 'Apps and Games', + 'Athletic/Sporting Goods', + 'Art and Design' + ] + ] + ); + } + + /** + * @return void + */ + public function testToOptionArray() + { + $expectedOptionsArray = [ + ['value' => '', 'label' => __('--Please Select--')], + ['value' => 'Apps and Games', 'label' => __('Apps and Games')], + ['value' => 'Athletic/Sporting Goods', 'label' => __('Athletic/Sporting Goods')], + ['value' => 'Art and Design', 'label' => __('Art and Design')] + ]; + + $this->assertEquals( + $expectedOptionsArray, + $this->subject->toOptionArray() + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..8739219ebdf09 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,68 @@ +dataInterfaceMock = $this->getMockBuilder(DataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->config = $this->objectManagerHelper->getObject( + Config::class, + [ + 'data' => $this->dataInterfaceMock, + ] + ); + } + + /** + * @return void + */ + public function testGet() + { + $key = 'configKey'; + $defaultValue = 'mock'; + $configValue = 'emptyString'; + + $this->dataInterfaceMock + ->expects($this->once()) + ->method('get') + ->with($key, $defaultValue) + ->willReturn($configValue); + + $this->assertSame($configValue, $this->config->get($key, $defaultValue)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php new file mode 100644 index 0000000000000..5ee59a7913a61 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php @@ -0,0 +1,208 @@ +curlAdapterMock = $this->getMockBuilder( + \Magento\Framework\HTTP\Adapter\Curl::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->loggerMock = $this->getMockBuilder( + \Psr\Log\LoggerInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + $curlFactoryMock = $this->getMockBuilder(CurlFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $curlFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->curlAdapterMock); + + $this->responseFactoryMock = $this->getMockBuilder( + ResponseFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->converterMock = $this->createJsonConverter(); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->curl = $objectManagerHelper->getObject( + \Magento\Analytics\Model\Connector\Http\Client\Curl::class, + [ + 'curlFactory' => $curlFactoryMock, + 'responseFactory' => $this->responseFactoryMock, + 'converter' => $this->converterMock, + 'logger' => $this->loggerMock, + ] + ); + } + + /** + * Returns test parameters for request. + * + * @return array + */ + public function getTestData() + { + return [ + [ + 'data' => [ + 'version' => '1.1', + 'body'=> ['name' => 'value'], + 'url' => 'http://www.mystore.com', + 'headers' => [JsonConverter::CONTENT_TYPE_HEADER], + 'method' => \Magento\Framework\HTTP\ZendClient::POST, + ] + ] + ]; + } + + /** + * @return void + * @dataProvider getTestData + */ + public function testRequestSuccess(array $data) + { + $responseString = 'This is response.'; + $response = new \Zend_Http_Response(201, [], $responseString); + $this->curlAdapterMock->expects($this->once()) + ->method('write') + ->with( + $data['method'], + $data['url'], + $data['version'], + $data['headers'], + json_encode($data['body']) + ); + $this->curlAdapterMock->expects($this->once()) + ->method('read') + ->willReturn($responseString); + $this->curlAdapterMock->expects($this->any()) + ->method('getErrno') + ->willReturn(0); + + $this->responseFactoryMock->expects($this->any()) + ->method('create') + ->with($responseString) + ->willReturn($response); + + $this->assertEquals( + $response, + $this->curl->request( + $data['method'], + $data['url'], + $data['body'], + $data['headers'], + $data['version'] + ) + ); + } + + /** + * @return void + * @dataProvider getTestData + */ + public function testRequestError(array $data) + { + $response = new \Zend_Http_Response(0, []); + $this->curlAdapterMock->expects($this->once()) + ->method('write') + ->with( + $data['method'], + $data['url'], + $data['version'], + $data['headers'], + json_encode($data['body']) + ); + $this->curlAdapterMock->expects($this->once()) + ->method('read'); + $this->curlAdapterMock->expects($this->atLeastOnce()) + ->method('getErrno') + ->willReturn(1); + $this->curlAdapterMock->expects($this->atLeastOnce()) + ->method('getError') + ->willReturn('CURL error.'); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with( + new \Exception( + 'MBI service CURL connection error #1: CURL error.' + ) + ); + + $this->assertEquals( + $response, + $this->curl->request( + $data['method'], + $data['url'], + $data['body'], + $data['headers'], + $data['version'] + ) + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function createJsonConverter() + { + $converterMock = $this->getMockBuilder(ConverterInterface::class) + ->getMockForAbstractClass(); + $converterMock->expects($this->any())->method('toBody')->willReturnCallback(function ($value) { + return json_encode($value); + }); + $converterMock->expects($this->any()) + ->method('getContentTypeHeader') + ->willReturn(JsonConverter::CONTENT_TYPE_HEADER); + return $converterMock; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php new file mode 100644 index 0000000000000..5ad8eebfc7ad3 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php @@ -0,0 +1,73 @@ +objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->serializerMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + $this->converter = $this->objectManagerHelper->getObject( + JsonConverter::class, + ['serializer' => $this->serializerMock] + ); + } + + public function testConverterContainsHeader() + { + $this->assertEquals(JsonConverter::CONTENT_TYPE_HEADER, $this->converter->getContentTypeHeader()); + } + + /** + * @param array|null $unserializedResult + * @param array $expected + * @dataProvider convertBodyDataProvider + */ + public function testConvertBody($unserializedResult, $expected) + { + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->willReturn($unserializedResult); + $this->assertEquals($expected, $this->converter->fromBody('body')); + } + + public function convertBodyDataProvider() + { + return [ + [null, ['body']], + [['unserializedBody'], ['unserializedBody']] + ]; + } + + public function testConvertData() + { + $this->serializerMock->expects($this->once()) + ->method('serialize') + ->willReturn('serializedResult'); + $this->assertEquals('serializedResult', $this->converter->toBody(["token" => "secret-token"])); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php new file mode 100644 index 0000000000000..3d4c90bcd07f7 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php @@ -0,0 +1,51 @@ + 'testValue']; + $response = new \Zend_Http_Response(201, [], json_encode($expectedBody)); + $responseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class) + ->getMockForAbstractClass(); + $responseHandlerMock->expects($this->once()) + ->method('handleResponse') + ->with($expectedBody) + ->willReturn(true); + $notFoundResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class) + ->getMockForAbstractClass(); + $notFoundResponseHandlerMock->expects($this->never())->method('handleResponse'); + $serializerMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + $serializerMock->expects($this->once()) + ->method('unserialize') + ->willReturn($expectedBody); + $objectManager = new ObjectManager($this); + $responseResolver = $objectManager->getObject( + ResponseResolver::class, + [ + 'converter' => $objectManager->getObject( + JsonConverter::class, + ['serializer' => $serializerMock] + ), + 'responseHandlers' => [ + 201 => $responseHandlerMock, + 404 => $notFoundResponseHandlerMock, + ] + ] + ); + $this->assertTrue($responseResolver->getResult($response)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php new file mode 100644 index 0000000000000..c2b6c72e868c1 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php @@ -0,0 +1,127 @@ +analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->httpClientMock = $this->getMockBuilder(ClientInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $successHandler = $this->getMockBuilder(\Magento\Analytics\Model\Connector\Http\ResponseHandlerInterface::class) + ->getMockForAbstractClass(); + $successHandler->method('handleResponse') + ->willReturn(true); + $serializerMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + $serializerMock->expects($this->any()) + ->method('unserialize') + ->willReturn(['unserialized data']); + $objectManager = new ObjectManager($this); + $this->notifyDataChangedCommand = $objectManager->getObject( + NotifyDataChangedCommand::class, + [ + 'analyticsToken' => $this->analyticsTokenMock, + 'httpClient' => $this->httpClientMock, + 'config' => $this->configMock, + 'responseResolver' => $objectManager->getObject( + ResponseResolver::class, + [ + 'converter' => $objectManager->getObject( + JsonConverter::class, + ['serializer' => $serializerMock] + ), + 'responseHandlers' => [201 => $successHandler] + ] + ), + 'logger' => $this->loggerMock + ] + ); + } + + public function testExecuteSuccess() + { + $configVal = "Config val"; + $token = "Secret token!"; + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + $this->configMock->expects($this->any()) + ->method('getValue') + ->willReturn($configVal); + $this->analyticsTokenMock->expects($this->once()) + ->method('getToken') + ->willReturn($token); + $this->httpClientMock->expects($this->once()) + ->method('request') + ->with( + ZendClient::POST, + $configVal, + ['access-token' => $token, 'url' => $configVal] + )->willReturn(new \Zend_Http_Response(201, [])); + $this->assertTrue($this->notifyDataChangedCommand->execute()); + } + + public function testExecuteWithoutToken() + { + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + $this->httpClientMock->expects($this->never()) + ->method('request'); + $this->assertFalse($this->notifyDataChangedCommand->execute()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php new file mode 100644 index 0000000000000..8a3f4efb15cf4 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php @@ -0,0 +1,187 @@ +loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->httpClientMock = $this->getMockBuilder(ClientInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->subject = new OTPRequest( + $this->analyticsTokenMock, + $this->httpClientMock, + $this->configMock, + $this->responseResolverMock, + $this->loggerMock + ); + } + + /** + * Returns test parameters for request. + * + * @return array + */ + private function getTestData() + { + return [ + 'otp' => 'thisisotp', + 'url' => 'http://www.mystore.com', + 'access-token' => 'thisisaccesstoken', + 'method' => \Magento\Framework\HTTP\ZendClient::POST, + 'body'=> ['access-token' => 'thisisaccesstoken','url' => 'http://www.mystore.com'], + ]; + } + + /** + * @return void + */ + public function testCallSuccess() + { + $data = $this->getTestData(); + + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + $this->analyticsTokenMock->expects($this->once()) + ->method('getToken') + ->willReturn($data['access-token']); + + $this->configMock->expects($this->any()) + ->method('getValue') + ->willReturn($data['url']); + + $this->httpClientMock->expects($this->once()) + ->method('request') + ->with( + $data['method'], + $data['url'], + $data['body'] + ) + ->willReturn(new \Zend_Http_Response(201, [])); + $this->responseResolverMock->expects($this->once()) + ->method('getResult') + ->willReturn($data['otp']); + + $this->assertEquals( + $data['otp'], + $this->subject->call() + ); + } + + /** + * @return void + */ + public function testCallNoAccessToken() + { + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + + $this->httpClientMock->expects($this->never()) + ->method('request'); + + $this->assertFalse($this->subject->call()); + } + + /** + * @return void + */ + public function testCallNoOtp() + { + $data = $this->getTestData(); + + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + $this->analyticsTokenMock->expects($this->once()) + ->method('getToken') + ->willReturn($data['access-token']); + + $this->configMock->expects($this->any()) + ->method('getValue') + ->willReturn($data['url']); + + $this->httpClientMock->expects($this->once()) + ->method('request') + ->with( + $data['method'], + $data['url'], + $data['body'] + ) + ->willReturn(new \Zend_Http_Response(0, [])); + + $this->responseResolverMock->expects($this->once()) + ->method('getResult') + ->willReturn(false); + + $this->loggerMock->expects($this->once()) + ->method('warning'); + + $this->assertFalse($this->subject->call()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php new file mode 100644 index 0000000000000..4f3101e87ab9a --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php @@ -0,0 +1,19 @@ +assertFalse($OTPHandler->handleResponse([])); + $expectedOtp = 123; + $this->assertEquals($expectedOtp, $OTPHandler->handleResponse(['otp' => $expectedOtp])); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php new file mode 100644 index 0000000000000..928883ca7bad4 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php @@ -0,0 +1,33 @@ +getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + $analyticsToken->expects($this->once()) + ->method('storeToken') + ->with(null); + $subscriptionHandler = $this->getMockBuilder(SubscriptionHandler::class) + ->disableOriginalConstructor() + ->getMock(); + $subscriptionStatusProvider = $this->getMockBuilder(SubscriptionStatusProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $subscriptionStatusProvider->method('getStatus')->willReturn(SubscriptionStatusProvider::ENABLED); + $reSignUpHandler = new ReSignUp($analyticsToken, $subscriptionHandler, $subscriptionStatusProvider); + $this->assertFalse($reSignUpHandler->handleResponse([])); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php new file mode 100644 index 0000000000000..15f9884428d2e --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php @@ -0,0 +1,31 @@ +getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + $analyticsToken->expects($this->once()) + ->method('storeToken') + ->with($accessToken); + $objectManager = new ObjectManager($this); + $signUpHandler = $objectManager->getObject( + SignUp::class, + ['analyticsToken' => $analyticsToken] + ); + $this->assertFalse($signUpHandler->handleResponse([])); + $this->assertEquals($accessToken, $signUpHandler->handleResponse(['access-token' => $accessToken])); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php new file mode 100644 index 0000000000000..9a3093535afbf --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php @@ -0,0 +1,17 @@ +assertTrue($updateHandler->handleResponse([])); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php new file mode 100644 index 0000000000000..db6cda7153c1a --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php @@ -0,0 +1,171 @@ +analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + $this->integrationManagerMock = $this->getMockBuilder(IntegrationManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->integrationToken = $this->getMockBuilder(IntegrationToken::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->httpClientMock = $this->getMockBuilder(ClientInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->signUpCommand = new SignUpCommand( + $this->analyticsTokenMock, + $this->integrationManagerMock, + $this->configMock, + $this->httpClientMock, + $this->loggerMock, + $this->responseResolverMock + ); + } + + public function testExecuteSuccess() + { + $this->integrationManagerMock->expects($this->once()) + ->method('generateToken') + ->willReturn($this->integrationToken); + $this->integrationManagerMock->expects($this->once()) + ->method('activateIntegration') + ->willReturn(true); + $data = $this->getTestData(); + + $this->configMock->expects($this->any()) + ->method('getValue') + ->willReturn($data['url']); + $this->integrationToken->expects($this->any()) + ->method('getData') + ->with('token') + ->willReturn($data['integration-token']); + $httpResponse = new \Zend_Http_Response(201, [], '{"access-token": "' . $data['access-token'] . '"}'); + $this->httpClientMock->expects($this->once()) + ->method('request') + ->with( + $data['method'], + $data['url'], + $data['body'] + ) + ->willReturn($httpResponse); + $this->responseResolverMock->expects($this->any()) + ->method('getResult') + ->with($httpResponse) + ->willReturn(true); + $this->assertTrue($this->signUpCommand->execute()); + } + + public function testExecuteFailureCannotGenerateToken() + { + $this->integrationManagerMock->expects($this->once()) + ->method('generateToken') + ->willReturn(false); + $this->integrationManagerMock->expects($this->never()) + ->method('activateIntegration'); + $this->assertFalse($this->signUpCommand->execute()); + } + + public function testExecuteFailureResponseIsEmpty() + { + $this->integrationManagerMock->expects($this->once()) + ->method('generateToken') + ->willReturn($this->integrationToken); + $this->integrationManagerMock->expects($this->once()) + ->method('activateIntegration') + ->willReturn(true); + $httpResponse = new \Zend_Http_Response(0, []); + $this->httpClientMock->expects($this->once()) + ->method('request') + ->willReturn($httpResponse); + $this->responseResolverMock->expects($this->any()) + ->method('getResult') + ->willReturn(false); + $this->assertFalse($this->signUpCommand->execute()); + } + + /** + * Returns test parameters for request. + * + * @return array + */ + private function getTestData() + { + return [ + 'url' => 'http://www.mystore.com', + 'access-token' => 'thisisaccesstoken', + 'integration-token' => 'thisisintegrationtoken', + 'headers' => [JsonConverter::CONTENT_TYPE_HEADER], + 'method' => \Magento\Framework\HTTP\ZendClient::POST, + 'body'=> ['token' => 'thisisintegrationtoken','url' => 'http://www.mystore.com'], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php new file mode 100644 index 0000000000000..bf3c79e22fc52 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php @@ -0,0 +1,140 @@ +analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->httpClientMock = $this->getMockBuilder(ClientInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->updateCommand = new UpdateCommand( + $this->analyticsTokenMock, + $this->httpClientMock, + $this->configMock, + $this->loggerMock, + $this->flagManagerMock, + $this->responseResolverMock + ); + } + + public function testExecuteSuccess() + { + $url = "old.localhost.com"; + $configVal = "Config val"; + $token = "Secret token!"; + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + + $this->configMock->expects($this->any()) + ->method('getValue') + ->willReturn($configVal); + + $this->flagManagerMock->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) + ->willReturn($url); + + $this->analyticsTokenMock->expects($this->once()) + ->method('getToken') + ->willReturn($token); + + $this->httpClientMock->expects($this->once()) + ->method('request') + ->with( + ZendClient::PUT, + $configVal, + [ + 'url' => $url, + 'new-url' => $configVal, + 'access-token' => $token + ] + )->willReturn(new \Zend_Http_Response(200, [])); + + $this->responseResolverMock->expects($this->once()) + ->method('getResult') + ->willReturn(true); + + $this->assertTrue($this->updateCommand->execute()); + } + + public function testExecuteWithoutToken() + { + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + + $this->assertFalse($this->updateCommand->execute()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php new file mode 100644 index 0000000000000..714d0daf5c419 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php @@ -0,0 +1,67 @@ +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..6ccf81cb94bad --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php @@ -0,0 +1,223 @@ +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..e85dde90657ff --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php @@ -0,0 +1,58 @@ +objectManagerHelper = new ObjectManagerHelper($this); + } + + /** + * @param string $content + * @param string|null $initializationVector + * @return void + * @dataProvider constructDataProvider + */ + public function testConstruct($content, $initializationVector) + { + $constructorArguments = [ + 'content' => $content, + 'initializationVector' => $initializationVector, + ]; + /** @var EncodedContext $encodedContext */ + $encodedContext = $this->objectManagerHelper->getObject( + EncodedContext::class, + array_filter($constructorArguments) + ); + + $this->assertSame($content, $encodedContext->getContent()); + $this->assertSame($initializationVector ?: '', $encodedContext->getInitializationVector()); + } + + /** + * @return array + */ + public function constructDataProvider() + { + return [ + 'Without Initialization Vector' => ['content text', null], + 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php new file mode 100644 index 0000000000000..d1cf2ce48a04e --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php @@ -0,0 +1,71 @@ +objectManagerHelper = new ObjectManagerHelper($this); + } + + /** + * @return void + */ + public function testThatNotifyExecuted() + { + $expectedResult = true; + $notifyCommandName = 'notifyDataChanged'; + $exportDataHandlerMockObject = $this->createExportDataHandlerMock(); + $analyticsConnectorMockObject = $this->createAnalyticsConnectorMock(); + /** + * @var $exportDataHandlerNotification ExportDataHandlerNotification + */ + $exportDataHandlerNotification = $this->objectManagerHelper->getObject( + ExportDataHandlerNotification::class, + [ + 'exportDataHandler' => $exportDataHandlerMockObject, + 'connector' => $analyticsConnectorMockObject, + ] + ); + $exportDataHandlerMockObject->expects($this->once()) + ->method('prepareExportData') + ->willReturn($expectedResult); + $analyticsConnectorMockObject->expects($this->once()) + ->method('execute') + ->with($notifyCommandName); + $this->assertEquals($expectedResult, $exportDataHandlerNotification->prepareExportData()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function createExportDataHandlerMock() + { + return $this->getMockBuilder(ExportDataHandler::class)->disableOriginalConstructor()->getMock(); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function createAnalyticsConnectorMock() + { + return $this->getMockBuilder(Connector::class)->disableOriginalConstructor()->getMock(); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php new file mode 100644 index 0000000000000..47747027ed702 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php @@ -0,0 +1,267 @@ +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..e49c942161cf2 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php @@ -0,0 +1,191 @@ +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..2a3588c8032fc --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php @@ -0,0 +1,59 @@ +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..bf5795111230b --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php @@ -0,0 +1,206 @@ +fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->fileInfoMock = $this->getMockBuilder(FileInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->directoryMock = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->fileRecorder = $this->objectManagerHelper->getObject( + FileRecorder::class, + [ + 'fileInfoManager' => $this->fileInfoManagerMock, + 'fileInfoFactory' => $this->fileInfoFactoryMock, + 'filesystem' => $this->filesystemMock, + 'fileSubdirectoryPath' => $this->fileSubdirectoryPath, + 'encodedFileName' => $this->encodedFileName, + ] + ); + } + + /** + * @param string $pathToExistingFile + * @dataProvider recordNewFileDataProvider + */ + public function testRecordNewFile($pathToExistingFile) + { + $content = openssl_random_pseudo_bytes(200); + + $this->filesystemMock + ->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($this->directoryMock); + + $this->encodedContextMock + ->expects($this->once()) + ->method('getContent') + ->with() + ->willReturn($content); + + $hashLength = 64; + $fileRelativePathPattern = '#' . preg_quote($this->fileSubdirectoryPath, '#') + . '.{' . $hashLength . '}/' . preg_quote($this->encodedFileName, '#') . '#'; + $this->directoryMock + ->expects($this->once()) + ->method('writeFile') + ->with($this->matchesRegularExpression($fileRelativePathPattern), $content) + ->willReturn($this->directoryMock); + + $this->fileInfoManagerMock + ->expects($this->once()) + ->method('load') + ->with() + ->willReturn($this->fileInfoMock); + + $this->encodedContextMock + ->expects($this->once()) + ->method('getInitializationVector') + ->with() + ->willReturn('init_vector***'); + + /** register file */ + $this->fileInfoFactoryMock + ->expects($this->once()) + ->method('create') + ->with($this->callback( + function ($parameters) { + return !empty($parameters['path']) && ('init_vector***' === $parameters['initializationVector']); + } + )) + ->willReturn($this->fileInfoMock); + $this->fileInfoManagerMock + ->expects($this->once()) + ->method('save') + ->with($this->fileInfoMock); + + /** remove old file */ + $this->fileInfoMock + ->expects($this->exactly($pathToExistingFile ? 3 : 1)) + ->method('getPath') + ->with() + ->willReturn($pathToExistingFile); + $directoryName = dirname($pathToExistingFile); + if ($directoryName === '.') { + $this->directoryMock + ->expects($this->once()) + ->method('delete') + ->with($pathToExistingFile); + } elseif ($directoryName) { + $this->directoryMock + ->expects($this->exactly(2)) + ->method('delete') + ->withConsecutive( + [$pathToExistingFile], + [$directoryName] + ); + } + + $this->assertTrue($this->fileRecorder->recordNewFile($this->encodedContextMock)); + } + + /** + * @return array + */ + public function recordNewFileDataProvider() + { + return [ + 'File doesn\'t exist' => [''], + 'Existing file into subdirectory' => ['dir_name/file.txt'], + 'Existing file doesn\'t into subdirectory' => ['file.txt'], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php new file mode 100644 index 0000000000000..568047a4521f8 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php @@ -0,0 +1,225 @@ +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..2053187e641ae --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php @@ -0,0 +1,163 @@ +linkInterfaceFactoryMock = $this->getMockBuilder(LinkInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerInterfaceMock = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->linkInterfaceMock = $this->getMockBuilder(LinkInterface::class) + ->getMockForAbstractClass(); + $this->fileInfoMock = $this->getMockBuilder(FileInfo::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->linkProvider = $this->objectManagerHelper->getObject( + LinkProvider::class, + [ + 'linkFactory' => $this->linkInterfaceFactoryMock, + 'fileInfoManager' => $this->fileInfoManagerMock, + 'storeManager' => $this->storeManagerInterfaceMock + ] + ); + } + + public function testGet() + { + $baseUrl = 'http://magento.local/pub/media/'; + $fileInfoPath = 'analytics/data.tgz'; + $fileInitializationVector = 'er312esq23eqq'; + $this->fileInfoManagerMock->expects($this->once()) + ->method('load') + ->willReturn($this->fileInfoMock); + $this->linkInterfaceFactoryMock->expects($this->once()) + ->method('create') + ->with( + [ + 'initializationVector' => base64_encode($fileInitializationVector), + 'url' => $baseUrl . $fileInfoPath + ] + ) + ->willReturn($this->linkInterfaceMock); + $this->storeManagerInterfaceMock->expects($this->once()) + ->method('getStore')->willReturn($this->storeMock); + $this->storeMock->expects($this->once()) + ->method('getBaseUrl') + ->with( + UrlInterface::URL_TYPE_MEDIA + ) + ->willReturn($baseUrl); + $this->fileInfoMock->expects($this->atLeastOnce()) + ->method('getPath') + ->willReturn($fileInfoPath); + $this->fileInfoMock->expects($this->atLeastOnce()) + ->method('getInitializationVector') + ->willReturn($fileInitializationVector); + $this->assertEquals($this->linkInterfaceMock, $this->linkProvider->get()); + } + + /** + * @param string|null $fileInfoPath + * @param string|null $fileInitializationVector + * + * @dataProvider fileNotReadyDataProvider + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage File is not ready yet. + */ + public function testFileNotReady($fileInfoPath, $fileInitializationVector) + { + $this->fileInfoManagerMock->expects($this->once()) + ->method('load') + ->willReturn($this->fileInfoMock); + $this->fileInfoMock->expects($this->once()) + ->method('getPath') + ->willReturn($fileInfoPath); + $this->fileInfoMock->expects($this->any()) + ->method('getInitializationVector') + ->willReturn($fileInitializationVector); + $this->linkProvider->get(); + } + + /** + * @return array + */ + public function fileNotReadyDataProvider() + { + return [ + [null, 'initVector'], + ['path', null], + ['', 'initVector'], + ['path', ''], + ['', ''], + [null, null] + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php new file mode 100644 index 0000000000000..cdcd726f3e8fa --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php @@ -0,0 +1,144 @@ +subscriptionUpdateHandlerMock = $this->getMockBuilder(SubscriptionUpdateHandler::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configValueMock = $this->getMockBuilder(Value::class) + ->disableOriginalConstructor() + ->setMethods(['isValueChanged', 'getPath', 'getScope', 'getOldValue']) + ->getMock(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->plugin = $this->objectManagerHelper->getObject( + BaseUrlConfigPlugin::class, + [ + 'subscriptionUpdateHandler' => $this->subscriptionUpdateHandlerMock, + ] + ); + } + + /** + * @param array $configValueData + * @return void + * @dataProvider afterSavePluginIsNotApplicableDataProvider + */ + public function testAfterSavePluginIsNotApplicable( + array $configValueData + ) { + $this->configValueMock + ->method('isValueChanged') + ->willReturn($configValueData['isValueChanged']); + $this->configValueMock + ->method('getPath') + ->willReturn($configValueData['path']); + $this->configValueMock + ->method('getScope') + ->willReturn($configValueData['scope']); + $this->subscriptionUpdateHandlerMock + ->expects($this->never()) + ->method('processUrlUpdate'); + + $this->assertEquals( + $this->configValueMock, + $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock) + ); + } + + /** + * @return array + */ + public function afterSavePluginIsNotApplicableDataProvider() + { + return [ + 'Value has not been changed' => [ + 'Config Value Data' => [ + 'isValueChanged' => false, + 'path' => Store::XML_PATH_SECURE_BASE_URL, + 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ], + ], + 'Unsecure URL has been changed' => [ + 'Config Value Data' => [ + 'isValueChanged' => true, + 'path' => Store::XML_PATH_UNSECURE_BASE_URL, + 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ], + ], + 'Secure URL has been changed not in the Default scope' => [ + 'Config Value Data' => [ + 'isValueChanged' => true, + 'path' => Store::XML_PATH_SECURE_BASE_URL, + 'scope' => ScopeInterface::SCOPE_STORES + ], + ], + ]; + } + + /** + * @return void + */ + public function testAfterSavePluginIsApplicable() + { + $this->configValueMock + ->method('isValueChanged') + ->willReturn(true); + $this->configValueMock + ->method('getPath') + ->willReturn(Store::XML_PATH_SECURE_BASE_URL); + $this->configValueMock + ->method('getScope') + ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + $this->configValueMock + ->method('getOldValue') + ->willReturn('http://store.com'); + $this->subscriptionUpdateHandlerMock + ->expects($this->once()) + ->method('processUrlUpdate') + ->with('http://store.com'); + + $this->assertEquals( + $this->configValueMock, + $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock) + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php new file mode 100644 index 0000000000000..0607a977e5b68 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php @@ -0,0 +1,149 @@ +configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->otpRequestMock = $this->getMockBuilder(OTPRequest::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->reportUrlProvider = $this->objectManagerHelper->getObject( + ReportUrlProvider::class, + [ + 'config' => $this->configMock, + 'analyticsToken' => $this->analyticsTokenMock, + 'otpRequest' => $this->otpRequestMock, + 'flagManager' => $this->flagManagerMock, + 'urlReportConfigPath' => $this->urlReportConfigPath, + ] + ); + } + + /** + * @param bool $isTokenExist + * @param string|null $otp If null OTP was not received. + * + * @dataProvider getUrlDataProvider + */ + public function testGetUrl($isTokenExist, $otp) + { + $reportUrl = 'https://example.com/report'; + $url = ''; + + $this->configMock + ->expects($this->once()) + ->method('getValue') + ->with($this->urlReportConfigPath) + ->willReturn($reportUrl); + $this->analyticsTokenMock + ->expects($this->once()) + ->method('isTokenExist') + ->with() + ->willReturn($isTokenExist); + $this->otpRequestMock + ->expects($isTokenExist ? $this->once() : $this->never()) + ->method('call') + ->with() + ->willReturn($otp); + if ($isTokenExist && $otp) { + $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&'); + } + $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl()); + } + + /** + * @return array + */ + public function getUrlDataProvider() + { + return [ + 'TokenDoesNotExist' => [false, null], + 'TokenExistAndOtpEmpty' => [true, null], + 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'], + ]; + } + + /** + * @return void + */ + public function testGetUrlWhenSubscriptionUpdateRunning() + { + $this->flagManagerMock + ->expects($this->once()) + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) + ->willReturn('http://store.com'); + $this->expectException(SubscriptionUpdateException::class); + $this->reportUrlProvider->getUrl(); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php new file mode 100644 index 0000000000000..d9b030b84d639 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php @@ -0,0 +1,213 @@ +configInterfaceMock = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass(); + $this->reportValidatorMock = $this->getMockBuilder(ReportValidator::class) + ->disableOriginalConstructor()->getMock(); + $this->providerFactoryMock = $this->getMockBuilder(ProviderFactory::class) + ->disableOriginalConstructor()->getMock(); + $this->reportProviderMock = $this->getMockBuilder(ReportProvider::class) + ->disableOriginalConstructor()->getMock(); + $this->directoryMock = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->reportWriter = $this->objectManagerHelper->getObject( + ReportWriter::class, + [ + 'config' => $this->configInterfaceMock, + 'reportValidator' => $this->reportValidatorMock, + 'providerFactory' => $this->providerFactoryMock + ] + ); + } + + /** + * @param array $configData + * @return void + * + * @dataProvider configDataProvider + */ + public function testWrite(array $configData) + { + $errors = []; + $fileData = [ + ['number' => 1, 'type' => 'Shoes Usual'] + ]; + $this->configInterfaceMock + ->expects($this->once()) + ->method('get') + ->with() + ->willReturn([$configData]); + $this->providerFactoryMock + ->expects($this->once()) + ->method('create') + ->with($this->providerClass) + ->willReturn($this->reportProviderMock); + $parameterName = isset(reset($configData)[0]['parameters']['name']) + ? reset($configData)[0]['parameters']['name'] + : ''; + $this->reportProviderMock->expects($this->once()) + ->method('getReport') + ->with($parameterName ?: null) + ->willReturn($fileData); + $errorStreamMock = $this->getMockBuilder( + \Magento\Framework\Filesystem\File\WriteInterface::class + )->getMockForAbstractClass(); + $errorStreamMock + ->expects($this->once()) + ->method('lock') + ->with(); + $errorStreamMock + ->expects($this->exactly(2)) + ->method('writeCsv') + ->withConsecutive( + [array_keys($fileData[0])], + [$fileData[0]] + ); + $errorStreamMock->expects($this->once())->method('unlock'); + $errorStreamMock->expects($this->once())->method('close'); + if ($parameterName) { + $this->reportValidatorMock + ->expects($this->once()) + ->method('validate') + ->with($parameterName) + ->willReturn($errors); + } + $this->directoryMock + ->expects($this->once()) + ->method('openFile') + ->with( + $this->stringContains('/var/tmp' . $parameterName ?: $this->reportName), + 'w+' + )->willReturn($errorStreamMock); + $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp')); + } + + /** + * @param array $configData + * @return void + * + * @dataProvider configDataProvider + */ + public function testWriteErrorFile($configData) + { + $errors = ['orders', 'SQL Error: test']; + $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]); + $errorStreamMock = $this->getMockBuilder( + \Magento\Framework\Filesystem\File\WriteInterface::class + )->getMockForAbstractClass(); + $errorStreamMock->expects($this->once())->method('lock'); + $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors); + $errorStreamMock->expects($this->once())->method('unlock'); + $errorStreamMock->expects($this->once())->method('close'); + $this->reportValidatorMock->expects($this->once())->method('validate')->willReturn($errors); + $this->directoryMock->expects($this->once())->method('openFile')->with('/var/tmp' . 'errors.csv', 'w+') + ->willReturn($errorStreamMock); + $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp')); + } + + /** + * @return void + */ + public function testWriteEmptyReports() + { + $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]); + $this->reportValidatorMock->expects($this->never())->method('validate'); + $this->directoryMock->expects($this->never())->method('openFile'); + $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp')); + } + + /** + * @return array + */ + public function configDataProvider() + { + return [ + 'reportProvider' => [ + [ + 'providers' => [ + [ + 'name' => $this->providerName, + 'class' => $this->providerClass, + 'parameters' => [ + 'name' => $this->reportName + ], + ] + ] + ] + ], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php new file mode 100644 index 0000000000000..f314d77f32b41 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php @@ -0,0 +1,50 @@ +moduleManagerMock = $this->getMockBuilder(ModuleManager::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerHelper = new ObjectManagerHelper($this); + $this->moduleIterator = $objectManagerHelper->getObject( + ModuleIterator::class, + [ + 'moduleManager' => $this->moduleManagerMock, + 'iterator' => new \ArrayIterator([0 => ['module_name' => 'Coco_Module']]) + ] + ); + } + + public function testCurrent() + { + $this->moduleManagerMock->expects($this->once()) + ->method('isEnabled') + ->with('Coco_Module') + ->willReturn(true); + foreach ($this->moduleIterator as $item) { + $this->assertEquals(['module_name' => 'Coco_Module', 'status' => 'Enabled'], $item); + } + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php new file mode 100644 index 0000000000000..cc46d175543ad --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php @@ -0,0 +1,123 @@ +scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->websiteMock = $this->getMockBuilder(WebsiteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configPaths = [ + 'web/unsecure/base_url', + 'currency/options/base', + 'general/locale/timezone' + ]; + + $this->storeConfigurationProvider = new StoreConfigurationProvider( + $this->scopeConfigMock, + $this->storeManagerMock, + $this->configPaths + ); + } + + public function testGetReport() + { + $map = [ + ['web/unsecure/base_url', 'default', 0, '127.0.0.1'], + ['currency/options/base', 'default', 0, 'USD'], + ['general/locale/timezone', 'default', 0, 'America/Dawson'], + ['web/unsecure/base_url', 'websites', 1, '127.0.0.2'], + ['currency/options/base', 'websites', 1, 'USD'], + ['general/locale/timezone', 'websites', 1, 'America/Belem'], + ['web/unsecure/base_url', 'stores', 2, '127.0.0.3'], + ['currency/options/base', 'stores', 2, 'USD'], + ['general/locale/timezone', 'stores', 2, 'America/Phoenix'], + ]; + + $this->scopeConfigMock + ->method('getValue') + ->will($this->returnValueMap($map)); + + $this->storeManagerMock->expects($this->once()) + ->method('getWebsites') + ->willReturn([$this->websiteMock]); + + $this->storeManagerMock->expects($this->once()) + ->method('getStores') + ->willReturn([$this->storeMock]); + + $this->websiteMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn(2); + $result = iterator_to_array($this->storeConfigurationProvider->getReport()); + $resultValues = []; + foreach ($result as $item) { + $resultValues[] = array_values($item); + } + array_multisort($resultValues); + array_multisort($map); + $this->assertEquals($resultValues, $map); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php new file mode 100644 index 0000000000000..373bb73d999f8 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php @@ -0,0 +1,193 @@ +scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + + $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->flagManagerMock = $this->getMockBuilder(FlagManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->statusProvider = $this->objectManagerHelper->getObject( + SubscriptionStatusProvider::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'analyticsToken' => $this->analyticsTokenMock, + 'flagManager' => $this->flagManagerMock, + ] + ); + } + + /** + * @param array $flagManagerData + * @dataProvider getStatusShouldBeFailedDataProvider + */ + public function testGetStatusShouldBeFailed(array $flagManagerData) + { + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(false); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('analytics/subscription/enabled') + ->willReturn(true); + + $this->expectFlagManagerReturn($flagManagerData); + $this->assertEquals(SubscriptionStatusProvider::FAILED, $this->statusProvider->getStatus()); + } + + /** + * @return array + */ + public function getStatusShouldBeFailedDataProvider() + { + return [ + 'Subscription update doesn\'t active' => [ + 'Flag Manager data mapping' => [ + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null], + [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null] + ], + ], + 'Subscription update is active' => [ + 'Flag Manager data mapping' => [ + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'], + [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null] + ], + ], + ]; + } + + /** + * @param array $flagManagerData + * @param bool $isTokenExist + * @dataProvider getStatusShouldBePendingDataProvider + */ + public function testGetStatusShouldBePending(array $flagManagerData, bool $isTokenExist) + { + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn($isTokenExist); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('analytics/subscription/enabled') + ->willReturn(true); + + $this->expectFlagManagerReturn($flagManagerData); + $this->assertEquals(SubscriptionStatusProvider::PENDING, $this->statusProvider->getStatus()); + } + + /** + * @return array + */ + public function getStatusShouldBePendingDataProvider() + { + return [ + 'Subscription update doesn\'t active and the token does not exist' => [ + 'Flag Manager data mapping' => [ + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null], + [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45] + ], + 'isTokenExist' => false, + ], + 'Subscription update is active and the token does not exist' => [ + 'Flag Manager data mapping' => [ + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'], + [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45] + ], + 'isTokenExist' => false, + ], + 'Subscription update is active and token exist' => [ + 'Flag Manager data mapping' => [ + [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'], + [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null] + ], + 'isTokenExist' => true, + ], + ]; + } + + public function testGetStatusShouldBeEnabled() + { + $this->flagManagerMock + ->method('getFlagData') + ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) + ->willReturn(null); + $this->analyticsTokenMock->expects($this->once()) + ->method('isTokenExist') + ->willReturn(true); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('analytics/subscription/enabled') + ->willReturn(true); + $this->assertEquals(SubscriptionStatusProvider::ENABLED, $this->statusProvider->getStatus()); + } + + public function testGetStatusShouldBeDisabled() + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('analytics/subscription/enabled') + ->willReturn(false); + $this->assertEquals(SubscriptionStatusProvider::DISABLED, $this->statusProvider->getStatus()); + } + + /** + * @param array $mapping + */ + private function expectFlagManagerReturn(array $mapping) + { + $this->flagManagerMock + ->method('getFlagData') + ->willReturnMap($mapping); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php new file mode 100644 index 0000000000000..8c515afa32dd6 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php @@ -0,0 +1,103 @@ +subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlBuilderMock = $this->getMockBuilder(UrlInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->notification = $this->objectManagerHelper->getObject( + NotificationAboutFailedSubscription::class, + [ + 'subscriptionStatusProvider' => $this->subscriptionStatusMock, + 'urlBuilder' => $this->urlBuilderMock + ] + ); + } + + public function testIsDisplayedWhenMessageShouldBeDisplayed() + { + $this->subscriptionStatusMock->expects($this->once()) + ->method('getStatus') + ->willReturn( + SubscriptionStatusProvider::FAILED + ); + $this->assertTrue($this->notification->isDisplayed()); + } + + /** + * @dataProvider notDisplayedNotificationStatuses + * + * @param $status + */ + public function testIsDisplayedWhenMessageShouldNotBeDisplayed($status) + { + $this->subscriptionStatusMock->expects($this->once()) + ->method('getStatus') + ->willReturn($status); + $this->assertFalse($this->notification->isDisplayed()); + } + + public function testGetTextShouldBuildMessage() + { + $retryUrl = 'http://magento.dev/retryUrl'; + $this->urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with('analytics/subscription/retry') + ->willReturn($retryUrl); + $messageDetails = 'Failed to synchronize data to the Magento Business Intelligence service. '; + $messageDetails .= sprintf('Retry Synchronization', $retryUrl); + $this->assertEquals($messageDetails, $this->notification->getText()); + } + + /** + * Provide statuses according to which message should not be displayed. + * + * @return array + */ + public function notDisplayedNotificationStatuses() + { + return [ + [SubscriptionStatusProvider::PENDING], + [SubscriptionStatusProvider::DISABLED], + [SubscriptionStatusProvider::ENABLED], + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php new file mode 100644 index 0000000000000..3f1ed9a5cf4c0 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php @@ -0,0 +1,121 @@ +objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\ReportXml\Config\Converter\Xml::class + ); + } + + /** + * @return void + */ + public function testConvertNoElements() + { + $this->assertEmpty( + $this->subject->convert(new \DOMDocument()) + ); + } + + /** + * @return void + */ + public function testConvert() + { + $dom = new \DOMDocument(); + + $expectedArray = [ + 'config' => [ + [ + 'noNamespaceSchemaLocation' => 'urn:magento:module:Magento_Analytics:etc/reports.xsd', + 'report' => [ + [ + 'name' => 'test_report_1', + 'connection' => 'sales', + 'source' => [ + [ + 'name' => 'sales_order', + 'alias' => 'orders', + 'attribute' => [ + [ + 'name' => 'entity_id', + 'alias' => 'identifier', + ] + ], + 'filter' => [ + [ + 'glue' => 'and', + 'condition' => [ + [ + 'attribute' => 'entity_id', + 'operator' => 'gt', + '_value' => '10' + ] + ] + ] + ] + ] + ] + ], + [ + 'name' => 'test_report_2', + 'connection' => 'default', + 'source' => [ + [ + 'name' => 'customer_entity', + 'alias' => 'customers', + 'attribute' => [ + [ + 'name' => 'email' + ] + ], + 'filter' => [ + [ + 'glue' => 'and', + 'condition' => [ + [ + 'attribute' => 'dob', + 'operator' => 'null' + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ]; + + $dom->loadXML(file_get_contents(__DIR__ . '/../_files/valid_reports.xml')); + + $this->assertEquals($expectedArray, $this->subject->convert($dom)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php new file mode 100644 index 0000000000000..e764add9ba2f0 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php @@ -0,0 +1,44 @@ +mapper = new Mapper(); + } + + public function testExecute() + { + $configData['config'][0]['report'] = [ + [ + 'source' => ['product'], + 'name' => 'Product', + ] + ]; + $expectedResult = [ + 'Product' => [ + 'source' => 'product', + 'name' => 'Product', + ] + ]; + $this->assertEquals($this->mapper->execute($configData), $expectedResult); + } + + public function testExecuteWithoutReports() + { + $configData = []; + $this->assertEquals($this->mapper->execute($configData), []); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml new file mode 100644 index 0000000000000..e04ee96163797 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml @@ -0,0 +1,25 @@ + + + + + + + + 10 + + + + + + + + + + + + diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php new file mode 100644 index 0000000000000..7e6866d5a1e60 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php @@ -0,0 +1,61 @@ +dataMock = $this->getMockBuilder(DataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->config = $this->objectManagerHelper->getObject( + Config::class, + [ + 'data' => $this->dataMock, + ] + ); + } + + public function testGet() + { + $queryName = 'query string'; + $queryResult = [ 'query' => 1 ]; + + $this->dataMock + ->expects($this->once()) + ->method('get') + ->with($queryName) + ->willReturn($queryResult); + + $this->assertSame($queryResult, $this->config->get($queryName)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php new file mode 100644 index 0000000000000..1d40919ecfc4a --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php @@ -0,0 +1,103 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionMock = $this->getMockBuilder(MysqlPdoAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionNewMock = $this->getMockBuilder(MysqlPdoAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->connectionFactory = $this->objectManagerHelper->getObject( + ConnectionFactory::class, + [ + 'resourceConnection' => $this->resourceConnectionMock, + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + public function testGetConnection() + { + $connectionName = 'read'; + + $this->resourceConnectionMock + ->expects($this->once()) + ->method('getConnection') + ->with($connectionName) + ->willReturn($this->connectionMock); + + $this->connectionMock + ->expects($this->once()) + ->method('getConfig') + ->with() + ->willReturn(['persistent' => 1]); + + $this->objectManagerMock + ->expects($this->once()) + ->method('create') + ->with(get_class($this->connectionMock), ['config' => ['use_buffered_query' => false]]) + ->willReturn($this->connectionNewMock); + + $this->assertSame($this->connectionNewMock, $this->connectionFactory->getConnection($connectionName)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php new file mode 100644 index 0000000000000..3b01105a8873b --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php @@ -0,0 +1,143 @@ +nameResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\NameResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectBuilderMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\SelectBuilder::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->selectBuilderMock->expects($this->any()) + ->method('getFilters') + ->willReturn([]); + + $this->conditionResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\ConditionResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler::class, + [ + 'conditionResolver' => $this->conditionResolverMock, + 'nameResolver' => $this->nameResolverMock + ] + ); + } + + /** + * @return void + */ + public function testAssembleEmpty() + { + $queryConfigMock = [ + 'source' => [ + 'name' => 'sales_order', + 'alias' => 'sales' + ] + ]; + + $this->selectBuilderMock->expects($this->never()) + ->method('setFilters'); + + $this->assertEquals( + $this->selectBuilderMock, + $this->subject->assemble($this->selectBuilderMock, $queryConfigMock) + ); + } + + /** + * @return void + */ + public function testAssembleNotEmpty() + { + $queryConfigMock = [ + 'source' => [ + 'name' => 'sales_order', + 'alias' => 'sales', + 'filter' => [ + [ + 'glue' => 'and', + 'condition' => [ + [ + 'attribute' => 'entity_id', + 'operator' => 'null' + ] + ] + ] + ] + ] + ]; + + $this->nameResolverMock->expects($this->any()) + ->method('getAlias') + ->with($queryConfigMock['source']) + ->willReturn($queryConfigMock['source']['alias']); + + $this->conditionResolverMock->expects($this->once()) + ->method('getFilter') + ->with( + $this->selectBuilderMock, + $queryConfigMock['source']['filter'], + $queryConfigMock['source']['alias'] + ) + ->willReturn('(sales.entity_id IS NULL)'); + + $this->selectBuilderMock->expects($this->once()) + ->method('setFilters') + ->with(['(sales.entity_id IS NULL)']); + + $this->assertEquals( + $this->selectBuilderMock, + $this->subject->assemble($this->selectBuilderMock, $queryConfigMock) + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php new file mode 100644 index 0000000000000..575db94a7b7e1 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php @@ -0,0 +1,167 @@ +nameResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\NameResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectBuilderMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\SelectBuilder::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->selectBuilderMock->expects($this->any()) + ->method('getColumns') + ->willReturn([]); + + $this->columnsResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\ColumnsResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\ReportXml\DB\Assembler\FromAssembler::class, + [ + 'nameResolver' => $this->nameResolverMock, + 'columnsResolver' => $this->columnsResolverMock, + 'resourceConnection' => $this->resourceConnection, + ] + ); + } + + /** + * @dataProvider assembleDataProvider + * @param array $queryConfig + * @param string $tableName + * @return void + */ + public function testAssemble(array $queryConfig, $tableName) + { + $this->nameResolverMock->expects($this->any()) + ->method('getAlias') + ->with($queryConfig['source']) + ->willReturn($queryConfig['source']['alias']); + + $this->nameResolverMock->expects($this->once()) + ->method('getName') + ->with($queryConfig['source']) + ->willReturn($queryConfig['source']['name']); + + $this->resourceConnection + ->expects($this->once()) + ->method('getTableName') + ->with($queryConfig['source']['name']) + ->willReturn($tableName); + + $this->selectBuilderMock->expects($this->once()) + ->method('setFrom') + ->with([$queryConfig['source']['alias'] => $tableName]); + + $this->columnsResolverMock->expects($this->once()) + ->method('getColumns') + ->with($this->selectBuilderMock, $queryConfig['source']) + ->willReturn(['entity_id' => 'sales.entity_id']); + + $this->selectBuilderMock->expects($this->once()) + ->method('setColumns') + ->with(['entity_id' => 'sales.entity_id']); + + $this->assertEquals( + $this->selectBuilderMock, + $this->subject->assemble($this->selectBuilderMock, $queryConfig) + ); + } + + /** + * @return array + */ + public function assembleDataProvider() + { + return [ + 'Tables without prefixes' => [ + [ + 'source' => [ + 'name' => 'sales_order', + 'alias' => 'sales', + 'attribute' => [ + [ + 'name' => 'entity_id' + ] + ], + ], + ], + 'sales_order', + ], + 'Tables with prefixes' => [ + [ + 'source' => [ + 'name' => 'sales_order', + 'alias' => 'sales', + 'attribute' => [ + [ + 'name' => 'entity_id' + ] + ], + ], + ], + 'pref_sales_order', + ] + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php new file mode 100644 index 0000000000000..aaafd731552a0 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php @@ -0,0 +1,279 @@ +nameResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\NameResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectBuilderMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\SelectBuilder::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->selectBuilderMock->expects($this->any()) + ->method('getFilters') + ->willReturn([]); + $this->selectBuilderMock->expects($this->any()) + ->method('getColumns') + ->willReturn([]); + $this->selectBuilderMock->expects($this->any()) + ->method('getJoins') + ->willReturn([]); + + $this->columnsResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\ColumnsResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->conditionResolverMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\ConditionResolver::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler::class, + [ + 'conditionResolver' => $this->conditionResolverMock, + 'nameResolver' => $this->nameResolverMock, + 'columnsResolver' => $this->columnsResolverMock, + 'resourceConnection' => $this->resourceConnection, + ] + ); + } + + /** + * @return void + */ + public function testAssembleEmpty() + { + $queryConfigMock = [ + 'source' => [ + 'name' => 'sales_order', + 'alias' => 'sales' + ] + ]; + + $this->selectBuilderMock->expects($this->never()) + ->method('setColumns'); + $this->selectBuilderMock->expects($this->never()) + ->method('setFilters'); + $this->selectBuilderMock->expects($this->never()) + ->method('setJoins'); + + $this->assertEquals( + $this->selectBuilderMock, + $this->subject->assemble($this->selectBuilderMock, $queryConfigMock) + ); + } + + /** + * @param array $queryConfigMock + * @param array $joinsMock + * @param array $tablesMapping + * @return void + * @dataProvider assembleNotEmptyDataProvider + */ + public function testAssembleNotEmpty(array $queryConfigMock, array $joinsMock, array $tablesMapping) + { + $filtersMock = []; + + $this->nameResolverMock->expects($this->at(0)) + ->method('getAlias') + ->with($queryConfigMock['source']) + ->willReturn($queryConfigMock['source']['alias']); + $this->nameResolverMock->expects($this->at(1)) + ->method('getAlias') + ->with($queryConfigMock['source']['link-source'][0]) + ->willReturn($queryConfigMock['source']['link-source'][0]['alias']); + $this->nameResolverMock->expects($this->once()) + ->method('getName') + ->with($queryConfigMock['source']['link-source'][0]) + ->willReturn($queryConfigMock['source']['link-source'][0]['name']); + + $this->resourceConnection + ->expects($this->any()) + ->method('getTableName') + ->willReturnOnConsecutiveCalls(...array_values($tablesMapping)); + + $this->conditionResolverMock->expects($this->at(0)) + ->method('getFilter') + ->with( + $this->selectBuilderMock, + $queryConfigMock['source']['link-source'][0]['using'], + $queryConfigMock['source']['link-source'][0]['alias'], + $queryConfigMock['source']['alias'] + ) + ->willReturn('(billing.parent_id = `sales`.`entity_id`)'); + + if (isset($queryConfigMock['source']['link-source'][0]['filter'])) { + $filtersMock = ['(sales.entity_id IS NULL)']; + + $this->conditionResolverMock->expects($this->at(1)) + ->method('getFilter') + ->with( + $this->selectBuilderMock, + $queryConfigMock['source']['link-source'][0]['filter'], + $queryConfigMock['source']['link-source'][0]['alias'], + $queryConfigMock['source']['alias'] + ) + ->willReturn($filtersMock[0]); + + $this->columnsResolverMock->expects($this->once()) + ->method('getColumns') + ->with($this->selectBuilderMock, $queryConfigMock['source']['link-source'][0]) + ->willReturn( + [ + 'entity_id' => 'sales.entity_id', + 'billing_address_id' => 'billing.entity_id' + ] + ); + + $this->selectBuilderMock->expects($this->once()) + ->method('setColumns') + ->with( + [ + 'entity_id' => 'sales.entity_id', + 'billing_address_id' => 'billing.entity_id' + ] + ); + } + + $this->selectBuilderMock->expects($this->once()) + ->method('setFilters') + ->with($filtersMock); + $this->selectBuilderMock->expects($this->once()) + ->method('setJoins') + ->with($joinsMock); + + $this->assertEquals( + $this->selectBuilderMock, + $this->subject->assemble($this->selectBuilderMock, $queryConfigMock) + ); + } + + /** + * @return array + */ + public function assembleNotEmptyDataProvider() + { + return [ + [ + [ + 'source' => [ + 'name' => 'sales_order', + 'alias' => 'sales', + 'link-source' => [ + [ + 'name' => 'sales_order_address', + 'alias' => 'billing', + 'link-type' => 'left', + 'attribute' => [ + [ + 'alias' => 'billing_address_id', + 'name' => 'entity_id' + ] + ], + 'using' => [ + [ + 'glue' => 'and', + 'condition' => [ + [ + 'attribute' => 'parent_id', + 'operator' => 'eq', + 'type' => 'identifier', + '_value' => 'entity_id' + ] + ] + ] + ], + 'filter' => [ + [ + 'glue' => 'and', + 'condition' => [ + [ + 'attribute' => 'entity_id', + 'operator' => 'null' + ] + ] + ] + ] + ] + ] + ] + ], + [ + 'billing' => [ + 'link-type' => 'left', + 'table' => [ + 'billing' => 'pref_sales_order_address' + ], + 'condition' => '(billing.parent_id = `sales`.`entity_id`)' + ] + ], + ['sales_order_address' => 'pref_sales_order_address'] + ] + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php new file mode 100644 index 0000000000000..6f881fc4ae96a --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php @@ -0,0 +1,147 @@ +selectBuilderMock = $this->getMockBuilder(SelectBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + $this->columnsResolver = $objectManager->getObject( + ColumnsResolver::class, + [ + 'nameResolver' => new NameResolver(), + 'resourceConnection' => $this->resourceConnectionMock + ] + ); + } + + public function testGetColumnsWithoutAttributes() + { + $this->assertEquals($this->columnsResolver->getColumns($this->selectBuilderMock, []), []); + } + + /** + * @dataProvider getColumnsDataProvider + */ + public function testGetColumnsWithFunction($expectedColumns, $expectedGroup, $entityConfig) + { + $this->resourceConnectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->any()) + ->method('quoteIdentifier') + ->with('cpe.name') + ->willReturn('`cpe`.`name`'); + $this->selectBuilderMock->expects($this->once()) + ->method('getColumns') + ->willReturn([]); + $this->selectBuilderMock->expects($this->once()) + ->method('getGroup') + ->willReturn([]); + $this->selectBuilderMock->expects($this->once()) + ->method('setGroup') + ->with($expectedGroup); + $this->assertEquals( + $expectedColumns, + $this->columnsResolver->getColumns( + $this->selectBuilderMock, + $entityConfig + ) + ); + } + + /** + * @return array + */ + public function getColumnsDataProvider() + { + return [ + 'COUNT( DISTINCT `cpe`.`name`) AS name' => [ + 'expectedColumns' => [ + 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)') + ], + 'expectedGroup' => [ + 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)') + ], + 'entityConfig' => + [ + 'name' => 'catalog_product_entity', + 'alias' => 'cpe', + 'attribute' => [ + [ + 'name' => 'name', + 'function' => 'COUNT', + 'distinct' => true, + 'group' => true + ] + ], + ], + ], + 'AVG(`cpe`.`name`) AS avg_name' => [ + 'expectedColumns' => [ + 'avg_name' => new ColumnValueExpression('AVG(`cpe`.`name`)') + ], + 'expectedGroup' => [], + 'entityConfig' => + [ + 'name' => 'catalog_product_entity', + 'alias' => 'cpe', + 'attribute' => [ + [ + 'name' => 'name', + 'alias' => 'avg_name', + 'function' => 'AVG', + ] + ], + ], + ] + ]; + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php new file mode 100644 index 0000000000000..8fa9d3b538e71 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php @@ -0,0 +1,105 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectBuilderMock = $this->getMockBuilder(SelectBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->conditionResolver = new ConditionResolver($this->resourceConnectionMock); + } + + public function testGetFilter() + { + $condition = ["type" => "variable", "_value" => "1", "attribute" => "id", "operator" => "neq"]; + $valueCondition = ["type" => "value", "_value" => "2", "attribute" => "first_name", "operator" => "eq"]; + $identifierCondition = [ + "type" => "identifier", + "_value" => "other_field", + "attribute" => "last_name", + "operator" => "eq"]; + $filter = [["glue" => "AND", "condition" => [$valueCondition]]]; + $filterConfig = [ + ["glue" => "OR", "condition" => [$condition], 'filter' => $filter], + ["glue" => "OR", "condition" => [$identifierCondition]], + ]; + $aliasName = 'n'; + $this->selectBuilderMock->expects($this->any()) + ->method('setParams') + ->with(array_merge([], [$condition['_value']])); + + $this->selectBuilderMock->expects($this->once()) + ->method('getParams') + ->willReturn([]); + + $this->selectBuilderMock->expects($this->any()) + ->method('getColumns') + ->willReturn(['price' => new Expression("(n.price = 400)")]); + + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->any()) + ->method('quote') + ->willReturn("'John'"); + $this->connectionMock->expects($this->exactly(4)) + ->method('quoteIdentifier') + ->willReturnMap([ + ['n.id', false, '`n`.`id`'], + ['n.first_name', false, '`n`.`first_name`'], + ['n.last_name', false, '`n`.`last_name`'], + ['other_field', false, '`other_field`'], + ]); + + $result = "(`n`.`id` != 1 OR ((`n`.`first_name` = 'John'))) OR (`n`.`last_name` = `other_field`)"; + $this->assertEquals( + $result, + $this->conditionResolver->getFilter($this->selectBuilderMock, $filterConfig, $aliasName) + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php new file mode 100644 index 0000000000000..8109e6f9e8631 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php @@ -0,0 +1,87 @@ +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..d7dcf50620550 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php @@ -0,0 +1,122 @@ +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..8be2f0ee968ef --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php @@ -0,0 +1,100 @@ +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..bc20a62dcb366 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php @@ -0,0 +1,56 @@ +objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->iteratorIteratorMock = $this->getMockBuilder(\IteratorIterator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->iteratorFactory = new IteratorFactory( + $this->objectManagerMock + ); + } + + public function testCreate() + { + $arrayObject = new \ArrayIterator([1, 2, 3, 4, 5]); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with(\IteratorIterator::class, ['iterator' => $arrayObject]) + ->willReturn($this->iteratorIteratorMock); + + $this->assertEquals($this->iteratorFactory->create($arrayObject), $this->iteratorIteratorMock); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php new file mode 100644 index 0000000000000..9a3805a50f167 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php @@ -0,0 +1,239 @@ +queryMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\Query::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\Config::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectMock = $this->getMockBuilder( + \Magento\Framework\DB\Select::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->assemblerMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\Assembler\AssemblerInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->queryCacheMock = $this->getMockBuilder( + \Magento\Framework\App\CacheInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerMock = $this->getMockBuilder( + \Magento\Framework\ObjectManagerInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectHydratorMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\SelectHydrator::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectBuilderFactoryMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\SelectBuilderFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\ReportXml\QueryFactory::class, + [ + 'config' => $this->configMock, + 'selectBuilderFactory' => $this->selectBuilderFactoryMock, + 'assemblers' => [$this->assemblerMock], + 'queryCache' => $this->queryCacheMock, + 'objectManager' => $this->objectManagerMock, + 'selectHydrator' => $this->selectHydratorMock + ] + ); + } + + /** + * @return void + */ + public function testCreateCached() + { + $queryName = 'test_query'; + + $this->queryCacheMock->expects($this->any()) + ->method('load') + ->with($queryName) + ->willReturn('{"connectionName":"sales","config":{},"select_parts":{}}'); + + $this->selectHydratorMock->expects($this->any()) + ->method('recreate') + ->with([]) + ->willReturn($this->selectMock); + + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with( + \Magento\Analytics\ReportXml\Query::class, + [ + 'select' => $this->selectMock, + 'selectHydrator' => $this->selectHydratorMock, + 'connectionName' => 'sales', + 'config' => [] + ] + ) + ->willReturn($this->queryMock); + + $this->queryCacheMock->expects($this->never()) + ->method('save'); + + $this->assertEquals( + $this->queryMock, + $this->subject->create($queryName) + ); + } + + /** + * @return void + */ + public function testCreateNotCached() + { + $queryName = 'test_query'; + + $queryConfigMock = [ + 'name' => 'test_query', + 'connection' => 'sales' + ]; + + $selectBuilderMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\DB\SelectBuilder::class + ) + ->disableOriginalConstructor() + ->getMock(); + $selectBuilderMock->expects($this->once()) + ->method('setConnectionName') + ->with($queryConfigMock['connection']); + $selectBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($this->selectMock); + $selectBuilderMock->expects($this->any()) + ->method('getConnectionName') + ->willReturn($queryConfigMock['connection']); + + $this->queryCacheMock->expects($this->any()) + ->method('load') + ->with($queryName) + ->willReturn(null); + + $this->configMock->expects($this->any()) + ->method('get') + ->with($queryName) + ->willReturn($queryConfigMock); + + $this->selectBuilderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($selectBuilderMock); + + $this->assemblerMock->expects($this->once()) + ->method('assemble') + ->with($selectBuilderMock, $queryConfigMock) + ->willReturn($selectBuilderMock); + + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with( + \Magento\Analytics\ReportXml\Query::class, + [ + 'select' => $this->selectMock, + 'selectHydrator' => $this->selectHydratorMock, + 'connectionName' => $queryConfigMock['connection'], + 'config' => $queryConfigMock + ] + ) + ->willReturn($this->queryMock); + + $this->queryCacheMock->expects($this->once()) + ->method('save') + ->with(json_encode($this->queryMock), $queryName); + + $this->assertEquals( + $this->queryMock, + $this->subject->create($queryName) + ); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php new file mode 100644 index 0000000000000..947e07b569e04 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php @@ -0,0 +1,87 @@ +selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectHydratorMock = $this->getMockBuilder(selectHydrator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->query = $this->objectManagerHelper->getObject( + Query::class, + [ + 'select' => $this->selectMock, + 'connectionName' => $this->connectionName, + 'selectHydrator' => $this->selectHydratorMock, + 'config' => [] + ] + ); + } + + /** + * @return void + */ + public function testJsonSerialize() + { + $selectParts = ['part' => 1]; + + $this->selectHydratorMock + ->expects($this->once()) + ->method('extract') + ->with($this->selectMock) + ->willReturn($selectParts); + + $expectedResult = [ + 'connectionName' => $this->connectionName, + 'select_parts' => $selectParts, + 'config' => [] + ]; + + $this->assertSame($expectedResult, $this->query->jsonSerialize()); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php new file mode 100644 index 0000000000000..5f329993dd291 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php @@ -0,0 +1,180 @@ +selectMock = $this->getMockBuilder( + \Magento\Framework\DB\Select::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->queryMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\Query::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->queryMock->expects($this->any()) + ->method('getSelect') + ->willReturn($this->selectMock); + + $this->iteratorMock = $this->getMockBuilder( + \IteratorIterator::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->statementMock = $this->getMockBuilder( + \Magento\Framework\DB\Statement\Pdo\Mysql::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->statementMock->expects($this->any()) + ->method('getIterator') + ->willReturn($this->iteratorMock); + + $this->connectionMock = $this->getMockBuilder( + \Magento\Framework\DB\Adapter\AdapterInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->queryFactoryMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\QueryFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->iteratorFactoryMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\IteratorFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->iteratorMock = $this->getMockBuilder( + \IteratorIterator::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerHelper = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->connectionFactoryMock = $this->getMockBuilder( + \Magento\Analytics\ReportXml\ConnectionFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->subject = $this->objectManagerHelper->getObject( + \Magento\Analytics\ReportXml\ReportProvider::class, + [ + 'queryFactory' => $this->queryFactoryMock, + 'connectionFactory' => $this->connectionFactoryMock, + 'iteratorFactory' => $this->iteratorFactoryMock + ] + ); + } + + /** + * @return void + */ + public function testGetReport() + { + $reportName = 'test_report'; + $connectionName = 'sales'; + + $this->queryFactoryMock->expects($this->once()) + ->method('create') + ->with($reportName) + ->willReturn($this->queryMock); + + $this->connectionFactoryMock->expects($this->once()) + ->method('getConnection') + ->with($connectionName) + ->willReturn($this->connectionMock); + + $this->queryMock->expects($this->once()) + ->method('getConnectionName') + ->willReturn($connectionName); + + $this->queryMock->expects($this->once()) + ->method('getConfig') + ->willReturn( + [ + 'connection' => $connectionName + ] + ); + + $this->connectionMock->expects($this->once()) + ->method('query') + ->with($this->selectMock) + ->willReturn($this->statementMock); + + $this->iteratorFactoryMock->expects($this->once()) + ->method('create') + ->with($this->statementMock, null) + ->willReturn($this->iteratorMock); + $this->assertEquals($this->iteratorMock, $this->subject->getReport($reportName)); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php new file mode 100644 index 0000000000000..058c74f341eb8 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php @@ -0,0 +1,254 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->selectHydrator = $this->objectManagerHelper->getObject( + SelectHydrator::class, + [ + 'resourceConnection' => $this->resourceConnectionMock, + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + public function testExtract() + { + $selectParts = + [ + Select::DISTINCT, + Select::COLUMNS, + Select::UNION, + Select::FROM, + Select::WHERE, + Select::GROUP, + Select::HAVING, + Select::ORDER, + Select::LIMIT_COUNT, + Select::LIMIT_OFFSET, + Select::FOR_UPDATE + ]; + + $result = []; + foreach ($selectParts as $part) { + $result[$part] = "Part"; + } + $this->selectMock->expects($this->any()) + ->method('getPart') + ->willReturn("Part"); + $this->assertEquals($this->selectHydrator->extract($this->selectMock), $result); + } + + /** + * @dataProvider recreateWithoutExpressionDataProvider + * @param array $selectParts + * @param array $parts + * @param array $partValues + */ + public function testRecreateWithoutExpression($selectParts, $parts, $partValues) + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->selectMock); + foreach ($parts as $key => $part) { + $this->selectMock->expects($this->at($key)) + ->method('setPart') + ->with($part, $partValues[$key]); + } + + $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts)); + } + + /** + * @return array + */ + public function recreateWithoutExpressionDataProvider() + { + return [ + 'Select without expressions' => [ + [ + Select::COLUMNS => [ + [ + 'table_name', + 'field_name', + 'alias', + ], + [ + 'table_name', + 'field_name_2', + 'alias_2', + ], + ] + ], + [Select::COLUMNS], + [[ + [ + 'table_name', + 'field_name', + 'alias', + ], + [ + 'table_name', + 'field_name_2', + 'alias_2', + ], + ]], + ], + ]; + } + + /** + * @dataProvider recreateWithExpressionDataProvider + * @param array $selectParts + * @param array $expectedParts + * @param \PHPUnit_Framework_MockObject_MockObject[] $expressionMocks + */ + public function testRecreateWithExpression( + array $selectParts, + array $expectedParts, + array $expressionMocks + ) { + $this->objectManagerMock + ->expects($this->exactly(count($expressionMocks))) + ->method('create') + ->with($this->isType('string'), $this->isType('array')) + ->willReturnOnConsecutiveCalls(...$expressionMocks); + $this->resourceConnectionMock + ->expects($this->once()) + ->method('getConnection') + ->with() + ->willReturn($this->connectionMock); + $this->connectionMock + ->expects($this->once()) + ->method('select') + ->with() + ->willReturn($this->selectMock); + foreach (array_keys($selectParts) as $key => $partName) { + $this->selectMock + ->expects($this->at($key)) + ->method('setPart') + ->with($partName, $expectedParts[$partName]); + } + + $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts)); + } + + /** + * @return array + */ + public function recreateWithExpressionDataProvider() + { + $expressionMock = $this->getMockBuilder(JsonSerializableExpression::class) + ->disableOriginalConstructor() + ->getMock(); + + return [ + 'Select without expressions' => [ + 'Parts' => [ + Select::COLUMNS => [ + [ + 'table_name', + 'field_name', + 'alias', + ], + [ + 'table_name', + [ + 'class' => 'Some_class', + 'arguments' => [ + 'expression' => ['some(expression)'] + ] + ], + 'alias_2', + ], + ] + ], + 'expectedParts' => [ + Select::COLUMNS => [ + [ + 'table_name', + 'field_name', + 'alias', + ], + [ + 'table_name', + $expressionMock, + 'alias_2', + ], + ] + ], + 'expectedExpressions' => [ + $expressionMock + ] + ], + ]; + } +} diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json new file mode 100644 index 0000000000000..88127f3c62a92 --- /dev/null +++ b/app/code/Magento/Analytics/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-analytics", + "description": "N/A", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-integration": "*", + "magento/module-store": "*", + "magento/framework": "*" + }, + "type": "magento2-module", + "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..bf2251895f929 --- /dev/null +++ b/app/code/Magento/Analytics/etc/acl.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Analytics/etc/adminhtml/di.xml b/app/code/Magento/Analytics/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..5e305e70e5ad3 --- /dev/null +++ b/app/code/Magento/Analytics/etc/adminhtml/di.xml @@ -0,0 +1,16 @@ + + + + + + + Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription + + + + diff --git a/app/code/Magento/Analytics/etc/adminhtml/menu.xml b/app/code/Magento/Analytics/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..915211c4bb85e --- /dev/null +++ b/app/code/Magento/Analytics/etc/adminhtml/menu.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/app/code/Magento/Analytics/etc/adminhtml/routes.xml b/app/code/Magento/Analytics/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..0ae2762dacc5f --- /dev/null +++ b/app/code/Magento/Analytics/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..4e21648d00ce8 --- /dev/null +++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml @@ -0,0 +1,50 @@ + + + + +
+ + general + Magento_Analytics::analytics_settings + + + For more information, see our + terms and conditions.]]> + + + Magento\Config\Model\Config\Source\Enabledisable + Magento\Analytics\Model\Config\Backend\Enabled + Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel + analytics/subscription/enabled + + + + Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel + Magento\Analytics\Model\Config\Backend\CollectionTime + + + Industry Data + + In order to personalize your Advanced Reporting experience, please select your industry. + Magento\Analytics\Model\Config\Source\Vertical + Magento\Analytics\Model\Config\Backend\Vertical + Magento\Analytics\Block\Adminhtml\System\Config\Vertical + + + + Learn more about Magento BI Essentials and BI Pro tiers.]]> + Magento\Analytics\Block\Adminhtml\System\Config\AdditionalComment + + +
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xml b/app/code/Magento/Analytics/etc/analytics.xml new file mode 100644 index 0000000000000..77ebe751a31cf --- /dev/null +++ b/app/code/Magento/Analytics/etc/analytics.xml @@ -0,0 +1,50 @@ + + + + + + + + modules + + + + + + + + + + + + + + stores + + + + + + + + + websites + + + + + + + + + groups + + + + + diff --git a/app/code/Magento/Analytics/etc/analytics.xsd b/app/code/Magento/Analytics/etc/analytics.xsd new file mode 100644 index 0000000000000..2506e3d6a6a9a --- /dev/null +++ b/app/code/Magento/Analytics/etc/analytics.xsd @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File name attribute can has only [a-zA-Z0-9/_]. + + + + + + + + + + Value is required. + + + + + + + diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml new file mode 100644 index 0000000000000..b6194ba12993f --- /dev/null +++ b/app/code/Magento/Analytics/etc/config.xml @@ -0,0 +1,25 @@ + + + + + + + https://advancedreporting.rjmetrics.com/signup + https://advancedreporting.rjmetrics.com/update + https://dashboard.rjmetrics.com/v2/magento/signup + https://advancedreporting.rjmetrics.com/otp + https://advancedreporting.rjmetrics.com/report + https://advancedreporting.rjmetrics.com/report + + Magento Analytics user + + 02,00,00 + + + + diff --git a/app/code/Magento/Analytics/etc/crontab.xml b/app/code/Magento/Analytics/etc/crontab.xml new file mode 100644 index 0000000000000..a4beef0359540 --- /dev/null +++ b/app/code/Magento/Analytics/etc/crontab.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml new file mode 100644 index 0000000000000..b9bb9cc9ff00c --- /dev/null +++ b/app/code/Magento/Analytics/etc/di.xml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + Magento\Analytics\Model\Connector\SignUpCommand + Magento\Analytics\Model\Connector\UpdateCommand + Magento\Analytics\Model\Connector\NotifyDataChangedCommand + + + + + + + Magento\Analytics\ReportXml\Config\Data + + + + + Magento\Analytics\ReportXml\Config\Reader + Magento_Analytics_ReportXml_CacheId + + + + + urn:magento:module:Magento_Analytics:etc/reports.xsd + + + + + Magento\Analytics\ReportXml\Config\Converter\Xml + Magento\Analytics\ReportXml\Config\SchemaLocator + reports.xml + + name + + name + alias + + name + name + + glue + + attribute + operator + + + glue + + attribute + operator + + + glue + + attribute + operator + + glue + + attribute + operator + + + + + + + + Magento\Analytics\ReportXml\Config\Reader\Xml + + + + + + + Magento\Analytics\Model\Config\Data + + + + + Magento\Analytics\Model\Config\Reader + Magento_Analytics_CacheId + + + + + urn:magento:module:Magento_Analytics:etc/analytics.xsd + + + + + Magento\Analytics\ReportXml\Config\Converter\Xml + Magento\Analytics\Model\Config\SchemaLocator + analytics.xml + + name + + + + + + + + Magento\Analytics\ReportXml\DB\Assembler\FromAssembler + Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler + Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler + + + + + + + Magento\Analytics\Model\Config\Reader\Xml + + + + + + + web/unsecure/base_url + currency/options/base + general/locale/timezone + general/country/default + carriers/dhl/title + carriers/dhl/active + carriers/fedex/title + carriers/fedex/active + carriers/flatrate/title + carriers/flatrate/active + carriers/tablerate/title + carriers/tablerate/active + carriers/freeshipping/title + carriers/freeshipping/active + carriers/ups/title + carriers/ups/active + carriers/usps/title + carriers/usps/active + payment/free/title + payment/free/active + payment/checkmo/title + payment/checkmo/active + payment/purchaseorder/title + payment/purchaseorder/active + payment/banktransfer/title + payment/banktransfer/active + payment/cashondelivery/title + payment/cashondelivery/active + payment/authorizenet_directpost/title + payment/authorizenet_directpost/active + payment/paypal_billing_agreement/title + payment/paypal_billing_agreement/active + payment/braintree/title + payment/braintree/active + payment/braintree_paypal/title + payment/braintree_paypal/active + analytics/general/vertical + + + + + + + Apps and Games + Athletic/Sporting Goods + Art and Design + Auto Parts + Baby/Children’s Apparel, Gear and Toys + Beauty and Cosmetics + Books, Music and Magazines + Crafts and Stationery + Consumer Electronics + Deal Site + Fashion Apparel and Accessories + Food, Beverage and Grocery + Home Goods and Furniture + Home Improvement + Jewelry and Watches + Mass Merchant + Office Supplies + Outdoor and Camping Gear + Pet Goods + Pharma and Medical Devices + Technology B2B + Other + + + + + + + + + + \Magento\Analytics\Model\Connector\ResponseHandler\SignUp + + + + + + + Magento\Analytics\Model\Connector\ResponseHandler\Update + Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp + + + + + + + Magento\Analytics\Model\Connector\ResponseHandler\OTP + Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp + + + + + + + Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp + + + + + + SignUpResponseResolver + + + + + UpdateResponseResolver + + + + + OtpResponseResolver + + + + + NotifyDataChangedResponseResolver + + + + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + 1 + 1 + 1 + + + + diff --git a/app/code/Magento/Analytics/etc/module.xml b/app/code/Magento/Analytics/etc/module.xml new file mode 100644 index 0000000000000..24c2fbc81446e --- /dev/null +++ b/app/code/Magento/Analytics/etc/module.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/Analytics/etc/reports.xml b/app/code/Magento/Analytics/etc/reports.xml new file mode 100644 index 0000000000000..8a43658670293 --- /dev/null +++ b/app/code/Magento/Analytics/etc/reports.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Analytics/etc/reports.xsd b/app/code/Magento/Analytics/etc/reports.xsd new file mode 100644 index 0000000000000..d0ba4068244fe --- /dev/null +++ b/app/code/Magento/Analytics/etc/reports.xsd @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Analytics/etc/webapi.xml b/app/code/Magento/Analytics/etc/webapi.xml new file mode 100644 index 0000000000000..8252d039f1d03 --- /dev/null +++ b/app/code/Magento/Analytics/etc/webapi.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/Analytics/i18n/en_US.csv b/app/code/Magento/Analytics/i18n/en_US.csv new file mode 100644 index 0000000000000..4faa48fb73709 --- /dev/null +++ b/app/code/Magento/Analytics/i18n/en_US.csv @@ -0,0 +1,84 @@ +"Subscription status","Subscription status" +"Sorry, there has been an error processing your request. Please try again later.","Sorry, there has been an error processing your request. Please try again later." +"Sorry, there was an error processing your registration request to Magento Analytics. Please try again later.","Sorry, there was an error processing your registration request to Magento Analytics. Please try again later." +"Error occurred during postponement notification","Error occurred during postponement notification" +"Time value has an unsupported format","Time value has an unsupported format" +"Cron settings can't be saved","Cron settings can't be saved" +"There was an error save new configuration value.","There was an error save new configuration value." +"Please select an industry.","Please select an industry." +"--Please Select--","--Please Select--" +"Command was not found.","Command was not found." +"Input data must be string or convertible into string.","Input data must be string or convertible into string." +"Input data must be non-empty string.","Input data must be non-empty string." +"Not valid cipher method.","Not valid cipher method." +"Encryption key can't be empty.","Encryption key can't be empty." +"Source ""%1"" is not exist","Source ""%1"" is not exist" +"These arguments can't be empty ""%1""","These arguments can't be empty ""%1""" +"Cannot find predefined integration user!","Cannot find predefined integration user!" +"File is not ready yet.","File is not ready yet." +"Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later.","Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later." +"Failed to synchronize data to the Magento Business Intelligence service. ","Failed to synchronize data to the Magento Business Intelligence service. " +"Retry Synchronization","Retry Synchronization" +TestMessage,TestMessage +"Error message","Error message" +"Apps and Games","Apps and Games" +"Athletic/Sporting Goods","Athletic/Sporting Goods" +"Art and Design","Art and Design" +"Advanced Reporting","Advanced Reporting" +"Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data.","Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data." +"View details","View details" +"Go to Advanced Reporting","Go to Advanced Reporting" +"An error occurred while subscription process.","An error occurred while subscription process." +Analytics,Analytics +API,API +Configuration,Configuration +"Business Intelligence","Business Intelligence" +"BI Essentials","BI Essentials" +"This service provides a dynamic suite of reports with rich insights about your business. + Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the + ""Go to Advanced Reporting"" link.
For more information, see our + terms and conditions. + ","This service provides a dynamic suite of reports with rich insights about your business. + Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the + ""Go to Advanced Reporting"" link.
For more information, see our + terms and conditions." +"Advanced Reporting Service","Advanced Reporting Service" +Industry,Industry +"Time of day to send data","Time of day to send data" +"Get more insights from Magento Business Intelligence","Get more insights from Magento Business Intelligence" +"Magento Business Intelligence provides you with a simple and clear path to + becoming more data driven.
Learn more about BI Essentials tier.","Magento Business Intelligence provides you with a simple and clear path to + becoming more data driven.
Learn more about BI Essentials tier." +"Auto Parts","Auto Parts" +"Baby/Children’s Apparel, Gear and Toys","Baby/Children’s Apparel, Gear and Toys" +"Beauty and Cosmetics","Beauty and Cosmetics" +"Books, Music and Magazines","Books, Music and Magazines" +"Crafts and Stationery","Crafts and Stationery" +"Consumer Electronics","Consumer Electronics" +"Deal Site","Deal Site" +"Fashion Apparel and Accessories","Fashion Apparel and Accessories" +"Food, Beverage and Grocery","Food, Beverage and Grocery" +"Home Goods and Furniture","Home Goods and Furniture" +"Home Improvement","Home Improvement" +"Jewelry and Watches","Jewelry and Watches" +"Mass Merchant","Mass Merchant" +"Office Supplies","Office Supplies" +"Outdoor and Camping Gear","Outdoor and Camping Gear" +"Pet Goods","Pet Goods" +"Pharma and Medical Devices","Pharma and Medical Devices" +"Technology B2B","Technology B2B" +"Analytics Subscription","Analytics Subscription" +"powered by Magento Business Intelligence","powered by Magento Business Intelligence" +"Are you sure you want to opt out?","Are you sure you want to opt out?" +Cancel,Cancel +"Opt out","Opt out" +"

Advanced Reporting in included, + free of charge, in your Magento software. When you opt out, we collect no product, order, and + customer data to generate our dynamic reports.

To opt in later: You can always turn on Advanced + Reporting in you Admin Panel.

","

Advanced Reporting in included, + free of charge, in your Magento software. When you opt out, we collect no product, order, and + customer data to generate our dynamic reports.

To opt in later: You can always turn on Advanced + Reporting in you Admin Panel.

" +"In order to personalize your Advanced Reporting experience, please select your industry.","In order to personalize your Advanced Reporting experience, please select your industry." diff --git a/app/code/Magento/Analytics/registration.php b/app/code/Magento/Analytics/registration.php new file mode 100644 index 0000000000000..58d3688b7491d --- /dev/null +++ b/app/code/Magento/Analytics/registration.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml new file mode 100644 index 0000000000000..a22c603b2a8b3 --- /dev/null +++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml @@ -0,0 +1,28 @@ + + +
+
+
+ escapeHtml(__('Advanced Reporting')) ?> +
+
+ escapeHtml(__('Gain new insights and take command of your business\' performance,' . + ' using our dynamic product, order, and customer reports tailored to your customer data.')) ?> +
+
+ +
diff --git a/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php new file mode 100644 index 0000000000000..88db2d6d80141 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php @@ -0,0 +1,36 @@ +urlBuilder = $urlBuilder; + } + + /** + * Retrieve button data + * + * @return array button configuration + */ + public function getButtonData() + { + return [ + 'label' => __('Back'), + 'on_click' => sprintf("location.href = '%s';", $this->urlBuilder->getUrl('*/')), + 'class' => 'back', + 'sort_order' => 10 + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/DoneButton.php b/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/DoneButton.php new file mode 100644 index 0000000000000..5e30c20fd2fbf --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/DoneButton.php @@ -0,0 +1,75 @@ +bulkStatus = $bulkStatus; + $this->request = $request; + } + + /** + * Retrieve button data + * + * @return array button configuration + */ + public function getButtonData() + { + $uuid = $this->request->getParam('uuid'); + $operationsCount = $this->bulkStatus->getOperationsCountByBulkIdAndStatus( + $uuid, + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + ); + $button = []; + + if ($this->request->getParam('buttons') && $operationsCount === 0) { + $button = [ + 'label' => __('Done'), + 'class' => 'primary', + 'sort_order' => 10, + 'on_click' => '', + 'data_attribute' => [ + 'mage-init' => [ + 'Magento_Ui/js/form/button-adapter' => [ + 'actions' => [ + [ + 'targetName' => 'notification_area.notification_area.modalContainer.modal', + 'actionName' => 'closeModal' + ], + ], + ], + ], + ], + ]; + } + + return $button; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/RetryButton.php b/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/RetryButton.php new file mode 100644 index 0000000000000..9051f1ab9d428 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/RetryButton.php @@ -0,0 +1,66 @@ +details = $details; + $this->request = $request; + $this->targetName = $targetName; + } + + /** + * {@inheritdoc} + */ + public function getButtonData() + { + $uuid = $this->request->getParam('uuid'); + $details = $this->details->getDetails($uuid); + if ($details['failed_retriable'] === 0) { + return []; + } + return [ + 'label' => __('Retry'), + 'class' => 'retry primary', + 'data_attribute' => [ + 'mage-init' => ['button' => ['event' => 'save']], + 'form-role' => 'save', + ], + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php new file mode 100644 index 0000000000000..9e9dbd3dd67c5 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php @@ -0,0 +1,71 @@ +resultPageFactory = $resultPageFactory; + $this->accessValidator = $accessValidator; + $this->menuId = $menuId; + parent::__construct($context); + } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Magento_Logging::system_magento_logging_bulk_operations') + && $this->accessValidator->isAllowed($this->getRequest()->getParam('uuid')); + } + + /** + * Bulk details action + * + * @return \Magento\Framework\View\Result\Page + */ + public function execute() + { + $bulkId = $this->getRequest()->getParam('uuid'); + $resultPage = $this->resultPageFactory->create(); + $resultPage->initLayout(); + $this->_setActiveMenu($this->menuId); + $resultPage->getConfig()->getTitle()->prepend(__('Action Details - #' . $bulkId)); + + return $resultPage; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Retry.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Retry.php new file mode 100644 index 0000000000000..62e6b9ba4551b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Retry.php @@ -0,0 +1,99 @@ +bulkManagement = $bulkManagement; + $this->notificationManagement = $notificationManagement; + $this->accessValidator = $accessValidator; + } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Magento_Logging::system_magento_logging_bulk_operations') + && $this->accessValidator->isAllowed($this->getRequest()->getParam('uuid')); + } + + /** + * {@inheritdoc} + */ + public function execute() + { + $bulkUuid = $this->getRequest()->getParam('uuid'); + $isAjax = $this->getRequest()->getParam('isAjax'); + $operationsToRetry = (array)$this->getRequest()->getParam('operations_to_retry', []); + $errorCodes = []; + foreach ($operationsToRetry as $operationData) { + if (isset($operationData['error_code'])) { + $errorCodes[] = (int)$operationData['error_code']; + } + } + + $affectedOperations = $this->bulkManagement->retryBulk($bulkUuid, $errorCodes); + $this->notificationManagement->ignoreBulks([$bulkUuid]); + if (!$isAjax) { + $this->messageManager->addSuccessMessage( + __('%1 item(s) have been scheduled for update."', $affectedOperations) + ); + /** @var Redirect $result */ + $result = $this->resultRedirectFactory->create(); + $result->setPath('bulk/index'); + } else { + /** @var \Magento\Framework\Controller\Result\Json $result */ + $result = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $result->setHttpResponseCode(200); + $response = new \Magento\Framework\DataObject(); + $response->setError(0); + + $result->setData($response); + } + return $result; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Index/Index.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Index/Index.php new file mode 100644 index 0000000000000..5a2b9c0a34e64 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Index/Index.php @@ -0,0 +1,65 @@ +resultPageFactory = $resultPageFactory; + $this->menuId = $menuId; + parent::__construct($context); + } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return parent::_isAllowed(); + } + + /** + * Bulk list action + * + * @return \Magento\Framework\View\Result\Page + */ + public function execute() + { + $resultPage = $this->resultPageFactory->create(); + $resultPage->initLayout(); + $this->_setActiveMenu($this->menuId); + $resultPage->getConfig()->getTitle()->prepend(__('Bulk Actions Log')); + return $resultPage; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php new file mode 100644 index 0000000000000..0a71c130fb20a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php @@ -0,0 +1,65 @@ +notificationManagement = $notificationManagement; + } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Magento_Logging::system_magento_logging_bulk_operations'); + } + + /** + * {@inheritdoc} + */ + public function execute() + { + $bulkUuids = []; + foreach ((array)$this->getRequest()->getParam('uuid', []) as $bulkUuid) { + $bulkUuids[] = (string)$bulkUuid; + } + + $isAcknowledged = $this->notificationManagement->acknowledgeBulks($bulkUuids); + + /** @var \Magento\Framework\Controller\Result\Json $result */ + $result = $this->resultFactory->create(ResultFactory::TYPE_JSON); + if (!$isAcknowledged) { + $result->setHttpResponseCode(400); + } + + return $result; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Cron/BulkCleanup.php b/app/code/Magento/AsynchronousOperations/Cron/BulkCleanup.php new file mode 100644 index 0000000000000..7c8da3c1c4236 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Cron/BulkCleanup.php @@ -0,0 +1,77 @@ +metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->dateTime = $dateTime; + $this->scopeConfig = $scopeConfig; + $this->date = $time; + } + + /** + * Remove all expired bulks and corresponding operations + * + * @return void + */ + public function execute() + { + $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + + $bulkLifetime = 3600 * 24 * (int)$this->scopeConfig->getValue('system/bulk/lifetime'); + $maxBulkStartTime = $this->dateTime->formatDate($this->date->gmtTimestamp() - $bulkLifetime); + $connection->delete($metadata->getEntityTable(), ['start_time <= ?' => $maxBulkStartTime]); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php new file mode 100644 index 0000000000000..a14ec254cf897 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php @@ -0,0 +1,60 @@ +userContext = $userContext; + $this->entityManager = $entityManager; + $this->bulkSummaryFactory = $bulkSummaryFactory; + } + + /** + * Check if content allowed for current user + * + * @param int $bulkUuid + * @return bool + */ + public function isAllowed($bulkUuid) + { + /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */ + $bulkSummary = $this->entityManager->load( + $this->bulkSummaryFactory->create(), + $bulkUuid + ); + return $bulkSummary->getUserId() === $this->userContext->getUserId(); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/AsyncResponse.php b/app/code/Magento/AsynchronousOperations/Model/AsyncResponse.php new file mode 100644 index 0000000000000..02a2e8de1fa64 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/AsyncResponse.php @@ -0,0 +1,81 @@ +getData(self::BULK_UUID); + } + + /** + * @inheritDoc + */ + public function setBulkUuid($bulkUuid) + { + return $this->setData(self::BULK_UUID, $bulkUuid); + } + + /** + * @inheritDoc + */ + public function getRequestItems() + { + return $this->getData(self::REQUEST_ITEMS); + } + + /** + * @inheritDoc + */ + public function setRequestItems($requestItems) + { + return $this->setData(self::REQUEST_ITEMS, $requestItems); + } + + /** + * @inheritdoc + */ + public function setErrors($isErrors = false) + { + return $this->setData(self::ERRORS, $isErrors); + } + + /** + * @inheritdoc + */ + public function isErrors() + { + return $this->getData(self::ERRORS); + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes() + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes( + \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes + ) { + return $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkDescription/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkDescription/Options.php new file mode 100644 index 0000000000000..08e1a863b259d --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkDescription/Options.php @@ -0,0 +1,64 @@ +bulkCollectionFactory = $bulkCollection; + $this->userContext = $userContext; + } + + /** + * {@inheritdoc} + */ + public function toOptionArray() + { + /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\Collection $collection */ + $collection = $this->bulkCollectionFactory->create(); + + /** @var \Magento\Framework\DB\Select $select */ + $select = $collection->getSelect(); + $select->reset(); + $select->distinct(true); + $select->from($collection->getMainTable(), ['description']); + $select->where('user_id = ?', $this->userContext->getUserId()); + + $options = []; + + /** @var BulkSummaryInterface $item */ + foreach ($collection->getItems() as $item) { + $options[] = [ + 'value' => $item->getDescription(), + 'label' => $item->getDescription() + ]; + } + return $options; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php new file mode 100644 index 0000000000000..4f086ce8ac2ca --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php @@ -0,0 +1,205 @@ +entityManager = $entityManager; + $this->bulkSummaryFactory= $bulkSummaryFactory; + $this->operationCollectionFactory = $operationCollectionFactory; + $this->metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->publisher = $publisher; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function scheduleBulk($bulkUuid, array $operations, $description, $userId = null) + { + $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + // save bulk summary and related operations + $connection->beginTransaction(); + try { + /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */ + $bulkSummary = $this->bulkSummaryFactory->create(); + $this->entityManager->load($bulkSummary, $bulkUuid); + $bulkSummary->setBulkId($bulkUuid); + $bulkSummary->setDescription($description); + $bulkSummary->setUserId($userId); + $bulkSummary->setOperationCount((int)$bulkSummary->getOperationCount() + count($operations)); + + $this->entityManager->save($bulkSummary); + + $connection->commit(); + } catch (\Exception $exception) { + $connection->rollBack(); + $this->logger->critical($exception->getMessage()); + return false; + } + $this->publishOperations($operations); + + return true; + } + + /** + * Retry bulk operations that failed due to given errors. + * + * @param string $bulkUuid target bulk UUID + * @param array $errorCodes list of corresponding error codes + * @return int number of affected bulk operations + */ + public function retryBulk($bulkUuid, array $errorCodes) + { + $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + + /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation[] $retriablyFailedOperations */ + $retriablyFailedOperations = $this->operationCollectionFactory->create() + ->addFieldToFilter('error_code', ['in' => $errorCodes]) + ->addFieldToFilter('bulk_uuid', ['eq' => $bulkUuid]) + ->getItems(); + + // remove corresponding operations from database (i.e. move them to 'open' status) + $connection->beginTransaction(); + try { + $operationIds = []; + $currentBatchSize = 0; + $maxBatchSize = 10000; + /** @var OperationInterface $operation */ + foreach ($retriablyFailedOperations as $operation) { + if ($currentBatchSize === $maxBatchSize) { + $connection->delete( + $this->resourceConnection->getTableName('magento_operation'), + $connection->quoteInto('id IN (?)', $operationIds) + ); + $operationIds = []; + $currentBatchSize = 0; + } + $currentBatchSize++; + $operationIds[] = $operation->getId(); + // Rescheduled operations must be put in queue in 'open' state (i.e. without ID) + $operation->setId(null); + } + // remove operations from the last batch + if (!empty($operationIds)) { + $connection->delete( + $this->resourceConnection->getTableName('magento_operation'), + $connection->quoteInto('id IN (?)', $operationIds) + ); + } + + $connection->commit(); + } catch (\Exception $exception) { + $connection->rollBack(); + $this->logger->critical($exception->getMessage()); + return 0; + } + $this->publishOperations($retriablyFailedOperations); + + return count($retriablyFailedOperations); + } + + /** + * Publish list of operations to the corresponding message queues. + * + * @param array $operations + * @return void + */ + private function publishOperations(array $operations) + { + $operationsByTopics = []; + foreach ($operations as $operation) { + $operationsByTopics[$operation->getTopicName()][] = $operation; + } + foreach ($operationsByTopics as $topicName => $operations) { + $this->publisher->publish($topicName, $operations); + } + } + + /** + * @inheritDoc + */ + public function deleteBulk($bulkId) + { + return $this->entityManager->delete( + $this->entityManager->load( + $this->bulkSummaryFactory->create(), + $bulkId + ) + ); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkNotificationManagement.php b/app/code/Magento/AsynchronousOperations/Model/BulkNotificationManagement.php new file mode 100644 index 0000000000000..2ba7f7fe5e3ee --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkNotificationManagement.php @@ -0,0 +1,150 @@ +metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->bulkCollectionFactory = $bulkCollectionFactory; + $this->logger = $logger; + } + + /** + * Mark given bulks as acknowledged. + * Notifications related to these bulks will not appear in notification area. + * + * @param array $bulkUuids + * @return bool true on success or false on failure + */ + public function acknowledgeBulks(array $bulkUuids) + { + $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + + try { + $connection->insertArray( + $this->resourceConnection->getTableName('magento_acknowledged_bulk'), + ['bulk_uuid'], + $bulkUuids + ); + } catch (\Exception $exception) { + $this->logger->critical($exception->getMessage()); + return false; + } + return true; + } + + /** + * Remove given bulks from acknowledged list. + * Notifications related to these bulks will appear again in notification area. + * + * @param array $bulkUuids + * @return bool true on success or false on failure + */ + public function ignoreBulks(array $bulkUuids) + { + $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + + try { + $connection->delete( + $this->resourceConnection->getTableName('magento_acknowledged_bulk'), + ['bulk_uuid IN(?)' => $bulkUuids] + ); + } catch (\Exception $exception) { + $this->logger->critical($exception->getMessage()); + return false; + } + return true; + } + + /** + * Retrieve all bulks that were acknowledged by given user. + * + * @param int $userId + * @return BulkSummaryInterface[] + */ + public function getAcknowledgedBulksByUser($userId) + { + $bulks = $this->bulkCollectionFactory->create() + ->join( + ['acknowledged_bulk' => $this->resourceConnection->getTableName('magento_acknowledged_bulk')], + 'main_table.uuid = acknowledged_bulk.bulk_uuid', + [] + )->addFieldToFilter('user_id', $userId) + ->addOrder('start_time', Collection::SORT_ORDER_DESC) + ->getItems(); + + return $bulks; + } + + /** + * Retrieve all bulks that were not acknowledged by given user. + * + * @param int $userId + * @return BulkSummaryInterface[] + */ + public function getIgnoredBulksByUser($userId) + { + /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\Collection $bulkCollection */ + $bulkCollection = $this->bulkCollectionFactory->create(); + $bulkCollection->getSelect()->joinLeft( + ['acknowledged_bulk' => $this->resourceConnection->getTableName('magento_acknowledged_bulk')], + 'main_table.uuid = acknowledged_bulk.bulk_uuid', + ['acknowledged_bulk.bulk_uuid'] + ); + $bulks = $bulkCollection->addFieldToFilter('user_id', $userId) + ->addFieldToFilter('acknowledged_bulk.bulk_uuid', ['null' => true]) + ->addOrder('start_time', Collection::SORT_ORDER_DESC) + ->getItems(); + + return $bulks; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkOperationsStatus.php b/app/code/Magento/AsynchronousOperations/Model/BulkOperationsStatus.php new file mode 100644 index 0000000000000..5fc164ec833d8 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkOperationsStatus.php @@ -0,0 +1,150 @@ +operationCollectionFactory = $operationCollection; + $this->bulkStatus = $bulkStatus; + $this->bulkDetailedFactory = $bulkDetailedFactory; + $this->bulkShortFactory = $bulkShortFactory; + $this->entityManager = $entityManager; + } + + /** + * @inheritDoc + */ + public function getFailedOperationsByBulkId($bulkUuid, $failureType = null) + { + return $this->bulkStatus->getFailedOperationsByBulkId($bulkUuid, $failureType); + } + + /** + * @inheritDoc + */ + public function getOperationsCountByBulkIdAndStatus($bulkUuid, $status) + { + return $this->bulkStatus->getOperationsCountByBulkIdAndStatus($bulkUuid, $status); + } + + /** + * @inheritDoc + */ + public function getBulksByUser($userId) + { + return $this->bulkStatus->getBulksByUser($userId); + } + + /** + * @inheritDoc + */ + public function getBulkStatus($bulkUuid) + { + return $this->bulkStatus->getBulkStatus($bulkUuid); + } + + /** + * @inheritDoc + */ + public function getBulkDetailedStatus($bulkUuid) + { + $bulkSummary = $this->bulkDetailedFactory->create(); + + /** @var \Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterface $bulk */ + $bulk = $this->entityManager->load($bulkSummary, $bulkUuid); + + if ($bulk->getBulkId() === null) { + throw new NoSuchEntityException( + __( + 'Bulk uuid %bulkUuid not exist', + ['bulkUuid' => $bulkUuid] + ) + ); + } + $operations = $this->operationCollectionFactory->create()->addFieldToFilter('bulk_uuid', $bulkUuid)->getItems(); + $bulk->setOperationsList($operations); + + return $bulk; + } + + /** + * @inheritDoc + */ + public function getBulkShortStatus($bulkUuid) + { + $bulkSummary = $this->bulkShortFactory->create(); + + /** @var \Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterface $bulk */ + $bulk = $this->entityManager->load($bulkSummary, $bulkUuid); + if ($bulk->getBulkId() === null) { + throw new NoSuchEntityException( + __( + 'Bulk uuid %bulkUuid not exist', + ['bulkUuid' => $bulkUuid] + ) + ); + } + $operations = $this->operationCollectionFactory->create()->addFieldToFilter('bulk_uuid', $bulkUuid)->getItems(); + $bulk->setOperationsList($operations); + + return $bulk; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php new file mode 100644 index 0000000000000..c37ae0d23dd25 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php @@ -0,0 +1,200 @@ +operationCollectionFactory = $operationCollection; + $this->bulkCollectionFactory = $bulkCollection; + $this->resourceConnection = $resourceConnection; + $this->calculatedStatusSql = $calculatedStatusSql; + $this->metadataPool = $metadataPool; + } + + /** + * @inheritDoc + */ + public function getFailedOperationsByBulkId($bulkUuid, $failureType = null) + { + $failureCodes = $failureType + ? [$failureType] + : [ + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED + ]; + $operations = $this->operationCollectionFactory->create() + ->addFieldToFilter('bulk_uuid', $bulkUuid) + ->addFieldToFilter('status', $failureCodes) + ->getItems(); + return $operations; + } + + /** + * @inheritDoc + */ + public function getOperationsCountByBulkIdAndStatus($bulkUuid, $status) + { + if ($status === OperationInterface::STATUS_TYPE_OPEN) { + /** + * Total number of operations that has been scheduled within the given bulk + */ + $allOperationsQty = $this->getOperationCount($bulkUuid); + + /** + * Number of operations that has been processed (i.e. operations with any status but 'open') + */ + $allProcessedOperationsQty = (int)$this->operationCollectionFactory->create() + ->addFieldToFilter('bulk_uuid', $bulkUuid) + ->getSize(); + + return $allOperationsQty - $allProcessedOperationsQty; + } + + /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection $collection */ + $collection = $this->operationCollectionFactory->create(); + return $collection->addFieldToFilter('bulk_uuid', $bulkUuid) + ->addFieldToFilter('status', $status) + ->getSize(); + } + + /** + * @inheritDoc + */ + public function getBulksByUser($userId) + { + /** @var ResourceModel\Bulk\Collection $collection */ + $collection = $this->bulkCollectionFactory->create(); + $operationTableName = $this->resourceConnection->getTableName('magento_operation'); + $statusesArray = [ + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, + BulkSummaryInterface::NOT_STARTED, + OperationInterface::STATUS_TYPE_OPEN, + OperationInterface::STATUS_TYPE_COMPLETE + ]; + $select = $collection->getSelect(); + $select->columns(['status' => $this->calculatedStatusSql->get($operationTableName)]) + ->order(new \Zend_Db_Expr('FIELD(status, ' . implode(',', $statusesArray) . ')')); + $collection->addFieldToFilter('user_id', $userId) + ->addOrder('start_time'); + + return $collection->getItems(); + } + + /** + * @inheritDoc + */ + public function getBulkStatus($bulkUuid) + { + /** + * Number of operations that has been processed (i.e. operations with any status but 'open') + */ + $allProcessedOperationsQty = (int)$this->operationCollectionFactory->create() + ->addFieldToFilter('bulk_uuid', $bulkUuid) + ->getSize(); + + if ($allProcessedOperationsQty == 0) { + return BulkSummaryInterface::NOT_STARTED; + } + + /** + * Total number of operations that has been scheduled within the given bulk + */ + $allOperationsQty = $this->getOperationCount($bulkUuid); + + /** + * Number of operations that has not been started yet (i.e. operations with status 'open') + */ + $allOpenOperationsQty = $allOperationsQty - $allProcessedOperationsQty; + + /** + * Number of operations that has been completed successfully + */ + $allCompleteOperationsQty = $this->operationCollectionFactory->create() + ->addFieldToFilter('bulk_uuid', $bulkUuid)->addFieldToFilter( + 'status', + OperationInterface::STATUS_TYPE_COMPLETE + )->getSize(); + + if ($allCompleteOperationsQty == $allOperationsQty) { + return BulkSummaryInterface::FINISHED_SUCCESSFULLY; + } + + if ($allOpenOperationsQty > 0 && $allOpenOperationsQty !== $allOperationsQty) { + return BulkSummaryInterface::IN_PROGRESS; + } + + return BulkSummaryInterface::FINISHED_WITH_FAILURE; + } + + /** + * Get total number of operations that has been scheduled within the given bulk. + * + * @param string $bulkUuid + * @return int + */ + private function getOperationCount($bulkUuid) + { + $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + + return (int)$connection->fetchOne( + $connection->select() + ->from($metadata->getEntityTable(), 'operation_count') + ->where('uuid = ?', $bulkUuid) + ); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/CalculatedStatusSql.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/CalculatedStatusSql.php new file mode 100644 index 0000000000000..7bdf8a5b7d400 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/CalculatedStatusSql.php @@ -0,0 +1,32 @@ +getData(self::OPERATIONS_LIST); + } + + /** + * @inheritDoc + */ + public function setOperationsList($operationStatusList) + { + return $this->setData(self::OPERATIONS_LIST, $operationStatusList); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php new file mode 100644 index 0000000000000..47c317138ec64 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php @@ -0,0 +1,39 @@ + BulkSummaryInterface::NOT_STARTED, + 'label' => 'Not Started' + ], + [ + 'value' => BulkSummaryInterface::IN_PROGRESS, + 'label' => 'In Progress' + ], + [ + 'value' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, + 'label' => 'Finished Successfully' + ], + [ + 'value' => BulkSummaryInterface::FINISHED_WITH_FAILURE, + 'label' => 'Finished with Failure' + ] + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Short.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Short.php new file mode 100644 index 0000000000000..c6aa99e67202b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Short.php @@ -0,0 +1,31 @@ +getData(self::OPERATIONS_LIST); + } + + /** + * @inheritDoc + */ + public function setOperationsList($operationStatusList) + { + return $this->setData(self::OPERATIONS_LIST, $operationStatusList); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php new file mode 100644 index 0000000000000..e99233d076957 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php @@ -0,0 +1,118 @@ +getData(self::BULK_ID); + } + + /** + * @inheritDoc + */ + public function setBulkId($bulkUuid) + { + return $this->setData(self::BULK_ID, $bulkUuid); + } + + /** + * @inheritDoc + */ + public function getDescription() + { + return $this->getData(self::DESCRIPTION); + } + + /** + * @inheritDoc + */ + public function setDescription($description) + { + return $this->setData(self::DESCRIPTION, $description); + } + + /** + * @inheritDoc + */ + public function getStartTime() + { + return $this->getData(self::START_TIME); + } + + /** + * @inheritDoc + */ + public function setStartTime($timestamp) + { + return $this->setData(self::START_TIME, $timestamp); + } + + /** + * @inheritDoc + */ + public function getUserId() + { + return $this->getData(self::USER_ID); + } + + /** + * @inheritDoc + */ + public function setUserId($userId) + { + return $this->setData(self::USER_ID, $userId); + } + + /** + * @inheritDoc + */ + public function getOperationCount() + { + return $this->getData(self::OPERATION_COUNT); + } + + /** + * @inheritDoc + */ + public function setOperationCount($operationCount) + { + return $this->setData(self::OPERATION_COUNT, $operationCount); + } + + /** + * Retrieve existing extension attributes object. + * + * @return \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object. + * + * @param \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface $extensionAttributes + ) { + return $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php b/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php new file mode 100644 index 0000000000000..de0f89a71650a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php @@ -0,0 +1,60 @@ + self::SYSTEM_TOPIC_NAME, + CommunicationConfig::TOPIC_IS_SYNCHRONOUS => false, + CommunicationConfig::TOPIC_REQUEST => OperationInterface::class, + CommunicationConfig::TOPIC_REQUEST_TYPE => CommunicationConfig::TOPIC_REQUEST_TYPE_CLASS, + CommunicationConfig::TOPIC_RESPONSE => null, + CommunicationConfig::TOPIC_HANDLERS => [], + ]; + /**#@-*/ + + /** + * Get array of generated topics name and related to this topic service class and methods + * + * @return array + */ + public function getServices(); + + /** + * Get topic name from webapi_async_config services config array by route url and http method + * + * @param string $routeUrl + * @param string $httpMethod GET|POST|PUT|DELETE + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getTopicName($routeUrl, $httpMethod); +} diff --git a/app/code/Magento/AsynchronousOperations/Model/Entity/BulkSummaryMapper.php b/app/code/Magento/AsynchronousOperations/Model/Entity/BulkSummaryMapper.php new file mode 100644 index 0000000000000..4abbde4c3602b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/Entity/BulkSummaryMapper.php @@ -0,0 +1,65 @@ +metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + } + + /** + * {@inheritdoc} + */ + public function entityToDatabase($entityType, $data) + { + // workaround for delete/update operations that are currently using only primary key as identifier + if (!empty($data['uuid'])) { + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + $select = $connection->select()->from($metadata->getEntityTable(), 'id')->where("uuid = ?", $data['uuid']); + $identifier = $connection->fetchOne($select); + if ($identifier !== false) { + $data['id'] = $identifier; + } + } + return $data; + } + + /** + * {@inheritdoc} + * @codeCoverageIgnore + */ + public function databaseToEntity($entityType, $data) + { + return $data; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ItemStatus.php b/app/code/Magento/AsynchronousOperations/Model/ItemStatus.php new file mode 100644 index 0000000000000..b493e0bb663d3 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ItemStatus.php @@ -0,0 +1,103 @@ +getData(self::ENTITY_ID); + } + + /** + * @inheritDoc + */ + public function setId($entityId) + { + return $this->setData(self::ENTITY_ID, $entityId); + } + + /** + * @inheritDoc + */ + public function getDataHash() + { + return $this->getData(self::DATA_HASH); + } + + /** + * @inheritDoc + */ + public function setDataHash($hash) + { + return $this->setData(self::DATA_HASH, $hash); + } + + /** + * @inheritDoc + */ + public function getStatus() + { + return $this->getData(self::STATUS); + } + + /** + * @inheritDoc + */ + public function setStatus($status = self::STATUS_ACCEPTED) + { + return $this->setData(self::STATUS, $status); + } + + /** + * @inheritDoc + */ + public function getErrorMessage() + { + return $this->getData(self::ERROR_MESSAGE); + } + + /** + * @inheritDoc + */ + public function setErrorMessage($errorMessage = null) + { + if ($errorMessage instanceof \Exception) { + $errorMessage = $errorMessage->getMessage(); + } + + return $this->setData(self::ERROR_MESSAGE, $errorMessage); + } + + /** + * @inheritDoc + */ + public function getErrorCode() + { + return $this->getData(self::ERROR_CODE); + } + + /** + * @inheritDoc + */ + public function setErrorCode($errorCode = null) + { + if ($errorCode instanceof \Exception) { + $errorCode = $errorCode->getCode(); + } + + return $this->setData(self::ERROR_CODE, (int) $errorCode); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php new file mode 100644 index 0000000000000..28bc8141a8e99 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -0,0 +1,145 @@ +invoker = $invoker; + $this->resource = $resource; + $this->messageController = $messageController; + $this->configuration = $configuration; + $this->operationProcessor = $operationProcessorFactory->create([ + 'configuration' => $configuration + ]); + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function process($maxNumberOfMessages = null) + { + $queue = $this->configuration->getQueue(); + + if (!isset($maxNumberOfMessages)) { + $queue->subscribe($this->getTransactionCallback($queue)); + } else { + $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); + } + } + + /** + * Get transaction callback. This handles the case of async. + * + * @param QueueInterface $queue + * @return \Closure + */ + private function getTransactionCallback(QueueInterface $queue) + { + return function (EnvelopeInterface $message) use ($queue) { + /** @var LockInterface $lock */ + $lock = null; + try { + $topicName = $message->getProperties()['topic_name']; + $lock = $this->messageController->lock($message, $this->configuration->getConsumerName()); + + $allowedTopics = $this->configuration->getTopicNames(); + if (in_array($topicName, $allowedTopics)) { + $this->operationProcessor->process($message->getBody()); + } else { + $queue->reject($message); + return; + } + $queue->acknowledge($message); + } catch (MessageLockException $exception) { + $queue->acknowledge($message); + } catch (ConnectionLostException $e) { + if ($lock) { + $this->resource->getConnection() + ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); + } + } catch (NotFoundException $e) { + $queue->acknowledge($message); + $this->logger->warning($e->getMessage()); + } catch (\Exception $e) { + $queue->reject($message, false, $e->getMessage()); + if ($lock) { + $this->resource->getConnection() + ->delete($this->resource->getTableName('queue_lock'), ['id = ?' => $lock->getId()]); + } + } + }; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/MassPublisher.php b/app/code/Magento/AsynchronousOperations/Model/MassPublisher.php new file mode 100644 index 0000000000000..5f0f8e28f9fe6 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/MassPublisher.php @@ -0,0 +1,106 @@ +exchangeRepository = $exchangeRepository; + $this->envelopeFactory = $envelopeFactory; + $this->messageEncoder = $messageEncoder; + $this->messageValidator = $messageValidator; + $this->publisherConfig = $publisherConfig; + $this->messageIdGenerator = $messageIdGenerator; + } + + /** + * {@inheritdoc} + */ + public function publish($topicName, $data) + { + $envelopes = []; + foreach ($data as $message) { + $this->messageValidator->validate(AsyncConfig::SYSTEM_TOPIC_NAME, $message); + $message = $this->messageEncoder->encode(AsyncConfig::SYSTEM_TOPIC_NAME, $message); + $envelopes[] = $this->envelopeFactory->create( + [ + 'body' => $message, + 'properties' => [ + 'delivery_mode' => 2, + 'message_id' => $this->messageIdGenerator->generate($topicName), + ] + ] + ); + } + $publisher = $this->publisherConfig->getPublisher($topicName); + $connectionName = $publisher->getConnection()->getName(); + $exchange = $this->exchangeRepository->getByConnectionName($connectionName); + $exchange->enqueue($topicName, $envelopes); + return null; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php new file mode 100644 index 0000000000000..2d516e82f4016 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -0,0 +1,155 @@ +identityService = $identityService; + $this->itemStatusInterfaceFactory = $itemStatusInterfaceFactory; + $this->asyncResponseFactory = $asyncResponseFactory; + $this->bulkManagement = $bulkManagement; + $this->logger = $logger; + $this->operationRepository = $operationRepository; + } + + /** + * Schedule new bulk operation based on the list of entities + * + * @param $topicName + * @param $entitiesArray + * @param null $groupId + * @param null $userId + * @return AsyncResponseInterface + * @throws BulkException + * @throws LocalizedException + */ + public function publishMass($topicName, array $entitiesArray, $groupId = null, $userId = null) + { + $bulkDescription = __('Topic %1', $topicName); + + if ($groupId == null) { + $groupId = $this->identityService->generateId(); + + /** create new bulk without operations */ + if (!$this->bulkManagement->scheduleBulk($groupId, [], $bulkDescription, $userId)) { + throw new LocalizedException( + __('Something went wrong while processing the request.') + ); + } + } + + $operations = []; + $requestItems = []; + $bulkException = new BulkException(); + foreach ($entitiesArray as $key => $entityParams) { + /** @var \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface $requestItem */ + $requestItem = $this->itemStatusInterfaceFactory->create(); + + try { + $operations[] = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $requestItem->setId($key); + $requestItem->setStatus(ItemStatusInterface::STATUS_ACCEPTED); + $requestItems[] = $requestItem; + } catch (\Exception $exception) { + $this->logger->error($exception); + $requestItem->setId($key); + $requestItem->setStatus(ItemStatusInterface::STATUS_REJECTED); + $requestItem->setErrorMessage($exception); + $requestItem->setErrorCode($exception); + $requestItems[] = $requestItem; + $bulkException->addException(new LocalizedException( + __('Error processing %key element of input data', ['key' => $key]), + $exception + )); + } + } + + if (!$this->bulkManagement->scheduleBulk($groupId, $operations, $bulkDescription, $userId)) { + throw new LocalizedException( + __('Something went wrong while processing the request.') + ); + } + /** @var AsyncResponseInterface $asyncResponse */ + $asyncResponse = $this->asyncResponseFactory->create(); + $asyncResponse->setBulkUuid($groupId); + $asyncResponse->setRequestItems($requestItems); + + if ($bulkException->wasErrorAdded()) { + $asyncResponse->setErrors(true); + $bulkException->addData($asyncResponse); + throw $bulkException; + } else { + $asyncResponse->setErrors(false); + } + + return $asyncResponse; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/Operation.php b/app/code/Magento/AsynchronousOperations/Model/Operation.php new file mode 100644 index 0000000000000..70cc9f0ebc575 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/Operation.php @@ -0,0 +1,166 @@ +getData(self::ID); + } + + /** + * @inheritDoc + */ + public function setId($id) + { + return $this->setData(self::ID, $id); + } + + /** + * @inheritDoc + */ + public function getBulkUuid() + { + return $this->getData(self::BULK_ID); + } + + /** + * @inheritDoc + */ + public function setBulkUuid($bulkId) + { + return $this->setData(self::BULK_ID, $bulkId); + } + + /** + * @inheritDoc + */ + public function getTopicName() + { + return $this->getData(self::TOPIC_NAME); + } + + /** + * @inheritDoc + */ + public function setTopicName($topic) + { + return $this->setData(self::TOPIC_NAME, $topic); + } + + /** + * @inheritDoc + */ + public function getSerializedData() + { + return $this->getData(self::SERIALIZED_DATA); + } + + /** + * @inheritDoc + */ + public function setSerializedData($serializedData) + { + return $this->setData(self::SERIALIZED_DATA, $serializedData); + } + + /** + * @inheritDoc + */ + public function getResultSerializedData() + { + return $this->getData(self::RESULT_SERIALIZED_DATA); + } + + /** + * @inheritDoc + */ + public function setResultSerializedData($resultSerializedData) + { + return $this->setData(self::RESULT_SERIALIZED_DATA, $resultSerializedData); + } + + /** + * @inheritDoc + */ + public function getStatus() + { + return $this->getData(self::STATUS); + } + + /** + * @inheritDoc + */ + public function setStatus($status) + { + return $this->setData(self::STATUS, $status); + } + + /** + * @inheritDoc + */ + public function getResultMessage() + { + return $this->getData(self::RESULT_MESSAGE); + } + + /** + * @inheritDoc + */ + public function setResultMessage($resultMessage) + { + return $this->setData(self::RESULT_MESSAGE, $resultMessage); + } + + /** + * @inheritDoc + */ + public function getErrorCode() + { + return $this->getData(self::ERROR_CODE); + } + + /** + * @inheritDoc + */ + public function setErrorCode($errorCode) + { + return $this->setData(self::ERROR_CODE, $errorCode); + } + + /** + * Retrieve existing extension attributes object. + * + * @return \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object. + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterface $extensionAttributes + ) { + return $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/Operation/Details.php b/app/code/Magento/AsynchronousOperations/Model/Operation/Details.php new file mode 100644 index 0000000000000..d248f9c3e9276 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/Operation/Details.php @@ -0,0 +1,164 @@ + 'operations_successful', + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED => 'failed_retriable', + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED => 'failed_not_retriable', + OperationInterface::STATUS_TYPE_OPEN => 'open', + OperationInterface::STATUS_TYPE_REJECTED => 'rejected', + ]; + + /** + * Init dependencies. + * + * @param \Magento\Framework\Bulk\BulkStatusInterface $bulkStatus + * @param null $bulkUuid + */ + public function __construct( + BulkStatusInterface $bulkStatus, + $bulkUuid = null + ) { + $this->bulkStatus = $bulkStatus; + $this->bulkUuid = $bulkUuid; + } + + /** + * Collect operations statistics for the bulk + * + * @param string $bulkUuid + * @return array + */ + public function getDetails($bulkUuid) + { + $details = [ + 'operations_total' => 0, + 'operations_successful' => 0, + 'operations_failed' => 0, + 'failed_retriable' => 0, + 'failed_not_retriable' => 0, + 'rejected' => 0, + ]; + + if (array_key_exists($bulkUuid, $this->operationCache)) { + return $this->operationCache[$bulkUuid]; + } + + foreach ($this->statusMap as $statusCode => $readableKey) { + $details[$readableKey] = $this->bulkStatus->getOperationsCountByBulkIdAndStatus( + $bulkUuid, + $statusCode + ); + } + + $details['operations_total'] = array_sum($details); + $details['operations_failed'] = $details['failed_retriable'] + $details['failed_not_retriable']; + $this->operationCache[$bulkUuid] = $details; + + return $details; + } + + /** + * @inheritDoc + */ + public function getOperationsTotal() + { + $this->getDetails($this->bulkUuid); + + return $this->operationCache[$this->bulkUuid]['operations_total']; + } + + /** + * @inheritDoc + */ + public function getOpen() + { + $this->getDetails($this->bulkUuid); + $statusKey = $this->statusMap[OperationInterface::STATUS_TYPE_OPEN]; + + return $this->operationCache[$this->bulkUuid][$statusKey]; + } + + /** + * @inheritDoc + */ + public function getOperationsSuccessful() + { + $this->getDetails($this->bulkUuid); + $statusKey = $this->statusMap[OperationInterface::STATUS_TYPE_COMPLETE]; + + return $this->operationCache[$this->bulkUuid][$statusKey]; + } + + /** + * @inheritDoc + */ + public function getTotalFailed() + { + $this->getDetails($this->bulkUuid); + + return $this->operationCache[$this->bulkUuid]['operations_failed']; + } + + /** + * @inheritDoc + */ + public function getFailedNotRetriable() + { + $statusKey = $this->statusMap[OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED]; + + return $this->operationCache[$this->bulkUuid][$statusKey]; + } + + /** + * @inheritDoc + */ + public function getFailedRetriable() + { + $this->getDetails($this->bulkUuid); + $statusKey = $this->statusMap[OperationInterface::STATUS_TYPE_RETRIABLY_FAILED]; + + return $this->operationCache[$this->bulkUuid][$statusKey]; + } + + /** + * @inheritDoc + */ + public function getRejected() + { + $this->getDetails($this->bulkUuid); + $statusKey = $this->statusMap[OperationInterface::STATUS_TYPE_REJECTED]; + + return $this->operationCache[$this->bulkUuid][$statusKey]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationList.php b/app/code/Magento/AsynchronousOperations/Model/OperationList.php new file mode 100644 index 0000000000000..7de62107415b0 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationList.php @@ -0,0 +1,34 @@ +items = $items; + } + + /** + * @inheritdoc + */ + public function getItems() + { + return $this->items; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php new file mode 100644 index 0000000000000..ce780a4ba858d --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php @@ -0,0 +1,76 @@ +entityManager = $entityManager; + $this->operationFactory = $operationFactory; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function changeOperationStatus( + $operationId, + $status, + $errorCode = null, + $message = null, + $data = null, + $resultData = null + ) { + try { + $operationEntity = $this->operationFactory->create(); + $this->entityManager->load($operationEntity, $operationId); + $operationEntity->setErrorCode($errorCode); + $operationEntity->setStatus($status); + $operationEntity->setResultMessage($message); + $operationEntity->setSerializedData($data); + $operationEntity->setResultSerializedData($resultData); + $operationEntity->setResultSerializedData($resultData); + $this->entityManager->save($operationEntity); + } catch (\Exception $exception) { + $this->logger->critical($exception->getMessage()); + return false; + } + return true; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php b/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php new file mode 100644 index 0000000000000..6826c34fd35f0 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php @@ -0,0 +1,227 @@ +messageValidator = $messageValidator; + $this->messageEncoder = $messageEncoder; + $this->configuration = $configuration; + $this->jsonHelper = $jsonHelper; + $this->operationManagement = $operationManagement; + $this->logger = $logger; + $this->serviceOutputProcessor = $serviceOutputProcessor; + $this->communicationConfig = $communicationConfig; + } + + /** + * Process topic-based encoded message + * + * @param string $encodedMessage + * @return void + */ + public function process(string $encodedMessage) + { + $operation = $this->messageEncoder->decode(AsyncConfig::SYSTEM_TOPIC_NAME, $encodedMessage); + $this->messageValidator->validate(AsyncConfig::SYSTEM_TOPIC_NAME, $operation); + + $status = OperationInterface::STATUS_TYPE_COMPLETE; + $errorCode = null; + $messages = []; + $topicName = $operation->getTopicName(); + $handlers = $this->configuration->getHandlers($topicName); + try { + $data = $this->jsonHelper->unserialize($operation->getSerializedData()); + $entityParams = $this->messageEncoder->decode($topicName, $data['meta_information']); + $this->messageValidator->validate($topicName, $entityParams); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $messages[] = $e->getMessage(); + } + + $outputData = null; + if ($errorCode === null) { + foreach ($handlers as $callback) { + $result = $this->executeHandler($callback, $entityParams); + $status = $result['status']; + $errorCode = $result['error_code']; + $messages = array_merge($messages, $result['messages']); + $outputData = $result['output_data']; + } + } + + if (isset($outputData)) { + try { + $communicationConfig = $this->communicationConfig->getTopic($topicName); + $asyncHandler = + $communicationConfig[CommunicationConfig::TOPIC_HANDLERS][AsyncConfig::DEFAULT_HANDLER_NAME]; + $serviceClass = $asyncHandler[CommunicationConfig::HANDLER_TYPE]; + $serviceMethod = $asyncHandler[CommunicationConfig::HANDLER_METHOD]; + $outputData = $this->serviceOutputProcessor->process( + $outputData, + $serviceClass, + $serviceMethod + ); + $outputData = $this->jsonHelper->serialize($outputData); + } catch (\Exception $e) { + $messages[] = $e->getMessage(); + } + } + + $serializedData = (isset($errorCode)) ? $operation->getSerializedData() : null; + $this->operationManagement->changeOperationStatus( + $operation->getId(), + $status, + $errorCode, + implode('; ', $messages), + $serializedData, + $outputData + ); + } + + /** + * Execute topic handler + * + * @param $callback + * @param $entityParams + * @return array + */ + private function executeHandler($callback, $entityParams) + { + $result = [ + 'status' => OperationInterface::STATUS_TYPE_COMPLETE, + 'error_code' => null, + 'messages' => [], + 'output_data' => null + ]; + try { + $result['output_data'] = call_user_func_array($callback, $entityParams); + $result['messages'][] = sprintf('Service execution success %s::%s', get_class($callback[0]), $callback[1]); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof LockWaitException + || $e instanceof DeadlockException + || $e instanceof ConnectionException + ) { + $result['status'] = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $result['error_code'] = $e->getCode(); + $result['messages'][] = __($e->getMessage()); + } else { + $result['status'] = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $result['error_code'] = $e->getCode(); + $result['messages'][] = + __('Sorry, something went wrong during product prices update. Please see log for details.'); + } + } catch (NoSuchEntityException $e) { + $this->logger->error($e->getMessage()); + $result['status'] = ($e instanceof TemporaryStateExceptionInterface) ? + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED : + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $result['error_code'] = $e->getCode(); + $result['messages'][] = $e->getMessage(); + } catch (LocalizedException $e) { + $this->logger->error($e->getMessage()); + $result['status'] = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $result['error_code'] = $e->getCode(); + $result['messages'][] = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $result['status'] = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $result['error_code'] = $e->getCode(); + $result['messages'][] = $e->getMessage(); + } + return $result; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationStatus.php b/app/code/Magento/AsynchronousOperations/Model/OperationStatus.php new file mode 100644 index 0000000000000..5c975bd1a9a45 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationStatus.php @@ -0,0 +1,52 @@ +getData(OperationInterface::ID); + } + + /** + * @inheritDoc + */ + public function getStatus() + { + return $this->getData(OperationInterface::STATUS); + } + + /** + * @inheritDoc + */ + public function getResultMessage() + { + return $this->getData(OperationInterface::RESULT_MESSAGE); + } + + /** + * @inheritDoc + */ + public function getErrorCode() + { + return $this->getData(OperationInterface::ERROR_CODE); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Bulk.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Bulk.php new file mode 100644 index 0000000000000..d56d54359ee9a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Bulk.php @@ -0,0 +1,23 @@ +_init('magento_bulk', 'uuid'); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Bulk/Collection.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Bulk/Collection.php new file mode 100644 index 0000000000000..6dd997c5333ff --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Bulk/Collection.php @@ -0,0 +1,27 @@ +_init( + \Magento\AsynchronousOperations\Model\BulkSummary::class, + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk::class + ); + $this->setMainTable('magento_bulk'); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php new file mode 100644 index 0000000000000..061d0917e7ab0 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php @@ -0,0 +1,23 @@ +_init('magento_operation', 'id'); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/CheckIfExists.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/CheckIfExists.php new file mode 100644 index 0000000000000..46c4e4bc1c2bc --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/CheckIfExists.php @@ -0,0 +1,28 @@ +_init( + \Magento\AsynchronousOperations\Model\Operation::class, + \Magento\AsynchronousOperations\Model\ResourceModel\Operation::class + ); + $this->setMainTable('magento_operation'); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/Create.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/Create.php new file mode 100644 index 0000000000000..ce2357c6b2b4f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/Create.php @@ -0,0 +1,85 @@ +metadataPool = $metadataPool; + $this->typeResolver = $typeResolver; + $this->resourceConnection = $resourceConnection; + } + + /** + * Save all operations from the list in one query. + * + * @param object $entity + * @param array $arguments + * @return object + * @throws \Exception + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute($entity, $arguments = []) + { + $entityType = $this->typeResolver->resolve($entity); + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $this->resourceConnection->getConnection($metadata->getEntityConnectionName()); + try { + $connection->beginTransaction(); + $data = []; + foreach ($entity->getItems() as $operation) { + $data[] = $operation->getData(); + } + $connection->insertOnDuplicate( + $metadata->getEntityTable(), + $data, + [ + 'status', + 'error_code', + 'result_message', + ] + ); + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw $e; + } + return $entity; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php new file mode 100644 index 0000000000000..54e65cc3470dd --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php @@ -0,0 +1,98 @@ +operationFactory = $operationFactory; + $this->jsonSerializer = $jsonSerializer; + $this->messageEncoder = $messageEncoder; + $this->messageValidator = $messageValidator; + $this->entityManager = $entityManager; + } + + /** + * @param $topicName + * @param $entityParams + * @param $groupId + * @return mixed + */ + public function createByTopic($topicName, $entityParams, $groupId) + { + $this->messageValidator->validate($topicName, $entityParams); + $encodedMessage = $this->messageEncoder->encode($topicName, $entityParams); + + $serializedData = [ + 'entity_id' => null, + 'entity_link' => '', + 'meta_information' => $encodedMessage, + ]; + $data = [ + 'data' => [ + OperationInterface::BULK_ID => $groupId, + OperationInterface::TOPIC_NAME => $topicName, + OperationInterface::SERIALIZED_DATA => $this->jsonSerializer->serialize($serializedData), + OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, + ], + ]; + + /** @var \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation */ + $operation = $this->operationFactory->create($data); + return $this->entityManager->save($operation); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php new file mode 100644 index 0000000000000..8457a641ed9a9 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php @@ -0,0 +1,171 @@ +messageFactory = $messageFactory; + $this->bulkStatus = $bulkStatus; + $this->userContext = $userContext; + $this->operationDetails = $operationDetails; + $this->bulkNotificationManagement = $bulkNotificationManagement; + $this->authorization = $authorization; + $this->statusMapper = $statusMapper; + } + + /** + * Adding bulk related messages to notification area + * + * @param \Magento\AdminNotification\Model\ResourceModel\System\Message\Collection\Synchronized $collection + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterToArray( + \Magento\AdminNotification\Model\ResourceModel\System\Message\Collection\Synchronized $collection, + $result + ) { + if (!$this->authorization->isAllowed('Magento_Logging::system_magento_logging_bulk_operations')) { + return $result; + } + $userId = $this->userContext->getUserId(); + $userBulks = $this->bulkStatus->getBulksByUser($userId); + $acknowledgedBulks = $this->getAcknowledgedBulksUuid( + $this->bulkNotificationManagement->getAcknowledgedBulksByUser($userId) + ); + $bulkMessages = []; + foreach ($userBulks as $bulk) { + $bulkUuid = $bulk->getBulkId(); + if (!in_array($bulkUuid, $acknowledgedBulks)) { + $details = $this->operationDetails->getDetails($bulkUuid); + $text = $this->getText($details); + $bulkStatus = $this->statusMapper->operationStatusToBulkSummaryStatus($bulk->getStatus()); + if ($bulkStatus === \Magento\Framework\Bulk\BulkSummaryInterface::IN_PROGRESS) { + $text = __('%1 item(s) are currently being updated.', $details['operations_total']) . $text; + } + $data = [ + 'data' => [ + 'text' => __('Task "%1": ', $bulk->getDescription()) . $text, + 'severity' => \Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR, + 'identity' => md5('bulk' . $bulkUuid), + 'uuid' => $bulkUuid, + 'status' => $bulkStatus, + 'created_at' => $bulk->getStartTime() + ] + ]; + $bulkMessages[] = $this->messageFactory->create($data)->toArray(); + } + } + + if (!empty($bulkMessages)) { + $result['totalRecords'] += count($bulkMessages); + $bulkMessages = array_slice($bulkMessages, 0, 5); + $result['items'] = array_merge($bulkMessages, $result['items']); + } + return $result; + } + + /** + * Get Bulk notification message + * + * @param array $operationDetails + * @return \Magento\Framework\Phrase|string + */ + private function getText($operationDetails) + { + if (0 == $operationDetails['operations_successful'] && 0 == $operationDetails['operations_failed']) { + return __('%1 item(s) have been scheduled for update.', $operationDetails['operations_total']); + } + + $summaryReport = ''; + if ($operationDetails['operations_successful'] > 0) { + $summaryReport .= __( + '%1 item(s) have been successfully updated.', + $operationDetails['operations_successful'] + ); + } + + if ($operationDetails['operations_failed'] > 0) { + $summaryReport .= '' + . __('%1 item(s) failed to update', $operationDetails['operations_failed']) + . ''; + } + return $summaryReport; + } + + /** + * Get array with acknowledgedBulksUuid + * + * @param array $acknowledgedBulks + * @return array + */ + private function getAcknowledgedBulksUuid($acknowledgedBulks) + { + $acknowledgedBulksArray = []; + foreach ($acknowledgedBulks as $bulk) { + $acknowledgedBulksArray[] = $bulk->getBulkId(); + } + return $acknowledgedBulksArray; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/StatusMapper.php b/app/code/Magento/AsynchronousOperations/Model/StatusMapper.php new file mode 100644 index 0000000000000..e5aee6d2f59fa --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/StatusMapper.php @@ -0,0 +1,64 @@ + BulkSummaryInterface::FINISHED_WITH_FAILURE, + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED => BulkSummaryInterface::FINISHED_WITH_FAILURE, + OperationInterface::STATUS_TYPE_REJECTED => BulkSummaryInterface::FINISHED_WITH_FAILURE, + OperationInterface::STATUS_TYPE_COMPLETE => BulkSummaryInterface::FINISHED_SUCCESSFULLY, + OperationInterface::STATUS_TYPE_OPEN => BulkSummaryInterface::IN_PROGRESS, + BulkSummaryInterface::NOT_STARTED => BulkSummaryInterface::NOT_STARTED + ]; + + if (isset($statusMapping[$operationStatus])) { + return $statusMapping[$operationStatus]; + } + return null; + } + + /** + * Map bulk summary status to operation status + * + * @param int $bulkStatus + * @return int|null + */ + public function bulkSummaryStatusToOperationStatus($bulkStatus) + { + $statusMapping = [ + BulkSummaryInterface::FINISHED_WITH_FAILURE => [ + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_REJECTED + ], + BulkSummaryInterface::FINISHED_SUCCESSFULLY => OperationInterface::STATUS_TYPE_COMPLETE, + BulkSummaryInterface::IN_PROGRESS => OperationInterface::STATUS_TYPE_OPEN, + BulkSummaryInterface::NOT_STARTED => BulkSummaryInterface::NOT_STARTED + ]; + + if (isset($statusMapping[$bulkStatus])) { + return $statusMapping[$bulkStatus]; + } + return null; + } +} diff --git a/app/code/Magento/AsynchronousOperations/README.md b/app/code/Magento/AsynchronousOperations/README.md new file mode 100644 index 0000000000000..fb7d53df1b81c --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/README.md @@ -0,0 +1 @@ + This component is designed to provide response for client who launched the bulk operation as soon as possible and postpone handling of operations moving them to background handler. \ No newline at end of file diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/BackButtonTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/BackButtonTest.php new file mode 100644 index 0000000000000..d6d4c5e7479f9 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/BackButtonTest.php @@ -0,0 +1,46 @@ +urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) + ->getMock(); + $this->block = new \Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\BackButton( + $this->urlBuilderMock + ); + } + + public function testGetButtonData() + { + $backUrl = 'back url'; + $expectedResult = [ + 'label' => __('Back'), + 'on_click' => sprintf("location.href = '%s';", $backUrl), + 'class' => 'back', + 'sort_order' => 10 + ]; + + $this->urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with('*/') + ->willReturn($backUrl); + + $this->assertEquals($expectedResult, $this->block->getButtonData()); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/DoneButtonTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/DoneButtonTest.php new file mode 100644 index 0000000000000..10c9d898aa526 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/DoneButtonTest.php @@ -0,0 +1,91 @@ +bulkStatusMock = $this->createMock(\Magento\Framework\Bulk\BulkStatusInterface::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->getMock(); + $this->block = new \Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\DoneButton( + $this->bulkStatusMock, + $this->requestMock + ); + } + + /** + * @param int $failedCount + * @param int $buttonsParam + * @param array $expectedResult + * @dataProvider getButtonDataProvider + */ + public function testGetButtonData($failedCount, $buttonsParam, $expectedResult) + { + $uuid = 'some standard uuid string'; + $this->requestMock->expects($this->exactly(2)) + ->method('getParam') + ->withConsecutive(['uuid'], ['buttons']) + ->willReturnOnConsecutiveCalls($uuid, $buttonsParam); + $this->bulkStatusMock->expects($this->once()) + ->method('getOperationsCountByBulkIdAndStatus') + ->with($uuid, OperationInterface::STATUS_TYPE_RETRIABLY_FAILED) + ->willReturn($failedCount); + + $this->assertEquals($expectedResult, $this->block->getButtonData()); + } + + /** + * @return array + */ + public function getButtonDataProvider() + { + return [ + [1, 0, []], + [0, 0, []], + [ + 0, + 1, + [ + 'label' => __('Done'), + 'class' => 'primary', + 'sort_order' => 10, + 'on_click' => '', + 'data_attribute' => [ + 'mage-init' => [ + 'Magento_Ui/js/form/button-adapter' => [ + 'actions' => [ + [ + 'targetName' => 'notification_area.notification_area.modalContainer.modal', + 'actionName' => 'closeModal' + ], + ], + ], + ], + ], + ] + ], + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/RetryButtonTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/RetryButtonTest.php new file mode 100644 index 0000000000000..b7c154be09d89 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Block/Adminhtml/Bulk/Details/RetryButtonTest.php @@ -0,0 +1,79 @@ +detailsMock = $this->getMockBuilder(\Magento\AsynchronousOperations\Model\Operation\Details::class) + ->disableOriginalConstructor() + ->getMock(); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->getMock(); + $this->block = new \Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\RetryButton( + $this->detailsMock, + $this->requestMock + ); + } + + /** + * @param int $failedCount + * @param array $expectedResult + * @dataProvider getButtonDataProvider + */ + public function testGetButtonData($failedCount, $expectedResult) + { + $details = ['failed_retriable' => $failedCount]; + $uuid = 'some standard uuid string'; + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('uuid') + ->willReturn($uuid); + $this->detailsMock->expects($this->once()) + ->method('getDetails') + ->with($uuid) + ->willReturn($details); + + $this->assertEquals($expectedResult, $this->block->getButtonData()); + } + + /** + * @return array + */ + public function getButtonDataProvider() + { + return [ + [0, []], + [ + 20, + [ + 'label' => __('Retry'), + 'class' => 'retry primary', + 'data_attribute' => [ + 'mage-init' => ['button' => ['event' => 'save']], + 'form-role' => 'save', + ], + ] + ], + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Bulk/DetailsTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Bulk/DetailsTest.php new file mode 100644 index 0000000000000..ecd33d355c223 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Bulk/DetailsTest.php @@ -0,0 +1,78 @@ +viewMock = $this->createMock(\Magento\Framework\App\ViewInterface::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->resultFactoryMock = $this->createMock(\Magento\Framework\View\Result\PageFactory::class); + $this->model = $objectManager->getObject( + \Magento\AsynchronousOperations\Controller\Adminhtml\Bulk\Details::class, + [ + 'request' => $this->requestMock, + 'resultPageFactory' => $this->resultFactoryMock, + 'view' => $this->viewMock, + + ] + ); + } + + public function testExecute() + { + $id = '42'; + $parameterName = 'uuid'; + $itemId = 'Magento_AsynchronousOperations::system_magento_logging_bulk_operations'; + $layoutMock = $this->createMock(\Magento\Framework\View\LayoutInterface::class); + + $blockMock = $this->createPartialMock( + \Magento\Framework\View\Element\BlockInterface::class, + ['setActive', 'getMenuModel', 'toHtml'] + ); + $menuModelMock = $this->createMock(\Magento\Backend\Model\Menu::class); + $this->viewMock->expects($this->once())->method('getLayout')->willReturn($layoutMock); + $layoutMock->expects($this->once())->method('getBlock')->willReturn($blockMock); + $blockMock->expects($this->once())->method('setActive')->with($itemId); + $blockMock->expects($this->once())->method('getMenuModel')->willReturn($menuModelMock); + $menuModelMock->expects($this->once())->method('getParentItems')->willReturn([]); + $pageMock = $this->createMock(\Magento\Framework\View\Result\Page::class); + $pageConfigMock = $this->createMock(\Magento\Framework\View\Page\Config::class); + $titleMock = $this->createMock(\Magento\Framework\View\Page\Title::class); + $this->resultFactoryMock->expects($this->once())->method('create')->willReturn($pageMock); + $this->requestMock->expects($this->once())->method('getParam')->with($parameterName)->willReturn($id); + $pageMock->expects($this->once())->method('getConfig')->willReturn($pageConfigMock); + $pageConfigMock->expects($this->once())->method('getTitle')->willReturn($titleMock); + $titleMock->expects($this->once())->method('prepend')->with($this->stringContains($id)); + $pageMock->expects($this->once())->method('initLayout'); + $this->assertEquals($pageMock, $this->model->execute()); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Bulk/RetryTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Bulk/RetryTest.php new file mode 100644 index 0000000000000..ab5e117b0225f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Bulk/RetryTest.php @@ -0,0 +1,166 @@ +bulkManagementMock = $this->createMock(BulkManagement::class); + $this->notificationManagementMock = $this->createMock(BulkNotificationManagement::class); + $this->requestMock = $this->createMock(RequestInterface::class); + $this->resultFactoryMock = $this->createPartialMock(ResultFactory::class, ['create']); + $this->jsonResultMock = $this->createMock(Json::class); + + $this->resultRedirectFactoryMock = $this->createPartialMock(RedirectFactory::class, ['create']); + $this->resultRedirectMock = $this->createMock(Redirect::class); + + $this->model = $objectManager->getObject( + Retry::class, + [ + 'bulkManagement' => $this->bulkManagementMock, + 'notificationManagement' => $this->notificationManagementMock, + 'request' => $this->requestMock, + 'resultRedirectFactory' => $this->resultRedirectFactoryMock, + 'resultFactory' => $this->resultFactoryMock, + ] + ); + } + + public function testExecute() + { + $bulkUuid = '49da7406-1ec3-4100-95ae-9654c83a6801'; + $operationsToRetry = [ + [ + 'key' => 'value', + 'error_code' => 1111, + ], + [ + 'error_code' => 2222, + ], + [ + 'error_code' => '3333', + ], + ]; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['uuid', null, $bulkUuid], + ['operations_to_retry', [], $operationsToRetry], + ['isAjax', null, false], + ]); + + $this->bulkManagementMock->expects($this->once()) + ->method('retryBulk') + ->with($bulkUuid, [1111, 2222, 3333]); + + $this->notificationManagementMock->expects($this->once()) + ->method('ignoreBulks') + ->with([$bulkUuid]) + ->willReturn(true); + + $this->resultRedirectFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultRedirectMock); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('bulk/index'); + + $this->model->execute(); + } + + public function testExecuteReturnsJsonResultWhenRequestIsSentViaAjax() + { + $bulkUuid = '49da7406-1ec3-4100-95ae-9654c83a6801'; + $operationsToRetry = [ + [ + 'key' => 'value', + 'error_code' => 1111, + ], + ]; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['uuid', null, $bulkUuid], + ['operations_to_retry', [], $operationsToRetry], + ['isAjax', null, true], + ]); + + $this->bulkManagementMock->expects($this->once()) + ->method('retryBulk') + ->with($bulkUuid, [1111]); + + $this->notificationManagementMock->expects($this->once()) + ->method('ignoreBulks') + ->with([$bulkUuid]) + ->willReturn(true); + + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON, []) + ->willReturn($this->jsonResultMock); + + $this->jsonResultMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(200); + + $this->assertEquals($this->jsonResultMock, $this->model->execute()); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Index/IndexTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Index/IndexTest.php new file mode 100644 index 0000000000000..98d51d8b0fd46 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Index/IndexTest.php @@ -0,0 +1,79 @@ +viewMock = $this->createMock(\Magento\Framework\App\ViewInterface::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->resultFactoryMock = $this->createMock(\Magento\Framework\View\Result\PageFactory::class); + + $this->model = $objectManager->getObject( + \Magento\AsynchronousOperations\Controller\Adminhtml\Index\Index::class, + [ + 'request' => $this->requestMock, + 'view' => $this->viewMock, + 'resultPageFactory' => $this->resultFactoryMock + + ] + ); + } + + public function testExecute() + { + $itemId = 'Magento_AsynchronousOperations::system_magento_logging_bulk_operations'; + $prependText = 'Bulk Actions Log'; + $layoutMock = $this->createMock(\Magento\Framework\View\LayoutInterface::class); + $menuModelMock = $this->createMock(\Magento\Backend\Model\Menu::class); + $pageMock = $this->createMock(\Magento\Framework\View\Result\Page::class); + $pageConfigMock = $this->createMock(\Magento\Framework\View\Page\Config::class); + $titleMock = $this->createMock(\Magento\Framework\View\Page\Title::class); + $this->resultFactoryMock->expects($this->once())->method('create')->willReturn($pageMock); + + $blockMock = $this->createPartialMock( + \Magento\Framework\View\Element\BlockInterface::class, + ['setActive', 'getMenuModel', 'toHtml'] + ); + + $this->viewMock->expects($this->once())->method('getLayout')->willReturn($layoutMock); + $layoutMock->expects($this->once())->method('getBlock')->willReturn($blockMock); + $blockMock->expects($this->once())->method('setActive')->with($itemId); + $blockMock->expects($this->once())->method('getMenuModel')->willReturn($menuModelMock); + $menuModelMock->expects($this->once())->method('getParentItems')->willReturn([]); + + $pageMock->expects($this->once())->method('getConfig')->willReturn($pageConfigMock); + $pageConfigMock->expects($this->once())->method('getTitle')->willReturn($titleMock); + $titleMock->expects($this->once())->method('prepend')->with($prependText); + $pageMock->expects($this->once())->method('initLayout'); + $this->model->execute(); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php new file mode 100644 index 0000000000000..8ec1ec4609aa9 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php @@ -0,0 +1,108 @@ +notificationManagementMock = $this->createMock(BulkNotificationManagement::class); + $this->requestMock = $this->createMock(RequestInterface::class); + $this->resultFactoryMock = $this->createPartialMock(ResultFactory::class, ['create']); + + $this->jsonResultMock = $this->createMock(Json::class); + + $this->model = $objectManager->getObject( + Dismiss::class, + [ + 'notificationManagement' => $this->notificationManagementMock, + 'request' => $this->requestMock, + 'resultFactory' => $this->resultFactoryMock, + ] + ); + } + + public function testExecute() + { + $bulkUuids = ['49da7406-1ec3-4100-95ae-9654c83a6801']; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->with('uuid', []) + ->willReturn($bulkUuids); + + $this->notificationManagementMock->expects($this->once()) + ->method('acknowledgeBulks') + ->with($bulkUuids) + ->willReturn(true); + + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON, []) + ->willReturn($this->jsonResultMock); + + $this->assertEquals($this->jsonResultMock, $this->model->execute()); + } + + public function testExecuteSetsBadRequestResponseStatusIfBulkWasNotAcknowledgedCorrectly() + { + $bulkUuids = ['49da7406-1ec3-4100-95ae-9654c83a6801']; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->with('uuid', []) + ->willReturn($bulkUuids); + + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON, []) + ->willReturn($this->jsonResultMock); + + $this->notificationManagementMock->expects($this->once()) + ->method('acknowledgeBulks') + ->with($bulkUuids) + ->willReturn(false); + + $this->jsonResultMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(400); + + $this->assertEquals($this->jsonResultMock, $this->model->execute()); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Cron/BulkCleanupTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Cron/BulkCleanupTest.php new file mode 100644 index 0000000000000..be38e9181734a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Cron/BulkCleanupTest.php @@ -0,0 +1,79 @@ +dateTimeMock = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); + $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $this->resourceConnectionMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); + $this->metadataPoolMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); + $this->timeMock = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTime::class); + $this->model = new \Magento\AsynchronousOperations\Cron\BulkCleanup( + $this->metadataPoolMock, + $this->resourceConnectionMock, + $this->dateTimeMock, + $this->scopeConfigMock, + $this->timeMock + ); + } + + public function testExecute() + { + $entityType = 'BulkSummaryInterface'; + $connectionName = 'Connection'; + $bulkLifetimeMultiplier = 10; + $bulkLifetime = 3600 * 24 * $bulkLifetimeMultiplier; + + $adapterMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + $entityMetadataMock = $this->createMock(\Magento\Framework\EntityManager\EntityMetadataInterface::class); + + $this->metadataPoolMock->expects($this->once())->method('getMetadata')->with($this->stringContains($entityType)) + ->willReturn($entityMetadataMock); + $entityMetadataMock->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $this->resourceConnectionMock->expects($this->once())->method('getConnectionByName')->with($connectionName) + ->willReturn($adapterMock); + $this->scopeConfigMock->expects($this->once())->method('getValue')->with($this->stringContains('bulk/lifetime')) + ->willReturn($bulkLifetimeMultiplier); + $this->timeMock->expects($this->once())->method('gmtTimestamp')->willReturn($bulkLifetime*10); + $this->dateTimeMock->expects($this->once())->method('formatDate')->with($bulkLifetime*9); + $adapterMock->expects($this->once())->method('delete'); + + $this->model->execute(); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/AccessValidatorTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/AccessValidatorTest.php new file mode 100644 index 0000000000000..8eb8778a384b0 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/AccessValidatorTest.php @@ -0,0 +1,80 @@ +userContextMock = $this->createMock(\Magento\Authorization\Model\UserContextInterface::class); + $this->entityManagerMock = $this->createMock(\Magento\Framework\EntityManager\EntityManager::class); + $this->bulkSummaryFactoryMock = $this->createPartialMock( + \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory::class, + ['create'] + ); + + $this->model = new \Magento\AsynchronousOperations\Model\AccessValidator( + $this->userContextMock, + $this->entityManagerMock, + $this->bulkSummaryFactoryMock + ); + } + + /** + * @dataProvider summaryDataProvider + * @param string $bulkUserId + * @param bool $expectedResult + */ + public function testIsAllowed($bulkUserId, $expectedResult) + { + $adminId = 1; + $uuid = 'test-001'; + $bulkSummaryMock = $this->createMock(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class); + + $this->bulkSummaryFactoryMock->expects($this->once())->method('create')->willReturn($bulkSummaryMock); + $this->entityManagerMock->expects($this->once()) + ->method('load') + ->with($bulkSummaryMock, $uuid) + ->willReturn($bulkSummaryMock); + + $bulkSummaryMock->expects($this->once())->method('getUserId')->willReturn($bulkUserId); + $this->userContextMock->expects($this->once())->method('getUserId')->willReturn($adminId); + + $this->assertEquals($this->model->isAllowed($uuid), $expectedResult); + } + + /** + * @return array + */ + public static function summaryDataProvider() + { + return [ + [2, false], + [1, true] + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkDescription/OptionsTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkDescription/OptionsTest.php new file mode 100644 index 0000000000000..d5835f7856dff --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkDescription/OptionsTest.php @@ -0,0 +1,73 @@ +bulkCollectionFactoryMock = $this->createPartialMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory::class, + ['create'] + ); + $this->userContextMock = $this->createMock(\Magento\Authorization\Model\UserContextInterface::class); + $this->model = new \Magento\AsynchronousOperations\Model\BulkDescription\Options( + $this->bulkCollectionFactoryMock, + $this->userContextMock + ); + } + + public function testToOptionsArray() + { + $userId = 100; + $collectionMock = $this->createMock(\Magento\AsynchronousOperations\Model\ResourceModel\Bulk\Collection::class); + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $this->bulkCollectionFactoryMock->expects($this->once())->method('create')->willReturn($collectionMock); + + $this->userContextMock->expects($this->once())->method('getUserId')->willReturn($userId); + + $collectionMock->expects($this->once())->method('getMainTable')->willReturn('table'); + + $selectMock->expects($this->once())->method('reset')->willReturnSelf(); + $selectMock->expects($this->once())->method('distinct')->with(true)->willReturnSelf(); + $selectMock->expects($this->once())->method('from')->with('table', ['description'])->willReturnSelf(); + $selectMock->expects($this->once())->method('where')->with('user_id = ?', $userId)->willReturnSelf(); + + $itemMock = $this->createPartialMock( + \Magento\AsynchronousOperations\Model\BulkSummary::class, + ['getDescription'] + ); + $itemMock->expects($this->exactly(2))->method('getDescription')->willReturn('description'); + + $collectionMock->expects($this->once())->method('getSelect')->willReturn($selectMock); + $collectionMock->expects($this->once())->method('getItems')->willReturn([$itemMock]); + + $expectedResult = [ + [ + 'value' => 'description', + 'label' => 'description' + ] + ]; + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php new file mode 100644 index 0000000000000..3a45c34df17f8 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php @@ -0,0 +1,296 @@ +entityManager = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityManager::class) + ->disableOriginalConstructor()->getMock(); + $this->bulkSummaryFactory = $this + ->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->operationCollectionFactory = $this + ->getMockBuilder(\Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->publisher = $this->getMockBuilder(\Magento\Framework\MessageQueue\BulkPublisherInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) + ->disableOriginalConstructor()->getMock(); + $this->logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor()->getMock(); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->bulkManagement = $objectManager->getObject( + \Magento\AsynchronousOperations\Model\BulkManagement::class, + [ + 'entityManager' => $this->entityManager, + 'bulkSummaryFactory' => $this->bulkSummaryFactory, + 'operationCollectionFactory' => $this->operationCollectionFactory, + 'publisher' => $this->publisher, + 'metadataPool' => $this->metadataPool, + 'resourceConnection' => $this->resourceConnection, + 'logger' => $this->logger, + ] + ); + } + + /** + * Test for scheduleBulk method. + * + * @return void + */ + public function testScheduleBulk() + { + $bulkUuid = 'bulk-001'; + $description = 'Bulk summary description...'; + $userId = 1; + $connectionName = 'default'; + $topicNames = ['topic.name.0', 'topic.name.1']; + $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) + ->disableOriginalConstructor()->getMock(); + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->willReturn($metadata); + $metadata->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection->expects($this->once()) + ->method('getConnectionByName')->with($connectionName)->willReturn($connection); + $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $bulkSummary = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->bulkSummaryFactory->expects($this->once())->method('create')->willReturn($bulkSummary); + $this->entityManager->expects($this->once()) + ->method('load')->with($bulkSummary, $bulkUuid)->willReturn($bulkSummary); + $bulkSummary->expects($this->once())->method('setBulkId')->with($bulkUuid)->willReturnSelf(); + $bulkSummary->expects($this->once())->method('setDescription')->with($description)->willReturnSelf(); + $bulkSummary->expects($this->once())->method('setUserId')->with($userId)->willReturnSelf(); + $bulkSummary->expects($this->once())->method('getOperationCount')->willReturn(1); + $bulkSummary->expects($this->once())->method('setOperationCount')->with(3)->willReturnSelf(); + $this->entityManager->expects($this->once())->method('save')->with($bulkSummary)->willReturn($bulkSummary); + $connection->expects($this->once())->method('commit')->willReturnSelf(); + $operation->expects($this->exactly(2))->method('getTopicName') + ->willReturnOnConsecutiveCalls($topicNames[0], $topicNames[1]); + $this->publisher->expects($this->exactly(2))->method('publish') + ->withConsecutive([$topicNames[0], [$operation]], [$topicNames[1], [$operation]])->willReturn(null); + $this->assertTrue( + $this->bulkManagement->scheduleBulk($bulkUuid, [$operation, $operation], $description, $userId) + ); + } + + /** + * Test for scheduleBulk method with exception. + * + * @return void + */ + public function testScheduleBulkWithException() + { + $bulkUuid = 'bulk-001'; + $description = 'Bulk summary description...'; + $userId = 1; + $connectionName = 'default'; + $exceptionMessage = 'Exception message'; + $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) + ->disableOriginalConstructor()->getMock(); + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->willReturn($metadata); + $metadata->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection->expects($this->once()) + ->method('getConnectionByName')->with($connectionName)->willReturn($connection); + $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $bulkSummary = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->bulkSummaryFactory->expects($this->once())->method('create')->willReturn($bulkSummary); + $this->entityManager->expects($this->once())->method('load') + ->with($bulkSummary, $bulkUuid)->willThrowException(new \LogicException($exceptionMessage)); + $connection->expects($this->once())->method('rollBack')->willReturnSelf(); + $this->logger->expects($this->once())->method('critical')->with($exceptionMessage); + $this->publisher->expects($this->never())->method('publish'); + $this->assertFalse($this->bulkManagement->scheduleBulk($bulkUuid, [$operation], $description, $userId)); + } + + /** + * Test for retryBulk method. + * + * @return void + */ + public function testRetryBulk() + { + $bulkUuid = 'bulk-001'; + $errorCodes = ['errorCode']; + $connectionName = 'default'; + $operationId = 1; + $operationTable = 'magento_operation'; + $topicName = 'topic.name'; + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->willReturn($metadata); + $metadata->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection->expects($this->once()) + ->method('getConnectionByName')->with($connectionName)->willReturn($connection); + $operationCollection = $this + ->getMockBuilder(\Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection::class) + ->disableOriginalConstructor()->getMock(); + $this->operationCollectionFactory->expects($this->once())->method('create')->willReturn($operationCollection); + $operationCollection->expects($this->exactly(2))->method('addFieldToFilter') + ->withConsecutive(['error_code', ['in' => $errorCodes]], ['bulk_uuid', ['eq' => $bulkUuid]]) + ->willReturnSelf(); + $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) + ->disableOriginalConstructor()->getMock(); + $operationCollection->expects($this->once())->method('getItems')->willReturn([$operation]); + $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $operation->expects($this->once())->method('getId')->willReturn($operationId); + $operation->expects($this->once())->method('setId')->with(null)->willReturnSelf(); + $this->resourceConnection->expects($this->once()) + ->method('getTableName')->with($operationTable)->willReturn($operationTable); + $connection->expects($this->once()) + ->method('quoteInto')->with('id IN (?)', [$operationId])->willReturn('id IN (' . $operationId .')'); + $connection->expects($this->once()) + ->method('delete')->with($operationTable, 'id IN (' . $operationId .')')->willReturn(1); + $connection->expects($this->once())->method('commit')->willReturnSelf(); + $operation->expects($this->once())->method('getTopicName')->willReturn($topicName); + $this->publisher->expects($this->once())->method('publish')->with($topicName, [$operation])->willReturn(null); + $this->assertEquals(1, $this->bulkManagement->retryBulk($bulkUuid, $errorCodes)); + } + + /** + * Test for retryBulk method with exception. + * + * @return void + */ + public function testRetryBulkWithException() + { + $bulkUuid = 'bulk-001'; + $errorCodes = ['errorCode']; + $connectionName = 'default'; + $operationId = 1; + $operationTable = 'magento_operation'; + $exceptionMessage = 'Exception message'; + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->willReturn($metadata); + $metadata->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection->expects($this->once()) + ->method('getConnectionByName')->with($connectionName)->willReturn($connection); + $operationCollection = $this + ->getMockBuilder(\Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection::class) + ->disableOriginalConstructor()->getMock(); + $this->operationCollectionFactory->expects($this->once())->method('create')->willReturn($operationCollection); + $operationCollection->expects($this->exactly(2))->method('addFieldToFilter') + ->withConsecutive(['error_code', ['in' => $errorCodes]], ['bulk_uuid', ['eq' => $bulkUuid]]) + ->willReturnSelf(); + $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) + ->disableOriginalConstructor()->getMock(); + $operationCollection->expects($this->once())->method('getItems')->willReturn([$operation]); + $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $operation->expects($this->once())->method('getId')->willReturn($operationId); + $operation->expects($this->once())->method('setId')->with(null)->willReturnSelf(); + $this->resourceConnection->expects($this->once()) + ->method('getTableName')->with($operationTable)->willReturn($operationTable); + $connection->expects($this->once()) + ->method('quoteInto')->with('id IN (?)', [$operationId])->willReturn('id IN (' . $operationId .')'); + $connection->expects($this->once()) + ->method('delete')->with($operationTable, 'id IN (' . $operationId .')') + ->willThrowException(new \Exception($exceptionMessage)); + $connection->expects($this->once())->method('rollBack')->willReturnSelf(); + $this->logger->expects($this->once())->method('critical')->with($exceptionMessage); + $this->publisher->expects($this->never())->method('publish'); + $this->assertEquals(0, $this->bulkManagement->retryBulk($bulkUuid, $errorCodes)); + } + + /** + * Test for deleteBulk method. + * + * @return void + */ + public function testDeleteBulk() + { + $bulkUuid = 'bulk-001'; + $bulkSummary = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->bulkSummaryFactory->expects($this->once())->method('create')->willReturn($bulkSummary); + $this->entityManager->expects($this->once()) + ->method('load')->with($bulkSummary, $bulkUuid)->willReturn($bulkSummary); + $this->entityManager->expects($this->once())->method('delete')->with($bulkSummary)->willReturn(true); + $this->assertTrue($this->bulkManagement->deleteBulk($bulkUuid)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php new file mode 100644 index 0000000000000..7a2f7941f9c04 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php @@ -0,0 +1,267 @@ +bulkCollectionFactory = $this->createPartialMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory::class, + ['create'] + ); + $this->operationCollectionFactory = $this->createPartialMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory::class, + ['create'] + ); + $this->operationMock = $this->createMock(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class); + $this->bulkMock = $this->createMock(\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class); + $this->resourceConnectionMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); + $this->calculatedStatusSqlMock = $this->createMock( + \Magento\AsynchronousOperations\Model\BulkStatus\CalculatedStatusSql::class + ); + $this->metadataPoolMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); + $this->bulkDetailedFactory = $this->createPartialMock( + \Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterfaceFactory ::class, + ['create'] + ); + $this->bulkShortFactory = $this->createPartialMock( + \Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterfaceFactory::class, + ['create'] + ); + $this->entityManager = $this->createMock(\Magento\Framework\EntityManager\EntityManager::class); + + $this->entityMetadataMock = $this->createMock(\Magento\Framework\EntityManager\EntityMetadataInterface::class); + $this->connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + + $this->model = new \Magento\AsynchronousOperations\Model\BulkStatus( + $this->bulkCollectionFactory, + $this->operationCollectionFactory, + $this->resourceConnectionMock, + $this->calculatedStatusSqlMock, + $this->metadataPoolMock, + $this->bulkDetailedFactory, + $this->bulkShortFactory, + $this->entityManager + ); + } + + /** + * @param int|null $failureType + * @param array $failureCodes + * @dataProvider getFailedOperationsByBulkIdDataProvider + */ + public function testGetFailedOperationsByBulkId($failureType, $failureCodes) + { + $bulkUuid = 'bulk-1'; + $operationCollection = $this->createMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection::class + ); + $this->operationCollectionFactory->expects($this->once())->method('create')->willReturn($operationCollection); + $operationCollection + ->expects($this->at(0)) + ->method('addFieldToFilter') + ->with('bulk_uuid', $bulkUuid) + ->willReturnSelf(); + $operationCollection + ->expects($this->at(1)) + ->method('addFieldToFilter') + ->with('status', $failureCodes) + ->willReturnSelf(); + $operationCollection->expects($this->once())->method('getItems')->willReturn([$this->operationMock]); + $this->assertEquals([$this->operationMock], $this->model->getFailedOperationsByBulkId($bulkUuid, $failureType)); + } + + public function testGetOperationsCountByBulkIdAndStatus() + { + $bulkUuid = 'bulk-1'; + $status = 1354; + $size = 32; + + $operationCollection = $this->createMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection::class + ); + $this->operationCollectionFactory->expects($this->once())->method('create')->willReturn($operationCollection); + $operationCollection + ->expects($this->at(0)) + ->method('addFieldToFilter') + ->with('bulk_uuid', $bulkUuid) + ->willReturnSelf(); + $operationCollection + ->expects($this->at(1)) + ->method('addFieldToFilter') + ->with('status', $status) + ->willReturnSelf(); + $operationCollection + ->expects($this->once()) + ->method('getSize') + ->willReturn($size); + $this->assertEquals($size, $this->model->getOperationsCountByBulkIdAndStatus($bulkUuid, $status)); + } + + public function getFailedOperationsByBulkIdDataProvider() + { + return [ + [1, [1]], + [ + null, + [ + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, + ], + ], + ]; + } + + public function testGetBulksByUser() + { + $userId = 1; + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $bulkCollection = $this->createMock(\Magento\AsynchronousOperations\Model\ResourceModel\Bulk\Collection::class); + $bulkCollection->expects($this->once())->method('getSelect')->willReturn($selectMock); + $selectMock->expects($this->once())->method('columns')->willReturnSelf(); + $selectMock->expects($this->once())->method('order')->willReturnSelf(); + $this->bulkCollectionFactory->expects($this->once())->method('create')->willReturn($bulkCollection); + $bulkCollection->expects($this->once())->method('addFieldToFilter')->with('user_id', $userId)->willReturnSelf(); + $bulkCollection->expects($this->once())->method('getItems')->willReturn([$this->bulkMock]); + $this->assertEquals([$this->bulkMock], $this->model->getBulksByUser($userId)); + } + + public function testGetBulksStatus() + { + $bulkUuid = 'bulk-1'; + $allProcessedOperationCollection = $this->createMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection::class + ); + + $completeOperationCollection = $this->createMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection::class + ); + + $connectionName = 'connection_name'; + $entityType = \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::class; + $this->metadataPoolMock + ->expects($this->once()) + ->method('getMetadata') + ->with($entityType) + ->willReturn($this->entityMetadataMock); + $this->entityMetadataMock + ->expects($this->once()) + ->method('getEntityConnectionName') + ->willReturn($connectionName); + $this->resourceConnectionMock + ->expects($this->once()) + ->method('getConnectionByName') + ->with($connectionName) + ->willReturn($this->connectionMock); + + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $selectMock->expects($this->once())->method('from')->willReturnSelf(); + $selectMock->expects($this->once())->method('where')->with('uuid = ?', $bulkUuid)->willReturnSelf(); + $this->connectionMock->expects($this->once())->method('select')->willReturn($selectMock); + $this->connectionMock->expects($this->once())->method('fetchOne')->with($selectMock)->willReturn(10); + + $this->operationCollectionFactory + ->expects($this->at(0)) + ->method('create') + ->willReturn($allProcessedOperationCollection); + $this->operationCollectionFactory + ->expects($this->at(1)) + ->method('create') + ->willReturn($completeOperationCollection); + $allProcessedOperationCollection + ->expects($this->once()) + ->method('addFieldToFilter') + ->with('bulk_uuid', $bulkUuid) + ->willReturnSelf(); + $allProcessedOperationCollection->expects($this->once())->method('getSize')->willReturn(5); + + $completeOperationCollection + ->expects($this->at(0)) + ->method('addFieldToFilter') + ->with('bulk_uuid', $bulkUuid) + ->willReturnSelf(); + $completeOperationCollection + ->expects($this->at(1)) + ->method('addFieldToFilter') + ->with('status', OperationInterface::STATUS_TYPE_COMPLETE) + ->willReturnSelf(); + $completeOperationCollection->expects($this->any())->method('getSize')->willReturn(5); + $this->assertEquals(BulkSummaryInterface::IN_PROGRESS, $this->model->getBulkStatus($bulkUuid)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Entity/BulkSummaryMapperTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Entity/BulkSummaryMapperTest.php new file mode 100644 index 0000000000000..725eae3c01ea3 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Entity/BulkSummaryMapperTest.php @@ -0,0 +1,105 @@ +metadataPoolMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); + $this->resourceConnectionMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); + $this->entityMetadataMock = $this->createMock(\Magento\Framework\EntityManager\EntityMetadataInterface::class); + $this->connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + $this->selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $this->model = new BulkSummaryMapper( + $this->metadataPoolMock, + $this->resourceConnectionMock + ); + } + + /** + * @param int $identifier + * @param array|false $result + * @dataProvider entityToDatabaseDataProvider + */ + public function testEntityToDatabase($identifier, $result) + { + $entityType = 'entityType'; + $data = ['uuid' => 'bulk-1']; + $connectionName = 'connection_name'; + $entityTable = 'table_name'; + $this->metadataPoolMock + ->expects($this->once()) + ->method('getMetadata') + ->with($entityType) + ->willReturn($this->entityMetadataMock); + $this->entityMetadataMock + ->expects($this->once()) + ->method('getEntityConnectionName') + ->willReturn($connectionName); + + $this->resourceConnectionMock + ->expects($this->once()) + ->method('getConnectionByName') + ->with($connectionName) + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once())->method('select')->willReturn($this->selectMock); + $this->entityMetadataMock->expects($this->once())->method('getEntityTable')->willReturn($entityTable); + $this->selectMock->expects($this->once())->method('from')->with($entityTable, 'id')->willReturnSelf(); + $this->selectMock->expects($this->once())->method('where')->with("uuid = ?", 'bulk-1')->willReturnSelf(); + $this->connectionMock + ->expects($this->once()) + ->method('fetchOne') + ->with($this->selectMock) + ->willReturn($identifier); + + $this->assertEquals($result, $this->model->entityToDatabase($entityType, $data)); + } + + public function entityToDatabaseDataProvider() + { + return [ + [1, ['uuid' => 'bulk-1', 'id' => 1]], + [false, ['uuid' => 'bulk-1']] + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Operation/DetailsTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Operation/DetailsTest.php new file mode 100644 index 0000000000000..f62e2b7f9d5ea --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Operation/DetailsTest.php @@ -0,0 +1,61 @@ +bulkStatusMock = $this->getMockBuilder(\Magento\Framework\Bulk\BulkStatusInterface::class) + ->getMock(); + $this->model = new \Magento\AsynchronousOperations\Model\Operation\Details($this->bulkStatusMock); + } + + public function testGetDetails() + { + $uuid = 'some_uuid_string'; + $completed = 100; + $failedRetriable = 23; + $failedNotRetriable = 45; + $open = 303; + $rejected = 0; + + $expectedResult = [ + 'operations_total' => $completed + $failedRetriable + $failedNotRetriable + $open, + 'operations_successful' => $completed, + 'operations_failed' => $failedRetriable + $failedNotRetriable, + 'failed_retriable' => $failedRetriable, + 'failed_not_retriable' => $failedNotRetriable, + 'rejected' => $rejected, + 'open' => $open, + ]; + + $this->bulkStatusMock->method('getOperationsCountByBulkIdAndStatus') + ->willReturnMap([ + [$uuid, OperationInterface::STATUS_TYPE_COMPLETE, $completed], + [$uuid, OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, $failedRetriable], + [$uuid, OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, $failedNotRetriable], + [$uuid, OperationInterface::STATUS_TYPE_OPEN, $open], + [$uuid, OperationInterface::STATUS_TYPE_REJECTED, $rejected], + ]); + + $result = $this->model->getDetails($uuid); + $this->assertEquals($expectedResult, $result); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php new file mode 100644 index 0000000000000..0a4e5f2f3ecc3 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php @@ -0,0 +1,90 @@ +entityManagerMock = $this->createMock(\Magento\Framework\EntityManager\EntityManager::class); + $this->metadataPoolMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); + $this->operationFactoryMock = $this->createPartialMock( + \Magento\AsynchronousOperations\Api\Data\DetailedOperationStatusInterfaceFactory::class, + ['create'] + ); + $this->operationMock = + $this->createMock(\Magento\AsynchronousOperations\Api\Data\DetailedOperationStatusInterface::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); + $this->model = new \Magento\AsynchronousOperations\Model\OperationManagement( + $this->entityManagerMock, + $this->operationFactoryMock, + $this->loggerMock + ); + } + + public function testChangeOperationStatus() + { + $operationId = 1; + $status = 1; + $message = 'Message'; + $data = 'data'; + $errorCode = 101; + $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($this->operationMock); + $this->entityManagerMock->expects($this->once())->method('load')->with($this->operationMock, $operationId); + $this->operationMock->expects($this->once())->method('setStatus')->with($status)->willReturnSelf(); + $this->operationMock->expects($this->once())->method('setResultMessage')->with($message)->willReturnSelf(); + $this->operationMock->expects($this->once())->method('setSerializedData')->with($data)->willReturnSelf(); + $this->operationMock->expects($this->once())->method('setErrorCode')->with($errorCode)->willReturnSelf(); + $this->entityManagerMock->expects($this->once())->method('save')->with($this->operationMock); + $this->assertTrue($this->model->changeOperationStatus($operationId, $status, $errorCode, $message, $data)); + } + + public function testChangeOperationStatusIfExceptionWasThrown() + { + $operationId = 1; + $status = 1; + $message = 'Message'; + $data = 'data'; + $errorCode = 101; + $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($this->operationMock); + $this->entityManagerMock->expects($this->once())->method('load')->with($this->operationMock, $operationId); + $this->operationMock->expects($this->once())->method('setStatus')->with($status)->willReturnSelf(); + $this->operationMock->expects($this->once())->method('setResultMessage')->with($message)->willReturnSelf(); + $this->operationMock->expects($this->once())->method('setSerializedData')->with($data)->willReturnSelf(); + $this->operationMock->expects($this->once())->method('setErrorCode')->with($errorCode)->willReturnSelf(); + $this->entityManagerMock->expects($this->once())->method('save')->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once())->method('critical'); + $this->assertFalse($this->model->changeOperationStatus($operationId, $status, $errorCode, $message, $data)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/Operation/CreateTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/Operation/CreateTest.php new file mode 100644 index 0000000000000..2f0fc8ceba46f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/Operation/CreateTest.php @@ -0,0 +1,134 @@ +metadataPool = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) + ->disableOriginalConstructor()->getMock(); + $this->typeResolver = $this->getMockBuilder(\Magento\Framework\EntityManager\TypeResolver::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) + ->disableOriginalConstructor()->getMock(); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->create = $objectManager->getObject( + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Create::class, + [ + 'metadataPool' => $this->metadataPool, + 'typeResolver' => $this->typeResolver, + 'resourceConnection' => $this->resourceConnection, + ] + ); + } + + /** + * Test for execute method. + * + * @return void + */ + public function testExecute() + { + $connectionName = 'default'; + $operationData = ['key1' => 'value1']; + $operationTable = 'magento_operation'; + $operationList = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationListInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->typeResolver->expects($this->once())->method('resolve')->with($operationList) + ->willReturn(\Magento\AsynchronousOperations\Api\Data\OperationListInterface::class); + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(\Magento\AsynchronousOperations\Api\Data\OperationListInterface::class)->willReturn($metadata); + $metadata->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection->expects($this->once()) + ->method('getConnection')->with($connectionName)->willReturn($connection); + $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) + ->setMethods(['getData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $operationList->expects($this->once())->method('getItems')->willReturn([$operation]); + $operation->expects($this->once())->method('getData')->willReturn($operationData); + $metadata->expects($this->once())->method('getEntityTable')->willReturn($operationTable); + $connection->expects($this->once())->method('insertOnDuplicate') + ->with($operationTable, [$operationData], ['status', 'error_code', 'result_message'])->willReturn(1); + $connection->expects($this->once())->method('commit')->willReturnSelf(); + $this->assertEquals($operationList, $this->create->execute($operationList)); + } + + /** + * Test for execute method with exception. + * + * @return void + * @expectedException \Exception + */ + public function testExecuteWithException() + { + $connectionName = 'default'; + $operationData = ['key1' => 'value1']; + $operationTable = 'magento_operation'; + $operationList = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationListInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->typeResolver->expects($this->once())->method('resolve')->with($operationList) + ->willReturn(\Magento\AsynchronousOperations\Api\Data\OperationListInterface::class); + $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(\Magento\AsynchronousOperations\Api\Data\OperationListInterface::class)->willReturn($metadata); + $metadata->expects($this->once())->method('getEntityConnectionName')->willReturn($connectionName); + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->resourceConnection->expects($this->once()) + ->method('getConnection')->with($connectionName)->willReturn($connection); + $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); + $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) + ->setMethods(['getData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $operationList->expects($this->once())->method('getItems')->willReturn([$operation]); + $operation->expects($this->once())->method('getData')->willReturn($operationData); + $metadata->expects($this->once())->method('getEntityTable')->willReturn($operationTable); + $connection->expects($this->once())->method('insertOnDuplicate') + ->with($operationTable, [$operationData], ['status', 'error_code', 'result_message']) + ->willThrowException(new \Exception()); + $connection->expects($this->once())->method('rollBack')->willReturnSelf(); + $this->create->execute($operationList); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php new file mode 100644 index 0000000000000..68864d12e7672 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php @@ -0,0 +1,169 @@ +messagefactoryMock = $this->createPartialMock( + \Magento\AdminNotification\Model\System\MessageFactory::class, + ['create'] + ); + $this->bulkStatusMock = $this->createMock(BulkStatusInterface::class); + + $this->userContextMock = $this->createMock(UserContextInterface::class); + $this->operationsDetailsMock = $this->createMock(Details::class); + $this->authorizationMock = $this->createMock(AuthorizationInterface::class); + $this->messageMock = $this->createMock(\Magento\AdminNotification\Model\System\Message::class); + $this->collectionMock = $this->createMock(Synchronized::class); + $this->bulkNotificationMock = $this->createMock(BulkNotificationManagement::class); + $this->statusMapper = $this->createMock(\Magento\AsynchronousOperations\Model\StatusMapper::class); + $this->plugin = new Plugin( + $this->messagefactoryMock, + $this->bulkStatusMock, + $this->bulkNotificationMock, + $this->userContextMock, + $this->operationsDetailsMock, + $this->authorizationMock, + $this->statusMapper + ); + } + + public function testAfterToArrayIfNotAllowed() + { + $result = []; + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with($this->resourceName) + ->willReturn(false); + $this->assertEquals($result, $this->plugin->afterToArray($this->collectionMock, $result)); + } + + /** + * @param array $operationDetails + * @dataProvider afterToDataProvider + */ + public function testAfterTo($operationDetails) + { + $methods = ['getBulkId', 'getDescription', 'getStatus', 'getStartTime']; + $bulkMock = $this->createPartialMock(\Magento\AsynchronousOperations\Model\BulkSummary::class, $methods); + $result = ['items' =>[], 'totalRecords' => 1]; + $userBulks = [$bulkMock]; + $userId = 1; + $bulkUuid = 2; + $bulkArray = [ + 'status' => \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface::NOT_STARTED + ]; + $bulkMock->expects($this->once())->method('getBulkId')->willReturn($bulkUuid); + $this->operationsDetailsMock + ->expects($this->once()) + ->method('getDetails') + ->with($bulkUuid) + ->willReturn($operationDetails); + $bulkMock->expects($this->once())->method('getDescription')->willReturn('Bulk Description'); + $this->messagefactoryMock->expects($this->once())->method('create')->willReturn($this->messageMock); + $this->messageMock->expects($this->once())->method('toArray')->willReturn($bulkArray); + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with($this->resourceName) + ->willReturn(true); + $this->userContextMock->expects($this->once())->method('getUserId')->willReturn($userId); + $this->bulkNotificationMock + ->expects($this->once()) + ->method('getAcknowledgedBulksByUser') + ->with($userId) + ->willReturn([]); + $this->statusMapper->expects($this->once())->method('operationStatusToBulkSummaryStatus'); + $this->bulkStatusMock->expects($this->once())->method('getBulksByUser')->willReturn($userBulks); + $result2 = $this->plugin->afterToArray($this->collectionMock, $result); + $this->assertEquals(2, $result2['totalRecords']); + } + + public function afterToDataProvider() + { + return [ + ['operations_successful' => 0, + 'operations_failed' => 0, + 'operations_total' => 10 + ], + ['operations_successful' => 1, + 'operations_failed' => 2, + 'operations_total' => 10 + ], + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/StatusMapperTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/StatusMapperTest.php new file mode 100644 index 0000000000000..89fa80de36378 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/StatusMapperTest.php @@ -0,0 +1,91 @@ +model = new \Magento\AsynchronousOperations\Model\StatusMapper(); + } + + public function testOperationStatusToBulkSummaryStatus() + { + $this->assertEquals( + $this->model->operationStatusToBulkSummaryStatus(OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED), + BulkSummaryInterface::FINISHED_WITH_FAILURE + ); + + $this->assertEquals( + $this->model->operationStatusToBulkSummaryStatus(OperationInterface::STATUS_TYPE_RETRIABLY_FAILED), + BulkSummaryInterface::FINISHED_WITH_FAILURE + ); + + $this->assertEquals( + $this->model->operationStatusToBulkSummaryStatus(OperationInterface::STATUS_TYPE_COMPLETE), + BulkSummaryInterface::FINISHED_SUCCESSFULLY + ); + + $this->assertEquals( + $this->model->operationStatusToBulkSummaryStatus(OperationInterface::STATUS_TYPE_OPEN), + BulkSummaryInterface::IN_PROGRESS + ); + + $this->assertEquals( + $this->model->operationStatusToBulkSummaryStatus(0), + BulkSummaryInterface::NOT_STARTED + ); + } + + public function testOperationStatusToBulkSummaryStatusWithUnknownStatus() + { + $this->assertNull($this->model->operationStatusToBulkSummaryStatus('unknown_status')); + } + + public function testBulkSummaryStatusToOperationStatus() + { + $this->assertEquals( + $this->model->bulkSummaryStatusToOperationStatus(BulkSummaryInterface::FINISHED_SUCCESSFULLY), + OperationInterface::STATUS_TYPE_COMPLETE + ); + + $this->assertEquals( + $this->model->bulkSummaryStatusToOperationStatus(BulkSummaryInterface::IN_PROGRESS), + OperationInterface::STATUS_TYPE_OPEN + ); + + $this->assertEquals( + $this->model->bulkSummaryStatusToOperationStatus(BulkSummaryInterface::FINISHED_WITH_FAILURE), + [ + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, + OperationInterface::STATUS_TYPE_REJECTED + ] + ); + + $this->assertEquals( + $this->model->bulkSummaryStatusToOperationStatus(BulkSummaryInterface::NOT_STARTED), + 0 + ); + } + + public function testBulkSummaryStatusToOperationStatusWithUnknownStatus() + { + $this->assertNull($this->model->bulkSummaryStatusToOperationStatus('unknown_status')); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/AdminNotification/PluginTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/AdminNotification/PluginTest.php new file mode 100644 index 0000000000000..cc0b3a3da38a7 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/AdminNotification/PluginTest.php @@ -0,0 +1,48 @@ +authorizationMock = $this->createMock(AuthorizationInterface::class); + $this->plugin = new \Magento\AsynchronousOperations\Ui\Component\AdminNotification\Plugin( + $this->authorizationMock + ); + } + + public function testAfterGetMeta() + { + $result = []; + $expectedResult = [ + 'columns' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'isAllowed' => true + ] + ] + ] + ] + ]; + $dataProviderMock = $this->createMock(\Magento\AdminNotification\Ui\Component\DataProvider\DataProvider::class); + $this->authorizationMock->expects($this->once())->method('isAllowed')->willReturn(true); + $this->assertEquals($expectedResult, $this->plugin->afterGetMeta($dataProviderMock, $result)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php new file mode 100644 index 0000000000000..f5cce7af943a1 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php @@ -0,0 +1,76 @@ +context = $this->createMock(\Magento\Framework\View\Element\UiComponent\ContextInterface::class); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $processor = $this->createPartialMock( + \Magento\Framework\View\Element\UiComponent\Processor::class, + ['getProcessor'] + ); + $this->context->expects($this->never())->method('getProcessor')->will($this->returnValue($processor)); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->actionColumn = $objectManager->getObject( + \Magento\AsynchronousOperations\Ui\Component\Listing\Column\Actions::class, + [ + 'context' => $this->context, + 'uiComponentFactory' => $this->uiComponentFactory, + 'components' => [], + 'data' => ['name' => 'Edit'], + 'editUrl' => '' + ] + ); + } + + /** + * Test for method prepareDataSource + */ + public function testPrepareDataSource() + { + $href = 'bulk/bulk/details/id/bulk-1'; + $this->context->expects($this->once())->method('getUrl')->with( + 'bulk/bulk/details', + ['uuid' => 'bulk-1'] + )->willReturn($href); + $dataSource['data']['items']['item'] = [BulkSummary::BULK_ID => 'bulk-1']; + $actionColumn['data']['items']['item'] = [ + 'Edit' => [ + 'edit' => [ + 'href' => $href, + 'label' => __('Details'), + 'hidden' => false + ] + ] + ]; + $expectedResult = array_merge_recursive($dataSource, $actionColumn); + $this->assertEquals($expectedResult, $this->actionColumn->prepareDataSource($dataSource)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/NotificationActionsTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/NotificationActionsTest.php new file mode 100644 index 0000000000000..a35fd82774148 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/NotificationActionsTest.php @@ -0,0 +1,132 @@ +context = $this->createMock(\Magento\Framework\View\Element\UiComponent\ContextInterface::class); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $processor = $this->createPartialMock( + \Magento\Framework\View\Element\UiComponent\Processor::class, + ['getProcessor'] + ); + $this->context->expects($this->never())->method('getProcessor')->will($this->returnValue($processor)); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->actionColumn = $objectManager->getObject( + \Magento\AsynchronousOperations\Ui\Component\Listing\Column\NotificationActions::class, + [ + 'context' => $this->context, + 'uiComponentFactory' => $this->uiComponentFactory, + 'components' => [], + 'data' => ['name' => 'actions'] + ] + ); + } + + public function testPrepareDataSource() + { + $testData['data']['items'] = [ + [ + 'key' => 'value', + ], + [ + BulkSummary::BULK_ID => 'uuid-1', + 'status' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, + ], + [ + BulkSummary::BULK_ID => 'uuid-2', + ], + ]; + $expectedResult['data']['items'] = [ + [ + 'key' => 'value', + ], + [ + BulkSummary::BULK_ID => 'uuid-1', + 'status' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, + 'actions' => [ + 'details' => [ + 'href' => '#', + 'label' => __('View Details'), + 'callback' => [ + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal.insertBulk', + 'target' => 'destroyInserted', + ], + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal.insertBulk', + 'target' => 'updateData', + 'params' => [ + BulkSummary::BULK_ID => 'uuid-1', + ], + ], + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal', + 'target' => 'openModal', + ], + [ + 'provider' => 'ns = notification_area, index = columns', + 'target' => 'dismiss', + 'params' => ['uuid-1'], + ], + ], + ], + ], + ], + [ + BulkSummary::BULK_ID => 'uuid-2', + 'actions' => [ + 'details' => [ + 'href' => '#', + 'label' => __('View Details'), + 'callback' => [ + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal.insertBulk', + 'target' => 'destroyInserted', + ], + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal.insertBulk', + 'target' => 'updateData', + 'params' => [ + BulkSummary::BULK_ID => 'uuid-2', + ], + ], + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal', + 'target' => 'openModal', + ], + ], + ], + ], + ], + ]; + $this->assertEquals($expectedResult, $this->actionColumn->prepareDataSource($testData)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/NotificationDismissActionsTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/NotificationDismissActionsTest.php new file mode 100644 index 0000000000000..cf1f0db58dfdf --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Listing/Column/NotificationDismissActionsTest.php @@ -0,0 +1,96 @@ +context = $this->createMock(\Magento\Framework\View\Element\UiComponent\ContextInterface::class); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $processor = $this->createPartialMock( + \Magento\Framework\View\Element\UiComponent\Processor::class, + ['getProcessor'] + ); + $this->context->expects($this->never())->method('getProcessor')->will($this->returnValue($processor)); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->actionColumn = $objectManager->getObject( + \Magento\AsynchronousOperations\Ui\Component\Listing\Column\NotificationDismissActions::class, + [ + 'context' => $this->context, + 'uiComponentFactory' => $this->uiComponentFactory, + 'components' => [], + 'data' => ['name' => 'actions'] + ] + ); + } + + public function testPrepareDataSource() + { + $testData['data']['items'] = [ + [ + 'key' => 'value', + ], + [ + BulkSummary::BULK_ID => 'uuid-1', + 'status' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, + ], + [ + 'status' => BulkSummaryInterface::IN_PROGRESS, + ], + ]; + $expectedResult['data']['items'] = [ + [ + 'key' => 'value', + ], + [ + BulkSummary::BULK_ID => 'uuid-1', + 'status' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, + 'actions' => [ + 'dismiss' => [ + 'href' => '#', + 'label' => __('Dismiss'), + 'callback' => [ + [ + 'provider' => 'ns = notification_area, index = columns', + 'target' => 'dismiss', + 'params' => [ + 0 => 'uuid-1', + ], + ], + ], + ], + ], + ], + [ + 'status' => BulkSummaryInterface::IN_PROGRESS, + ], + ]; + $this->assertEquals($expectedResult, $this->actionColumn->prepareDataSource($testData)); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Operation/DataProviderTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Operation/DataProviderTest.php new file mode 100644 index 0000000000000..bc1e4bcd7e3e2 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Ui/Component/Operation/DataProviderTest.php @@ -0,0 +1,143 @@ +bulkCollectionFactoryMock = $this->createPartialMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory::class, + ['create'] + ); + $this->bulkCollectionMock = $this->createMock( + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\Collection::class + ); + $this->operationDetailsMock = $this->createMock(\Magento\AsynchronousOperations\Model\Operation\Details::class); + $this->bulkMock = $this->createMock(\Magento\AsynchronousOperations\Model\BulkSummary::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + + $this->bulkCollectionFactoryMock + ->expects($this->once()) + ->method('create') + ->willReturn($this->bulkCollectionMock); + + $this->dataProvider = $helper->getObject( + \Magento\AsynchronousOperations\Ui\Component\Operation\DataProvider::class, + [ + 'name' => 'test-name', + 'bulkCollectionFactory' => $this->bulkCollectionFactoryMock, + 'operationDetails' => $this->operationDetailsMock, + 'request' => $this->requestMock + ] + ); + } + + public function testGetData() + { + $testData = [ + 'id' => '1', + 'uuid' => 'bulk-uuid1', + 'user_id' => '2', + 'description' => 'Description' + ]; + $testOperationData = [ + 'operations_total' => 2, + 'operations_successful' => 1, + 'operations_failed' => 2 + ]; + $testSummaryData = [ + 'summary' => '2 items selected for mass update, 1 successfully updated, 2 failed to update' + ]; + $resultData[$testData['id']] = array_merge($testData, $testOperationData, $testSummaryData); + + $this->bulkCollectionMock + ->expects($this->once()) + ->method('getItems') + ->willReturn([$this->bulkMock]); + $this->bulkMock + ->expects($this->once()) + ->method('getData') + ->willReturn($testData); + $this->operationDetailsMock + ->expects($this->once()) + ->method('getDetails') + ->with($testData['uuid']) + ->willReturn($testOperationData); + $this->bulkMock + ->expects($this->once()) + ->method('getBulkId') + ->willReturn($testData['id']); + + $expectedResult = $this->dataProvider->getData(); + $this->assertEquals($resultData, $expectedResult); + } + + public function testPrepareMeta() + { + $resultData['retriable_operations']['arguments']['data']['disabled'] = true; + $resultData['failed_operations']['arguments']['data']['disabled'] = true; + $testData = [ + 'uuid' => 'bulk-uuid1', + 'failed_retriable' => 0, + 'failed_not_retriable' => 0 + ]; + + $this->requestMock + ->expects($this->once()) + ->method('getParam') + ->willReturn($testData['uuid']); + $this->operationDetailsMock + ->expects($this->once()) + ->method('getDetails') + ->with($testData['uuid']) + ->willReturn($testData); + + $expectedResult = $this->dataProvider->prepareMeta([]); + $this->assertEquals($resultData, $expectedResult); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/AdminNotification/Plugin.php b/app/code/Magento/AsynchronousOperations/Ui/Component/AdminNotification/Plugin.php new file mode 100644 index 0000000000000..b5670639dce09 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/AdminNotification/Plugin.php @@ -0,0 +1,54 @@ +authorization = $authorization; + } + + /** + * Prepares Meta + * + * @param \Magento\AdminNotification\Ui\Component\DataProvider\DataProvider $dataProvider + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetMeta( + \Magento\AdminNotification\Ui\Component\DataProvider\DataProvider $dataProvider, + $result + ) { + if (!isset($this->isAllowed)) { + $this->isAllowed = $this->authorization->isAllowed( + 'Magento_Logging::system_magento_logging_bulk_operations' + ); + } + $result['columns']['arguments']['data']['config']['isAllowed'] = $this->isAllowed; + return $result; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Bulk/IdentifierResolver.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Bulk/IdentifierResolver.php new file mode 100644 index 0000000000000..b5b7da1318001 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Bulk/IdentifierResolver.php @@ -0,0 +1,36 @@ +request = $request; + } + + /** + * @return null|string + */ + public function execute() + { + return $this->request->getParam('uuid'); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Operation/Failed/SearchResult.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Operation/Failed/SearchResult.php new file mode 100644 index 0000000000000..aba7554c26d1d --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Operation/Failed/SearchResult.php @@ -0,0 +1,142 @@ +jsonHelper = $jsonHelper; + $this->identifierResolver = $identifierResolver; + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName + ); + } + + /** + * {@inheritdoc} + */ + protected function _initSelect() + { + $bulkUuid = $this->identifierResolver->execute(); + $this->getSelect()->from(['main_table' => $this->getMainTable()], ['id', 'result_message', 'serialized_data']) + ->where('bulk_uuid=?', $bulkUuid) + ->where('status=?', OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED); + return $this; + } + + /** + * {@inheritdoc} + */ + protected function _afterLoad() + { + parent::_afterLoad(); + foreach ($this->_items as $key => $item) { + try { + $unserializedData = $this->jsonHelper->jsonDecode($item['serialized_data']); + } catch (\Exception $e) { + $this->_logger->error($e->getMessage()); + $unserializedData = []; + } + $this->_items[$key]->setData('meta_information', $this->provideMetaInfo($unserializedData)); + $this->_items[$key]->setData('link', $this->getLink($unserializedData)); + $this->_items[$key]->setData('entity_id', $this->getEntityId($unserializedData)); + } + return $this; + } + + /** + * Provide meta info by serialized data + * + * @param array $item + * @return string + */ + private function provideMetaInfo($item) + { + $metaInfo = ''; + if (isset($item['meta_information'])) { + $metaInfo = $item['meta_information']; + } + return $metaInfo; + } + + /** + * Get link from serialized data + * + * @param array $item + * @return string + */ + private function getLink($item) + { + $entityLink = ''; + if (isset($item['entity_link'])) { + $entityLink = $item['entity_link']; + } + return $entityLink; + } + + /** + * Get entity id from serialized data + * + * @param array $item + * @return string + */ + private function getEntityId($item) + { + $entityLink = ''; + if (isset($item['entity_id'])) { + $entityLink = $item['entity_id']; + } + return $entityLink; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Operation/Retriable/SearchResult.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Operation/Retriable/SearchResult.php new file mode 100644 index 0000000000000..9641bd1333f9f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/Operation/Retriable/SearchResult.php @@ -0,0 +1,71 @@ +identifierResolver = $identifierResolver; + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName + ); + } + + /** + * {@inheritdoc} + */ + protected function _initSelect() + { + $bulkUuid = $this->identifierResolver->execute(); + $this->getSelect()->from(['main_table' => $this->getMainTable()], ['id', 'result_message', 'error_code']) + ->where('bulk_uuid=?', $bulkUuid) + ->where('status=?', OperationInterface::STATUS_TYPE_RETRIABLY_FAILED) + ->group('error_code') + ->columns(['records_qty' => new \Zend_Db_Expr('COUNT(id)')]); + return $this; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php new file mode 100644 index 0000000000000..5f2fbd9ea8b11 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php @@ -0,0 +1,148 @@ +userContext = $userContextInterface; + $this->statusMapper = $statusMapper; + $this->calculatedStatusSql = $calculatedStatusSql; + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName + ); + } + + /** + * {@inheritdoc} + */ + protected function _initSelect() + { + $this->getSelect()->from( + ['main_table' => $this->getMainTable()], + [ + '*', + 'status' => $this->calculatedStatusSql->get($this->getTable('magento_operation')) + ] + )->where( + 'user_id=?', + $this->userContext->getUserId() + ); + return $this; + } + + /** + * {@inheritdoc} + */ + protected function _afterLoad() + { + /** @var BulkSummaryInterface $item */ + foreach ($this->getItems() as $item) { + $item->setStatus($this->statusMapper->operationStatusToBulkSummaryStatus($item->getStatus())); + } + return parent::_afterLoad(); + } + + /** + * {@inheritdoc} + */ + public function addFieldToFilter($field, $condition = null) + { + if ($field == 'status') { + if (is_array($condition)) { + foreach ($condition as $value) { + $this->operationStatus = $this->statusMapper->bulkSummaryStatusToOperationStatus($value); + if (is_array($this->operationStatus)) { + foreach ($this->operationStatus as $statusValue) { + $this->getSelect()->orHaving('status = ?', $statusValue); + } + continue; + } + $this->getSelect()->having('status = ?', $this->operationStatus); + } + } + return $this; + } + return parent::addFieldToFilter($field, $condition); + } + + /** + * {@inheritdoc} + */ + public function getSelectCountSql() + { + $select = parent::getSelectCountSql(); + $select->columns(['status' => $this->calculatedStatusSql->get($this->getTable('magento_operation'))]); + //add grouping by status if filtering by status was executed + if (isset($this->operationStatus)) { + $select->group('status'); + } + return $select; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/Actions.php b/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/Actions.php new file mode 100644 index 0000000000000..232f8ca1356be --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/Actions.php @@ -0,0 +1,43 @@ +getData('name')]['edit'] = [ + 'href' => $this->context->getUrl( + 'bulk/bulk/details', + ['uuid' => $item['uuid']] + ), + 'label' => __('Details'), + 'hidden' => false, + ]; + } + + return $dataSource; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/NotificationActions.php b/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/NotificationActions.php new file mode 100644 index 0000000000000..1886bbf430bc7 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/NotificationActions.php @@ -0,0 +1,68 @@ +getData('name')]['details'] = [ + 'callback' => [ + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal.insertBulk', + 'target' => 'destroyInserted', + ], + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal.insertBulk', + 'target' => 'updateData', + 'params' => [ + 'uuid' => $item['uuid'], + ], + ], + [ + 'provider' => 'notification_area.notification_area.modalContainer.modal', + 'target' => 'openModal', + ], + ], + 'href' => '#', + 'label' => __('View Details'), + ]; + + if (isset($item['status']) + && ($item['status'] === BulkSummaryInterface::FINISHED_SUCCESSFULLY + || $item['status'] === BulkSummaryInterface::FINISHED_WITH_FAILURE) + ) { + $item[$this->getData('name')]['details']['callback'][] = [ + 'provider' => 'ns = notification_area, index = columns', + 'target' => 'dismiss', + 'params' => [ + 0 => $item['uuid'], + ], + ]; + } + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/NotificationDismissActions.php b/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/NotificationDismissActions.php new file mode 100644 index 0000000000000..cae2524f92600 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/Listing/Column/NotificationDismissActions.php @@ -0,0 +1,50 @@ +getData('name')]['dismiss'] = [ + 'callback' => [ + [ + 'provider' => 'ns = notification_area, index = columns', + 'target' => 'dismiss', + 'params' => [ + 0 => $item['uuid'], + ], + ], + ], + 'href' => '#', + 'label' => __('Dismiss'), + ]; + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/Operation/DataProvider.php b/app/code/Magento/AsynchronousOperations/Ui/Component/Operation/DataProvider.php new file mode 100644 index 0000000000000..89aae531fec4e --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/Operation/DataProvider.php @@ -0,0 +1,124 @@ +collection = $bulkCollectionFactory->create(); + $this->operationDetails = $operationDetails; + $this->request = $request; + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + $this->meta = $this->prepareMeta($this->meta); + } + + /** + * Human readable summary for bulk + * + * @param array $operationDetails structure is implied as getOperationDetails() result + * @return string + */ + private function getSummaryReport($operationDetails) + { + if (0 == $operationDetails['operations_successful'] && 0 == $operationDetails['operations_failed']) { + return __('Pending, in queue...'); + } + + $summaryReport = __('%1 items selected for mass update', $operationDetails['operations_total'])->__toString(); + if ($operationDetails['operations_successful'] > 0) { + $summaryReport .= __(', %1 successfully updated', $operationDetails['operations_successful']); + } + + if ($operationDetails['operations_failed'] > 0) { + $summaryReport .= __(', %1 failed to update', $operationDetails['operations_failed']); + } + + return $summaryReport; + } + + /** + * Bulk summary with operation statistics + * + * @return array + */ + public function getData() + { + $data = []; + $items = $this->collection->getItems(); + if (count($items) == 0) { + return $data; + } + $bulk = array_shift($items); + /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulk */ + $data = $bulk->getData(); + $operationDetails = $this->operationDetails->getDetails($data['uuid']); + $data['summary'] = $this->getSummaryReport($operationDetails); + $data = array_merge($data, $operationDetails); + + return [$bulk->getBulkId() => $data]; + } + + /** + * Prepares Meta + * + * @param array $meta + * @return array + */ + public function prepareMeta($meta) + { + $requestId = $this->request->getParam($this->requestFieldName); + $operationDetails = $this->operationDetails->getDetails($requestId); + + if (isset($operationDetails['failed_retriable']) && !$operationDetails['failed_retriable']) { + $meta['retriable_operations']['arguments']['data']['disabled'] = true; + } + + if (isset($operationDetails['failed_not_retriable']) && !$operationDetails['failed_not_retriable']) { + $meta['failed_operations']['arguments']['data']['disabled'] = true; + } + + return $meta; + } +} diff --git a/app/code/Magento/AsynchronousOperations/composer.json b/app/code/Magento/AsynchronousOperations/composer.json new file mode 100644 index 0000000000000..3acb92710e62b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/composer.json @@ -0,0 +1,32 @@ +{ + "name": "magento/module-asynchronous-operations", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "magento/framework": "*", + "magento/framework-bulk": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/module-user": "*", + "php": "~7.1.3||~7.2.0" + }, + "suggest": { + "magento/module-admin-notification": "*", + "magento/module-logging": "*" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AsynchronousOperations\\": "" + } + } +} diff --git a/app/code/Magento/AsynchronousOperations/etc/acl.xml b/app/code/Magento/AsynchronousOperations/etc/acl.xml new file mode 100644 index 0000000000000..42521ad40ff63 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/acl.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/adminhtml/di.xml b/app/code/Magento/AsynchronousOperations/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..26dd6a39473a6 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/adminhtml/di.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/adminhtml/menu.xml b/app/code/Magento/AsynchronousOperations/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..2e9fe34c45cec --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/adminhtml/menu.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/adminhtml/routes.xml b/app/code/Magento/AsynchronousOperations/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..a255af90eac8a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml b/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..7190b80750357 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml @@ -0,0 +1,20 @@ + + + + +
+ advanced + + + + + + +
+
+
diff --git a/app/code/Magento/AsynchronousOperations/etc/config.xml b/app/code/Magento/AsynchronousOperations/etc/config.xml new file mode 100644 index 0000000000000..e30c1005d0dd0 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/config.xml @@ -0,0 +1,16 @@ + + + + + + + 60 + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/crontab.xml b/app/code/Magento/AsynchronousOperations/etc/crontab.xml new file mode 100644 index 0000000000000..c55b0a886ac79 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/crontab.xml @@ -0,0 +1,14 @@ + + + + + + * * * * * + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml new file mode 100644 index 0000000000000..1b99ce9a2805f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..396e443355d8f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json @@ -0,0 +1,47 @@ +{ + "magento_bulk": { + "column": { + "id": true, + "uuid": true, + "user_id": true, + "description": true, + "operation_count": true, + "start_time": true + }, + "constraint": { + "PRIMARY": true, + "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true, + "MAGENTO_BULK_UUID": true + } + }, + "magento_operation": { + "column": { + "id": true, + "bulk_uuid": true, + "topic_name": true, + "serialized_data": true, + "result_serialized_data": true, + "status": true, + "error_code": true, + "result_message": true + }, + "index": { + "MAGENTO_OPERATION_BULK_UUID_ERROR_CODE": true + }, + "constraint": { + "PRIMARY": true, + "MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID": true + } + }, + "magento_acknowledged_bulk": { + "column": { + "id": true, + "bulk_uuid": true + }, + "constraint": { + "PRIMARY": true, + "MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID_MAGENTO_BULK_UUID": true, + "MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/AsynchronousOperations/etc/di.xml b/app/code/Magento/AsynchronousOperations/etc/di.xml new file mode 100644 index 0000000000000..c8fee29cd6838 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/di.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + magento_operation + id + + + magento_bulk + uuid + + + magento_operation + id + + + + + + + + + bulk_id + + + + + + + + Magento\AsynchronousOperations\Model\Entity\BulkSummaryMapper + + + + + + + bulkSummaryMapper + + + + + + + Magento\AsynchronousOperations\Ui\Component\DataProvider\SearchResult + Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Failed\SearchResult + Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Retriable\SearchResult + Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Failed\SearchResult + Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Retriable\SearchResult + + + + + + + + Magento\AsynchronousOperations\Model\ResourceModel\Operation\CheckIfExists + Magento\AsynchronousOperations\Model\ResourceModel\Operation\Create + + + + + + + + + + Magento\AsynchronousOperations\Model\MassPublisher + Magento\AsynchronousOperations\Model\MassPublisher + + + + + + + Magento\AsynchronousOperations\Model\VirtualType\PublisherPool + + + + + Magento\AsynchronousOperations\Model\VirtualType\BulkManagement + + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/module.xml b/app/code/Magento/AsynchronousOperations/etc/module.xml new file mode 100644 index 0000000000000..8f7a9e144462b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/module.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/etc/webapi.xml b/app/code/Magento/AsynchronousOperations/etc/webapi.xml new file mode 100644 index 0000000000000..253dedd1c7a0c --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/webapi.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv new file mode 100644 index 0000000000000..44cc0a0ab7754 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv @@ -0,0 +1,35 @@ +Back,Back +Done,Done +Retry,Retry +"'Action Details - #' .","'Action Details - #' ." +"%1 item(s) have been scheduled for update.""","%1 item(s) have been scheduled for update.""" +"Bulk Actions Log","Bulk Actions Log" +"%1 item(s) are currently being updated.","%1 item(s) are currently being updated." +"Task ""%1"": ","Task ""%1"": " +"%1 item(s) have been scheduled for update.","%1 item(s) have been scheduled for update." +"%1 item(s) have been successfully updated.","%1 item(s) have been successfully updated." +"%1 item(s) failed to update","%1 item(s) failed to update" +Details,Details +"View Details","View Details" +Dismiss,Dismiss +"Pending, in queue...","Pending, in queue..." +"%1 items selected for mass update","%1 items selected for mass update" +", %1 successfully updated",", %1 successfully updated" +", %1 failed to update",", %1 failed to update" +"Something went wrong.","Something went wrong." +"Action Log","Action Log" +"Bulk Actions","Bulk Actions" +"Days Saved in Log","Days Saved in Log" +"Description of Operation","Description of Operation" +Summary,Summary +"Start Time","Start Time" +"Items to Retry","Items to Retry" +"To retry, select the items and click “Retry”.","To retry, select the items and click “Retry”." +"Items That Can’t Be Updated.","Items That Can’t Be Updated." +ID,ID +Status,Status +"Meta Information","Meta Information" +Error,Error +"Dismiss All Completed Tasks","Dismiss All Completed Tasks" +"Action Details - #","Action Details - #" +"Number of Records Affected","Number of Records Affected" diff --git a/app/code/Magento/AsynchronousOperations/registration.php b/app/code/Magento/AsynchronousOperations/registration.php new file mode 100644 index 0000000000000..d384df583fb5a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/registration.php @@ -0,0 +1,9 @@ + + + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_bulk_details_modal.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_bulk_details_modal.xml new file mode 100644 index 0000000000000..946cf0a898585 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_bulk_details_modal.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_index_index.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_index_index.xml new file mode 100644 index 0000000000000..d8686887bbc59 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_index_index.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_details_form.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_details_form.xml new file mode 100644 index 0000000000000..19793ac82ba39 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_details_form.xml @@ -0,0 +1,150 @@ + + +
+ + + bulk_details_form.bulk_details_form_data_source + + templates/form/collapsible + + + + +
+ + + +
+ +
+
    +
  • + + + +
  • +
+ +
+ + diff --git a/app/code/Magento/Authorization/Model/Acl/AclRetriever.php b/app/code/Magento/Authorization/Model/Acl/AclRetriever.php index f22cbaf46332b..904c8d0ea7794 100644 --- a/app/code/Magento/Authorization/Model/Acl/AclRetriever.php +++ b/app/code/Magento/Authorization/Model/Acl/AclRetriever.php @@ -84,7 +84,7 @@ public function getAllowedResourcesByUser($userType, $userId) $role = $this->_getUserRole($userType, $userId); if (!$role) { throw new AuthorizationException( - __('We can\'t find the role for the user you wanted.') + __("The role wasn't found for the user. Verify the role and try again.") ); } $allowedResources = $this->getAllowedResourcesByRole($role->getId()); diff --git a/app/code/Magento/Authorization/Model/ResourceModel/Role.php b/app/code/Magento/Authorization/Model/ResourceModel/Role.php index 633ae741b44a1..48fe65e7f8b92 100644 --- a/app/code/Magento/Authorization/Model/ResourceModel/Role.php +++ b/app/code/Magento/Authorization/Model/ResourceModel/Role.php @@ -68,6 +68,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $role) } if (!$role->getTreeLevel()) { + $treeLevel = 0; if ($role->getPid() > 0) { $select = $this->getConnection()->select()->from( $this->getMainTable(), @@ -79,8 +80,6 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $role) $binds = ['pid' => (int)$role->getPid()]; $treeLevel = $this->getConnection()->fetchOne($select, $binds); - } else { - $treeLevel = 0; } $role->setTreeLevel($treeLevel + 1); diff --git a/app/code/Magento/Authorization/Setup/InstallData.php b/app/code/Magento/Authorization/Setup/InstallData.php deleted file mode 100644 index b8b18706722a5..0000000000000 --- a/app/code/Magento/Authorization/Setup/InstallData.php +++ /dev/null @@ -1,101 +0,0 @@ -authFactory = $authFactory; - } - - /** - * {@inheritdoc} - */ - public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) - { - $roleCollection = $this->authFactory->createRoleCollection() - ->addFieldToFilter('parent_id', 0) - ->addFieldToFilter('tree_level', 1) - ->addFieldToFilter('role_type', RoleGroup::ROLE_TYPE) - ->addFieldToFilter('user_id', 0) - ->addFieldToFilter('user_type', UserContextInterface::USER_TYPE_ADMIN) - ->addFieldToFilter('role_name', 'Administrators'); - - if ($roleCollection->count() == 0) { - $admGroupRole = $this->authFactory->createRole()->setData( - [ - 'parent_id' => 0, - 'tree_level' => 1, - 'sort_order' => 1, - 'role_type' => RoleGroup::ROLE_TYPE, - 'user_id' => 0, - 'user_type' => UserContextInterface::USER_TYPE_ADMIN, - 'role_name' => 'Administrators', - ] - )->save(); - } else { - foreach ($roleCollection as $item) { - $admGroupRole = $item; - break; - } - } - - $rulesCollection = $this->authFactory->createRulesCollection() - ->addFieldToFilter('role_id', $admGroupRole->getId()) - ->addFieldToFilter('resource_id', 'all'); - - if ($rulesCollection->count() == 0) { - $this->authFactory->createRules()->setData( - [ - 'role_id' => $admGroupRole->getId(), - 'resource_id' => 'Magento_Backend::all', - 'privileges' => null, - 'permission' => 'allow', - ] - )->save(); - } else { - /** @var \Magento\Authorization\Model\Rules $rule */ - foreach ($rulesCollection as $rule) { - $rule->setData('resource_id', 'Magento_Backend::all')->save(); - } - } - - /** - * Delete rows by condition from authorization_rule - */ - $setup->startSetup(); - - $tableName = $setup->getTable('authorization_rule'); - if ($tableName) { - $setup->getConnection()->delete($tableName, ['resource_id = ?' => 'admin/system/tools/compiler']); - } - - $setup->endSetup(); - } -} diff --git a/app/code/Magento/Authorization/Setup/InstallSchema.php b/app/code/Magento/Authorization/Setup/InstallSchema.php deleted file mode 100644 index 9471b448ea3b4..0000000000000 --- a/app/code/Magento/Authorization/Setup/InstallSchema.php +++ /dev/null @@ -1,150 +0,0 @@ -startSetup(); - - if (!$installer->getConnection()->isTableExists($installer->getTable('authorization_role'))) { - /** - * Create table 'authorization_role' - */ - $table = $installer->getConnection()->newTable( - $installer->getTable('authorization_role') - )->addColumn( - 'role_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], - 'Role ID' - )->addColumn( - 'parent_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Parent Role ID' - )->addColumn( - 'tree_level', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Role Tree Level' - )->addColumn( - 'sort_order', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Role Sort Order' - )->addColumn( - 'role_type', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 1, - ['nullable' => false, 'default' => '0'], - 'Role Type' - )->addColumn( - 'user_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'User ID' - )->addColumn( - 'user_type', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 16, - ['nullable' => true, 'default' => null], - 'User Type' - )->addColumn( - 'role_name', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 50, - ['nullable' => true, 'default' => null], - 'Role Name' - )->addIndex( - $installer->getIdxName('authorization_role', ['parent_id', 'sort_order']), - ['parent_id', 'sort_order'] - )->addIndex( - $installer->getIdxName('authorization_role', ['tree_level']), - ['tree_level'] - )->setComment( - 'Admin Role Table' - ); - $installer->getConnection()->createTable($table); - } - - if (!$installer->getConnection()->isTableExists($installer->getTable('authorization_rule'))) { - /** - * Create table 'authorization_rule' - */ - $table = $installer->getConnection()->newTable( - $installer->getTable('authorization_rule') - )->addColumn( - 'rule_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], - 'Rule ID' - )->addColumn( - 'role_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Role ID' - )->addColumn( - 'resource_id', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 255, - ['nullable' => true, 'default' => null], - 'Resource ID' - )->addColumn( - 'privileges', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 20, - ['nullable' => true], - 'Privileges' - )->addColumn( - 'permission', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 10, - [], - 'Permission' - )->addIndex( - $installer->getIdxName('authorization_rule', ['resource_id', 'role_id']), - ['resource_id', 'role_id'] - )->addIndex( - $installer->getIdxName('authorization_rule', ['role_id', 'resource_id']), - ['role_id', 'resource_id'] - )->addForeignKey( - $installer->getFkName('authorization_rule', 'role_id', 'authorization_role', 'role_id'), - 'role_id', - $installer->getTable('authorization_role'), - 'role_id', - \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE - )->setComment( - 'Admin Rule Table' - ); - $installer->getConnection()->createTable($table); - } - - $installer->endSetup(); - } -} diff --git a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php new file mode 100644 index 0000000000000..84992badf65db --- /dev/null +++ b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php @@ -0,0 +1,133 @@ +moduleDataSetup = $moduleDataSetup; + $this->authFactory = $authorizationFactory; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $roleCollection = $this->authFactory->createRoleCollection() + ->addFieldToFilter('parent_id', 0) + ->addFieldToFilter('tree_level', 1) + ->addFieldToFilter('role_type', RoleGroup::ROLE_TYPE) + ->addFieldToFilter('user_id', 0) + ->addFieldToFilter('user_type', UserContextInterface::USER_TYPE_ADMIN) + ->addFieldToFilter('role_name', 'Administrators'); + + if ($roleCollection->count() == 0) { + $admGroupRole = $this->authFactory->createRole()->setData( + [ + 'parent_id' => 0, + 'tree_level' => 1, + 'sort_order' => 1, + 'role_type' => RoleGroup::ROLE_TYPE, + 'user_id' => 0, + 'user_type' => UserContextInterface::USER_TYPE_ADMIN, + 'role_name' => 'Administrators', + ] + )->save(); + } else { + /** @var \Magento\Authorization\Model\ResourceModel\Role $item */ + foreach ($roleCollection as $item) { + $admGroupRole = $item; + break; + } + } + + $rulesCollection = $this->authFactory->createRulesCollection() + ->addFieldToFilter('role_id', $admGroupRole->getId()) + ->addFieldToFilter('resource_id', 'all'); + + if ($rulesCollection->count() == 0) { + $this->authFactory->createRules()->setData( + [ + 'role_id' => $admGroupRole->getId(), + 'resource_id' => 'Magento_Backend::all', + 'privileges' => null, + 'permission' => 'allow', + ] + )->save(); + } else { + /** @var \Magento\Authorization\Model\Rules $rule */ + foreach ($rulesCollection as $rule) { + $rule->setData('resource_id', 'Magento_Backend::all')->save(); + } + } + + /** + * Delete rows by condition from authorization_rule + */ + $tableName = $this->moduleDataSetup->getTable('authorization_rule'); + if ($tableName) { + $this->moduleDataSetup->getConnection()->delete( + $tableName, + ['resource_id = ?' => 'admin/system/tools/compiler'] + ); + } + } + + /** + * {@inheritdoc} + */ + public static function getDependencies() + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getVersion() + { + return '2.0.0'; + } + + /** + * {@inheritdoc} + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php b/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php index bd1a3616a746e..c214cfc832597 100644 --- a/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php +++ b/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php @@ -60,7 +60,7 @@ public function testGetAllowedResourcesByUserTypeCustomer() /** * @expectedException \Magento\Framework\Exception\AuthorizationException - * @expectedExceptionMessage We can't find the role for the user you wanted. + * @expectedExceptionMessage The role wasn't found for the user. Verify the role and try again. */ public function testGetAllowedResourcesByUserRoleNotFound() { diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json index 65e0d2a57e36d..5f5e7c62ef83b 100644 --- a/app/code/Magento/Authorization/composer.json +++ b/app/code/Magento/Authorization/composer.json @@ -5,12 +5,11 @@ "sort-packages": true }, "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", - "magento/framework": "100.3.*", - "magento/module-backend": "100.3.*" + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-backend": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Authorization/etc/db_schema.xml b/app/code/Magento/Authorization/etc/db_schema.xml new file mode 100644 index 0000000000000..45c02128bfc99 --- /dev/null +++ b/app/code/Magento/Authorization/etc/db_schema.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+
diff --git a/app/code/Magento/Authorization/etc/db_schema_whitelist.json b/app/code/Magento/Authorization/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..eb9256e415593 --- /dev/null +++ b/app/code/Magento/Authorization/etc/db_schema_whitelist.json @@ -0,0 +1,38 @@ +{ + "authorization_role": { + "column": { + "role_id": true, + "parent_id": true, + "tree_level": true, + "sort_order": true, + "role_type": true, + "user_id": true, + "user_type": true, + "role_name": true + }, + "index": { + "AUTHORIZATION_ROLE_PARENT_ID_SORT_ORDER": true, + "AUTHORIZATION_ROLE_TREE_LEVEL": true + }, + "constraint": { + "PRIMARY": true + } + }, + "authorization_rule": { + "column": { + "rule_id": true, + "role_id": true, + "resource_id": true, + "privileges": true, + "permission": true + }, + "index": { + "AUTHORIZATION_RULE_RESOURCE_ID_ROLE_ID": true, + "AUTHORIZATION_RULE_ROLE_ID_RESOURCE_ID": true + }, + "constraint": { + "PRIMARY": true, + "AUTHORIZATION_RULE_ROLE_ID_AUTHORIZATION_ROLE_ROLE_ID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/Authorization/etc/module.xml b/app/code/Magento/Authorization/etc/module.xml index 357e36d937e50..145b1ba10d0f8 100644 --- a/app/code/Magento/Authorization/etc/module.xml +++ b/app/code/Magento/Authorization/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php index bdd0c4a424e99..92957481b9290 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Authorizenet\Controller\Directpost\Payment; @@ -122,7 +123,7 @@ public function execute() /** * Place order for checkout flow * - * @return string + * @return void */ protected function placeCheckoutOrder() { @@ -147,7 +148,7 @@ protected function placeCheckoutOrder() $result->setData('error', true); $result->setData( 'error_messages', - __('An error occurred on the server. Please try to place the order again.') + __('A server error stopped your order from being placed. Please try to place your order again.') ); } if ($response instanceof Http) { diff --git a/app/code/Magento/Authorizenet/Model/TransactionService.php b/app/code/Magento/Authorizenet/Model/TransactionService.php index fef22d6c913c0..693a5b890faba 100644 --- a/app/code/Magento/Authorizenet/Model/TransactionService.php +++ b/app/code/Magento/Authorizenet/Model/TransactionService.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Authorizenet\Model; use Magento\Framework\Exception\LocalizedException; @@ -124,7 +125,7 @@ protected function loadTransactionDetails(Authorizenet $context, $transactionId) $responseXmlDocument = new Element($responseBody); libxml_use_internal_errors(false); } catch (\Exception $e) { - throw new LocalizedException(__('Unable to get transaction details. Try again later.')); + throw new LocalizedException(__('The transaction details are unavailable. Please try again later.')); } finally { $context->debugData($debugData); } @@ -132,7 +133,7 @@ protected function loadTransactionDetails(Authorizenet $context, $transactionId) if (!isset($responseXmlDocument->messages->resultCode) || $responseXmlDocument->messages->resultCode != static::PAYMENT_UPDATE_STATUS_CODE_SUCCESS ) { - throw new LocalizedException(__('Unable to get transaction details. Try again later.')); + throw new LocalizedException(__('The transaction details are unavailable. Please try again later.')); } $this->transactionDetails[$transactionId] = $responseXmlDocument; diff --git a/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/PlaceTest.php b/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/PlaceTest.php index 95ceed1ee11e7..c0a50e66759ba 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/PlaceTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Controller/Directpost/Payment/PlaceTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Authorizenet\Test\Unit\Controller\Directpost\Payment; use Magento\Authorizenet\Controller\Directpost\Payment\Place; @@ -297,7 +298,9 @@ public function textExecuteFailedPlaceOrderDataProvider() $objectFailed1 = new \Magento\Framework\DataObject( [ 'error' => true, - 'error_messages' => __('An error occurred on the server. Please try to place the order again.') + 'error_messages' => __( + 'A server error stopped your order from being placed. Please try to place your order again.' + ) ] ); $generalException = new \Exception('Exception logging will save the world!'); diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json index 89d3ba8045a40..31f2295da4307 100644 --- a/app/code/Magento/Authorizenet/composer.json +++ b/app/code/Magento/Authorizenet/composer.json @@ -5,21 +5,20 @@ "sort-packages": true }, "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", - "magento/framework": "100.3.*", - "magento/module-backend": "100.3.*", - "magento/module-catalog": "101.2.*", - "magento/module-checkout": "100.3.*", - "magento/module-payment": "100.3.*", - "magento/module-quote": "100.3.*", - "magento/module-sales": "100.3.*", - "magento/module-store": "100.3.*" + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-config": "100.3.*" + "magento/module-config": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ "proprietary" ], diff --git a/app/code/Magento/Authorizenet/etc/module.xml b/app/code/Magento/Authorizenet/etc/module.xml index 6d05f14d21318..a30fd34927746 100644 --- a/app/code/Magento/Authorizenet/etc/module.xml +++ b/app/code/Magento/Authorizenet/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Backend/Block/Cache.php b/app/code/Magento/Backend/Block/Cache.php index e14358396aa70..82c36bf3a1fe4 100644 --- a/app/code/Magento/Backend/Block/Cache.php +++ b/app/code/Magento/Backend/Block/Cache.php @@ -22,24 +22,29 @@ protected function _construct() $this->_headerText = __('Cache Storage Management'); parent::_construct(); $this->buttonList->remove('add'); - $this->buttonList->add( - 'flush_magento', - [ - 'label' => __('Flush Magento Cache'), - 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')', - 'class' => 'primary flush-cache-magento' - ] - ); - $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?'); - $this->buttonList->add( - 'flush_system', - [ - 'label' => __('Flush Cache Storage'), - 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')', - 'class' => 'flush-cache-storage' - ] - ); + if ($this->_authorization->isAllowed('Magento_Backend::flush_magento_cache')) { + $this->buttonList->add( + 'flush_magento', + [ + 'label' => __('Flush Magento Cache'), + 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')', + 'class' => 'primary flush-cache-magento' + ] + ); + } + + if ($this->_authorization->isAllowed('Magento_Backend::flush_cache_storage')) { + $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?'); + $this->buttonList->add( + 'flush_system', + [ + 'label' => __('Flush Cache Storage'), + 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')', + 'class' => 'flush-cache-storage' + ] + ); + } } /** diff --git a/app/code/Magento/Backend/Block/Cache/Permissions.php b/app/code/Magento/Backend/Block/Cache/Permissions.php new file mode 100644 index 0000000000000..272a603145f09 --- /dev/null +++ b/app/code/Magento/Backend/Block/Cache/Permissions.php @@ -0,0 +1,62 @@ +authorization = $authorization; + } + + /** + * @return bool + */ + public function hasAccessToFlushCatalogImages() + { + return $this->authorization->isAllowed('Magento_Backend::flush_catalog_images'); + } + /** + * @return bool + */ + public function hasAccessToFlushJsCss() + { + return $this->authorization->isAllowed('Magento_Backend::flush_js_css'); + } + /** + * @return bool + */ + public function hasAccessToFlushStaticFiles() + { + return $this->authorization->isAllowed('Magento_Backend::flush_static_files'); + } + /** + * @return bool + */ + public function hasAccessToAdditionalActions() + { + return ($this->hasAccessToFlushCatalogImages() + || $this->hasAccessToFlushJsCss() + || $this->hasAccessToFlushStaticFiles()); + } +} diff --git a/app/code/Magento/Backend/Block/Dashboard.php b/app/code/Magento/Backend/Block/Dashboard.php index 8d0a061621fe3..e1e87d8d4c5a3 100644 --- a/app/code/Magento/Backend/Block/Dashboard.php +++ b/app/code/Magento/Backend/Block/Dashboard.php @@ -20,7 +20,7 @@ class Dashboard extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'dashboard/index.phtml'; + protected $_template = 'Magento_Backend::dashboard/index.phtml'; /** * @return void diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index cecd7b8050352..8e238ccab44cb 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -90,7 +90,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * @var string */ - protected $_template = 'dashboard/graph.phtml'; + protected $_template = 'Magento_Backend::dashboard/graph.phtml'; /** * Adminhtml dashboard data @@ -421,6 +421,8 @@ public function getChartUrl($directUrl = true) $tmpstring = implode('|', $this->_axisLabels[$idx]); $valueBuffer[] = $indexid . ":|" . $tmpstring; + } elseif ($idx == 'y') { + $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels); } $indexid++; } diff --git a/app/code/Magento/Backend/Block/Dashboard/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Grid.php index 602b5e414d538..f7f9a79f17eb0 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Grid.php +++ b/app/code/Magento/Backend/Block/Dashboard/Grid.php @@ -17,7 +17,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended /** * @var string */ - protected $_template = 'dashboard/grid.phtml'; + protected $_template = 'Magento_Backend::dashboard/grid.phtml'; /** * Setting default for every grid on dashboard diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php index 9d9409fba093b..50279786c0a5b 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php +++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php @@ -92,7 +92,7 @@ protected function _prepareCollection() protected function _afterLoadCollection() { foreach ($this->getCollection() as $item) { - $item->getCustomer() ?: $item->setCustomer('Guest'); + $item->getCustomer() ?: $item->setCustomer($item->getBillingAddress()->getName()); } return $this; } diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php index d0f056230bcd1..6d7a4d6458a8e 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Sales.php +++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php @@ -15,7 +15,7 @@ class Sales extends \Magento\Backend\Block\Dashboard\Bar /** * @var string */ - protected $_template = 'dashboard/salebar.phtml'; + protected $_template = 'Magento_Backend::dashboard/salebar.phtml'; /** * @var \Magento\Framework\Module\Manager diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php index 96ae6dd636380..4dcda3677584c 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Totals.php +++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php @@ -16,7 +16,7 @@ class Totals extends \Magento\Backend\Block\Dashboard\Bar /** * @var string */ - protected $_template = 'dashboard/totalbar.phtml'; + protected $_template = 'Magento_Backend::dashboard/totalbar.phtml'; /** * @var \Magento\Framework\Module\Manager diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php index 9ec8ae5399d8d..5bad74d8a8be5 100644 --- a/app/code/Magento/Backend/Block/Media/Uploader.php +++ b/app/code/Magento/Backend/Block/Media/Uploader.php @@ -6,6 +6,7 @@ namespace Magento\Backend\Block\Media; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; /** * Adminhtml media library uploader @@ -30,25 +31,24 @@ class Uploader extends \Magento\Backend\Block\Widget protected $_fileSizeService; /** - * @var \Magento\Framework\Json\EncoderInterface + * @var Json */ - protected $_jsonEncoder; + private $jsonEncoder; /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\File\Size $fileSize * @param array $data - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param Json $jsonEncoder */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\File\Size $fileSize, array $data = [], - \Magento\Framework\Json\EncoderInterface $jsonEncoder = null + Json $jsonEncoder = null ) { - $this->_jsonEncoder = $jsonEncoder ?: - ObjectManager::getInstance()->get(\Magento\Framework\Json\EncoderInterface::class); $this->_fileSizeService = $fileSize; + $this->jsonEncoder = $jsonEncoder ?: ObjectManager::getInstance()->get(Json::class); parent::__construct($context, $data); } @@ -118,7 +118,7 @@ public function getJsObjectName() */ public function getConfigJson() { - return $this->_jsonEncoder->encode($this->getConfig()->getData()); + return $this->jsonEncoder->encode($this->getConfig()->getData()); } /** diff --git a/app/code/Magento/Backend/Block/Menu.php b/app/code/Magento/Backend/Block/Menu.php index bb239d31b1779..7d86497288a69 100644 --- a/app/code/Magento/Backend/Block/Menu.php +++ b/app/code/Magento/Backend/Block/Menu.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Backend\Block; /** @@ -137,7 +135,9 @@ protected function _getAnchorLabel($menuItem) */ protected function _renderMouseEvent($menuItem) { - return $menuItem->hasChildren() ? 'onmouseover="Element.addClassName(this,\'over\')" onmouseout="Element.removeClassName(this,\'over\')"' : ''; + return $menuItem->hasChildren() + ? 'onmouseover="Element.addClassName(this,\'over\')" onmouseout="Element.removeClassName(this,\'over\')"' + : ''; } /** @@ -352,7 +352,7 @@ protected function _addSubMenu($menuItem, $level, $limit, $id = null) return $output; } $output .= '
diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index 4eb0b986edfb1..ee4638670f60e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -151,48 +151,43 @@ true
- + category - ui/form/element/uploader/uploader + ui/form/element/uploader/image string true false - + false Magento_Catalog/image-preview + Media Gallery + catalog/category + jpg jpeg gif png + 4194304 - + - - bold,italic,|,justifyleft,justifycenter,justifyright,|,fontselect,fontsizeselect,|,forecolor,backcolor,|,link,unlink,image,|,bullist,numlist,|,code - false - false - false - false - - false 100px - false false false - false + true true category @@ -468,7 +463,7 @@ - + string diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml index 96055b73d363b..1e60823929770 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml @@ -22,10 +22,10 @@ Allowed file types: jpeg, gif, png. - fileUploader + imageUploader - + jpg jpeg gif png 2097152 @@ -33,7 +33,7 @@ theme/design_config_fileUploader/save - + @@ -80,12 +80,11 @@ - Allowed file types: jpeg, gif, png. - fileUploader + imageUploader - + jpg jpeg gif png 2097152 @@ -93,7 +92,7 @@ theme/design_config_fileUploader/save - + @@ -140,12 +139,11 @@ - Allowed file types: jpeg, gif, png. - fileUploader + imageUploader - + jpg jpeg gif png 2097152 @@ -153,7 +151,7 @@ theme/design_config_fileUploader/save - + diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml index 772bc1e6ec5d7..6c5d37a92ea4a 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml @@ -71,6 +71,9 @@ + + Text Editor input type requires WYSIWYG to be enabled in Stores > Configuration > Content Management. + product_attribute @@ -78,6 +81,7 @@ string frontend_input + Magento_Catalog/form/element/frontend-input-select getBlockHtml('formkey') ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index aaa040c3a5833..f434402346087 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -146,8 +146,8 @@ switch ($type = $block->getType()) { } break; - case 'other': - break; + default: + $exist = null; } ?> @@ -202,7 +202,7 @@ switch ($type = $block->getType()) { getReviewsSummaryHtml($_item, $templateType) ?> - isComposite() && $_item->isSaleable() && $type == 'related'): ?> + isComposite() && $_item->isSaleable() && $type == 'related'): ?> getRequiredOptions()): ?> -isRedirectToCartEnabled()) : ?> - - - diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml index c8c915a3140da..9c5cce7865532 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml @@ -16,12 +16,13 @@ getProduct(); ?>
-
getOptions()): ?> enctype="multipart/form-data"> + getBlockHtml('formkey') ?> getChildHtml('form_top') ?> hasOptions()):?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml index 5a064b33355a4..1bfa30478df8a 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml @@ -47,21 +47,11 @@ "data": getGalleryImagesJson() ?>, "options": { "nav": "getVar("gallery/nav") ?>", - getVar("gallery/loop"))): ?> - "loop": getVar("gallery/loop") ?>, - - getVar("gallery/keyboard"))): ?> - "keyboard": getVar("gallery/keyboard") ?>, - - getVar("gallery/arrows"))): ?> - "arrows": getVar("gallery/arrows") ?>, - - getVar("gallery/allowfullscreen"))): ?> - "allowfullscreen": getVar("gallery/allowfullscreen") ?>, - - getVar("gallery/caption"))): ?> - "showCaption": getVar("gallery/caption") ?>, - + "loop": getVar("gallery/loop") ? 'true' : 'false' ?>, + "keyboard": getVar("gallery/keyboard") ? 'true' : 'false' ?>, + "arrows": getVar("gallery/arrows") ? 'true' : 'false' ?>, + "allowfullscreen": getVar("gallery/allowfullscreen") ? 'true' : 'false' ?>, + "showCaption": getVar("gallery/caption") ? 'true' : 'false' ?>, "width": "getImageAttribute('product_page_image_medium', 'width') ?>", "thumbwidth": "getImageAttribute('product_page_image_small', 'width') ?>", getImageAttribute('product_page_image_small', 'height') || $block->getImageAttribute('product_page_image_small', 'width')): ?> @@ -76,28 +66,18 @@ "transitionduration": getVar("gallery/transition/duration") ?>, "transition": "getVar("gallery/transition/effect") ?>", - getVar("gallery/navarrows"))): ?> - "navarrows": getVar("gallery/navarrows") ?>, - + "navarrows": getVar("gallery/navarrows") ? 'true' : 'false' ?>, "navtype": "getVar("gallery/navtype") ?>", "navdir": "getVar("gallery/navdir") ?>" }, "fullscreen": { "nav": "getVar("gallery/fullscreen/nav") ?>", - getVar("gallery/fullscreen/loop")): ?> - "loop": getVar("gallery/fullscreen/loop") ?>, - + "loop": getVar("gallery/fullscreen/loop") ? 'true' : 'false' ?>, "navdir": "getVar("gallery/fullscreen/navdir") ?>", - getVar("gallery/transition/navarrows")): ?> - "navarrows": getVar("gallery/fullscreen/navarrows") ?>, - + "navarrows": getVar("gallery/fullscreen/navarrows") ? 'true' : 'false' ?>, "navtype": "getVar("gallery/fullscreen/navtype") ?>", - getVar("gallery/fullscreen/arrows")): ?> - "arrows": getVar("gallery/fullscreen/arrows") ?>, - - getVar("gallery/fullscreen/caption")): ?> - "showCaption": getVar("gallery/fullscreen/caption") ?>, - + "arrows": getVar("gallery/fullscreen/arrows") ? 'true' : 'false' ?>, + "showCaption": getVar("gallery/fullscreen/caption") ? 'true' : 'false' ?>, getVar("gallery/fullscreen/transition/duration")): ?> "transitionduration": getVar("gallery/fullscreen/transition/duration") ?>, diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml index b1e46776af465..a2b91a5eeb99f 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml @@ -9,7 +9,7 @@ /** @var $block \Magento\Catalog\Block\Product\View */ ?> - + diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml index 79dc8591fd724..11aedc33c2d42 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml @@ -29,6 +29,7 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; if ($_option->getMaxCharacters()) { $_textValidate['maxlength'] = $_option->getMaxCharacters(); } + $_textValidate['validate-no-utf8mb4-characters'] = true; ?> getIsRequire()) ? ' required' : ''; if ($_option->getMaxCharacters()) { $_textAreaValidate['maxlength'] = $_option->getMaxCharacters(); } + $_textAreaValidate['validate-no-utf8mb4-characters'] = true; ?> + texteditor date boolean multiselect diff --git a/app/code/Magento/Eav/etc/db_schema.xml b/app/code/Magento/Eav/etc/db_schema.xml new file mode 100644 index 0000000000000..24a9d405dd7a5 --- /dev/null +++ b/app/code/Magento/Eav/etc/db_schema.xml @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/app/code/Magento/Eav/etc/db_schema_whitelist.json b/app/code/Magento/Eav/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..9c015e0e3b8c5 --- /dev/null +++ b/app/code/Magento/Eav/etc/db_schema_whitelist.json @@ -0,0 +1,389 @@ +{ + "eav_entity_type": { + "column": { + "entity_type_id": true, + "entity_type_code": true, + "entity_model": true, + "attribute_model": true, + "entity_table": true, + "value_table_prefix": true, + "entity_id_field": true, + "is_data_sharing": true, + "data_sharing_key": true, + "default_attribute_set_id": true, + "increment_model": true, + "increment_per_store": true, + "increment_pad_length": true, + "increment_pad_char": true, + "additional_attribute_table": true, + "entity_attribute_collection": true + }, + "index": { + "EAV_ENTITY_TYPE_ENTITY_TYPE_CODE": true + }, + "constraint": { + "PRIMARY": true + } + }, + "eav_entity": { + "column": { + "entity_id": true, + "entity_type_id": true, + "attribute_set_id": true, + "increment_id": true, + "parent_id": true, + "store_id": true, + "created_at": true, + "updated_at": true, + "is_active": true + }, + "index": { + "EAV_ENTITY_ENTITY_TYPE_ID": true, + "EAV_ENTITY_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_STORE_ID_STORE_STORE_ID": true + } + }, + "eav_entity_datetime": { + "column": { + "value_id": true, + "entity_type_id": true, + "attribute_id": true, + "store_id": true, + "entity_id": true, + "value": true + }, + "index": { + "EAV_ENTITY_DATETIME_STORE_ID": true, + "EAV_ENTITY_DATETIME_ATTRIBUTE_ID_VALUE": true, + "EAV_ENTITY_DATETIME_ENTITY_TYPE_ID_VALUE": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_DATETIME_ENTITY_ID_EAV_ENTITY_ENTITY_ID": true, + "EAV_ENTT_DTIME_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID": true, + "EAV_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID": true, + "EAV_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID": true + } + }, + "eav_entity_decimal": { + "column": { + "value_id": true, + "entity_type_id": true, + "attribute_id": true, + "store_id": true, + "entity_id": true, + "value": true + }, + "index": { + "EAV_ENTITY_DECIMAL_STORE_ID": true, + "EAV_ENTITY_DECIMAL_ATTRIBUTE_ID_VALUE": true, + "EAV_ENTITY_DECIMAL_ENTITY_TYPE_ID_VALUE": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_DECIMAL_ENTITY_ID_EAV_ENTITY_ENTITY_ID": true, + "EAV_ENTITY_DECIMAL_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID": true, + "EAV_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID": true + } + }, + "eav_entity_int": { + "column": { + "value_id": true, + "entity_type_id": true, + "attribute_id": true, + "store_id": true, + "entity_id": true, + "value": true + }, + "index": { + "EAV_ENTITY_INT_STORE_ID": true, + "EAV_ENTITY_INT_ATTRIBUTE_ID_VALUE": true, + "EAV_ENTITY_INT_ENTITY_TYPE_ID_VALUE": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_INT_ENTITY_ID_EAV_ENTITY_ENTITY_ID": true, + "EAV_ENTITY_INT_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_INT_STORE_ID_STORE_STORE_ID": true, + "EAV_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID": true + } + }, + "eav_entity_text": { + "column": { + "value_id": true, + "entity_type_id": true, + "attribute_id": true, + "store_id": true, + "entity_id": true, + "value": true + }, + "index": { + "EAV_ENTITY_TEXT_ENTITY_TYPE_ID": true, + "EAV_ENTITY_TEXT_ATTRIBUTE_ID": true, + "EAV_ENTITY_TEXT_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_TEXT_ENTITY_ID_EAV_ENTITY_ENTITY_ID": true, + "EAV_ENTITY_TEXT_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_TEXT_STORE_ID_STORE_STORE_ID": true, + "EAV_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID": true + } + }, + "eav_entity_varchar": { + "column": { + "value_id": true, + "entity_type_id": true, + "attribute_id": true, + "store_id": true, + "entity_id": true, + "value": true + }, + "index": { + "EAV_ENTITY_VARCHAR_STORE_ID": true, + "EAV_ENTITY_VARCHAR_ATTRIBUTE_ID_VALUE": true, + "EAV_ENTITY_VARCHAR_ENTITY_TYPE_ID_VALUE": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_VARCHAR_ENTITY_ID_EAV_ENTITY_ENTITY_ID": true, + "EAV_ENTITY_VARCHAR_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID": true, + "EAV_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID": true + } + }, + "eav_attribute": { + "column": { + "attribute_id": true, + "entity_type_id": true, + "attribute_code": true, + "attribute_model": true, + "backend_model": true, + "backend_type": true, + "backend_table": true, + "frontend_model": true, + "frontend_input": true, + "frontend_label": true, + "frontend_class": true, + "source_model": true, + "is_required": true, + "is_user_defined": true, + "default_value": true, + "is_unique": true, + "note": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ATTRIBUTE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ATTRIBUTE_ENTITY_TYPE_ID_ATTRIBUTE_CODE": true + } + }, + "eav_entity_store": { + "column": { + "entity_store_id": true, + "entity_type_id": true, + "store_id": true, + "increment_prefix": true, + "increment_last_id": true + }, + "index": { + "EAV_ENTITY_STORE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_STORE_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_STORE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ENTITY_STORE_STORE_ID_STORE_STORE_ID": true + } + }, + "eav_attribute_set": { + "column": { + "attribute_set_id": true, + "entity_type_id": true, + "attribute_set_name": true, + "sort_order": true + }, + "index": { + "EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_SORT_ORDER": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, + "EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_ATTRIBUTE_SET_NAME": true + } + }, + "eav_attribute_group": { + "column": { + "attribute_group_id": true, + "attribute_set_id": true, + "attribute_group_name": true, + "sort_order": true, + "default_id": true, + "attribute_group_code": true, + "tab_group_code": true + }, + "index": { + "EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_SORT_ORDER": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ATTR_GROUP_ATTR_SET_ID_EAV_ATTR_SET_ATTR_SET_ID": true, + "EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_ATTRIBUTE_GROUP_NAME": true, + "CATALOG_CATEGORY_PRODUCT_ATTRIBUTE_SET_ID_ATTRIBUTE_GROUP_CODE": true + } + }, + "eav_entity_attribute": { + "column": { + "entity_attribute_id": true, + "entity_type_id": true, + "attribute_set_id": true, + "attribute_group_id": true, + "attribute_id": true, + "sort_order": true + }, + "index": { + "EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_SET_ID_SORT_ORDER": true, + "EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true, + "EAV_ENTT_ATTR_ATTR_GROUP_ID_EAV_ATTR_GROUP_ATTR_GROUP_ID": true, + "EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_SET_ID_ATTRIBUTE_ID": true, + "EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_GROUP_ID_ATTRIBUTE_ID": true + } + }, + "eav_attribute_option": { + "column": { + "option_id": true, + "attribute_id": true, + "sort_order": true + }, + "index": { + "EAV_ATTRIBUTE_OPTION_ATTRIBUTE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ATTRIBUTE_OPTION_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true + } + }, + "eav_attribute_option_value": { + "column": { + "value_id": true, + "option_id": true, + "store_id": true, + "value": true + }, + "index": { + "EAV_ATTRIBUTE_OPTION_VALUE_OPTION_ID": true, + "EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ATTR_OPT_VAL_OPT_ID_EAV_ATTR_OPT_OPT_ID": true, + "EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_STORE_STORE_ID": true + } + }, + "eav_attribute_label": { + "column": { + "attribute_label_id": true, + "attribute_id": true, + "store_id": true, + "value": true + }, + "index": { + "EAV_ATTRIBUTE_LABEL_STORE_ID": true, + "EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true, + "EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID": true + } + }, + "eav_form_type": { + "column": { + "type_id": true, + "code": true, + "label": true, + "is_system": true, + "theme": true, + "store_id": true + }, + "index": { + "EAV_FORM_TYPE_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_FORM_TYPE_STORE_ID_STORE_STORE_ID": true, + "EAV_FORM_TYPE_CODE_THEME_STORE_ID": true + } + }, + "eav_form_type_entity": { + "column": { + "type_id": true, + "entity_type_id": true + }, + "index": { + "EAV_FORM_TYPE_ENTITY_ENTITY_TYPE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_FORM_TYPE_ENTT_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID": true, + "EAV_FORM_TYPE_ENTITY_TYPE_ID_EAV_FORM_TYPE_TYPE_ID": true + } + }, + "eav_form_fieldset": { + "column": { + "fieldset_id": true, + "type_id": true, + "code": true, + "sort_order": true + }, + "constraint": { + "PRIMARY": true, + "EAV_FORM_FIELDSET_TYPE_ID_EAV_FORM_TYPE_TYPE_ID": true, + "EAV_FORM_FIELDSET_TYPE_ID_CODE": true + } + }, + "eav_form_fieldset_label": { + "column": { + "fieldset_id": true, + "store_id": true, + "label": true + }, + "index": { + "EAV_FORM_FIELDSET_LABEL_STORE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_FORM_FSET_LBL_FSET_ID_EAV_FORM_FSET_FSET_ID": true, + "EAV_FORM_FIELDSET_LABEL_STORE_ID_STORE_STORE_ID": true + } + }, + "eav_form_element": { + "column": { + "element_id": true, + "type_id": true, + "fieldset_id": true, + "attribute_id": true, + "sort_order": true + }, + "index": { + "EAV_FORM_ELEMENT_FIELDSET_ID": true, + "EAV_FORM_ELEMENT_ATTRIBUTE_ID": true + }, + "constraint": { + "PRIMARY": true, + "EAV_FORM_ELEMENT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true, + "EAV_FORM_ELEMENT_FIELDSET_ID_EAV_FORM_FIELDSET_FIELDSET_ID": true, + "EAV_FORM_ELEMENT_TYPE_ID_EAV_FORM_TYPE_TYPE_ID": true, + "EAV_FORM_ELEMENT_TYPE_ID_ATTRIBUTE_ID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index c8afd10aa3eee..ae4663cfc236a 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -8,6 +8,7 @@ + @@ -24,7 +25,7 @@ - + @@ -46,10 +47,35 @@ - + - - + + + + + + + Magento\Eav\Model\TypeLocator\SimpleType + Magento\Eav\Model\TypeLocator\ComplexType + Magento\Eav\Model\TypeLocator\ServiceClassLocator + + + + + + + string + string + boolean + string + string + Magento\Framework\Api\Data\ImageContentInterface + string + string + string + string + string + diff --git a/app/code/Magento/Eav/etc/module.xml b/app/code/Magento/Eav/etc/module.xml index acd5807a29f90..7b2b651b2d2f9 100644 --- a/app/code/Magento/Eav/etc/module.xml +++ b/app/code/Magento/Eav/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Eav/view/adminhtml/requirejs-config.js b/app/code/Magento/Eav/view/adminhtml/requirejs-config.js new file mode 100644 index 0000000000000..6e3835852fd2d --- /dev/null +++ b/app/code/Magento/Eav/view/adminhtml/requirejs-config.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + eavInputTypes: 'Magento_Eav/js/input-types' + } + } +}; diff --git a/app/code/Magento/Eav/view/adminhtml/templates/attribute/edit/js.phtml b/app/code/Magento/Eav/view/adminhtml/templates/attribute/edit/js.phtml index 223b3e9888eea..b7b29d7cdcd10 100644 --- a/app/code/Magento/Eav/view/adminhtml/templates/attribute/edit/js.phtml +++ b/app/code/Magento/Eav/view/adminhtml/templates/attribute/edit/js.phtml @@ -2,4 +2,19 @@ /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. - */; + */ + +/** @var \Magento\Eav\Block\Adminhtml\Attribute\Edit\Js $block */ + +// @codingStandardsIgnoreFile +?> + diff --git a/app/code/Magento/Eav/view/adminhtml/web/js/input-types.js b/app/code/Magento/Eav/view/adminhtml/web/js/input-types.js new file mode 100644 index 0000000000000..250bea09adf4b --- /dev/null +++ b/app/code/Magento/Eav/view/adminhtml/web/js/input-types.js @@ -0,0 +1,84 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($) { + 'use strict'; + + return function (config) { + $('select#frontend_input').each(function () { + var select = $(this), + currentValue = select.find('option:selected').val(), + compatibleTypes = config.inputTypes, + enabledTypes = [], + iterator, + warning = $(' diff --git a/app/code/Magento/Elasticsearch/etc/config.xml b/app/code/Magento/Elasticsearch/etc/config.xml new file mode 100644 index 0000000000000..0e01aba5ed857 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/config.xml @@ -0,0 +1,26 @@ + + + + + + + localhost + 9200 + magento2 + 0 + 15 + + localhost + 9200 + magento2 + 0 + 15 + + + + diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml new file mode 100644 index 0000000000000..b3ba240ae239c --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -0,0 +1,268 @@ + + + + + + + + Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy + + + + + + + + Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy + Magento\Elasticsearch\Model\Adapter\BatchDataMapper\PriceFieldsProvider + + + + + + AdditionalFieldsForElasticsearchDataMapper + + + + + + + Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper + + + + + + + + + Elasticsearch + Elasticsearch 5.0+ + + + + + + + Magento\Elasticsearch\Model\Adapter\BatchDataMapper\CategoryFieldsProvider + Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider + + + + + + + Magento\Elasticsearch\Model\Adapter\DataMapper\ProductDataMapper + Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapper + + + + + + + Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper + Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper + + + + + + + \Magento\Elasticsearch\Model\Client\ElasticsearchFactory + \Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory + + + \Magento\Elasticsearch\Model\Config + \Magento\Elasticsearch\Model\Config + + + + + + + Magento\Elasticsearch\Model\Indexer\IndexerHandler + Magento\Elasticsearch\Model\Indexer\IndexerHandler + + + + + + + Magento\Elasticsearch\Model\Indexer\IndexStructure + Magento\Elasticsearch\Model\Indexer\IndexStructure + + + + + + + Magento\Elasticsearch\Model\ResourceModel\Engine + Magento\Elasticsearch\Model\ResourceModel\Engine + + + + + + + Magento\Elasticsearch\SearchAdapter\Adapter + Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter + + + + + + + elasticsearch + elasticsearch5 + + + + + + _id + + + + + Magento\Elasticsearch\SearchAdapter\ProductEntityMetadata + + + + + Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy + Magento\Elasticsearch\Model\Config + + + + + Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory + Magento\Elasticsearch\Model\Config + + + + + Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch + + + + + + Magento\Elasticsearch\Model\Client\ElasticsearchFactory + Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory + + + + + + Magento\Elasticsearch\Elasticsearch5\SearchAdapter\ConnectionManager + + + + + + Magento\Elasticsearch\SearchAdapter\Aggregation\Interval + Magento\Elasticsearch\SearchAdapter\Aggregation\Interval + + + + + + + Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider + Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider + + + + + + + Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider + + + Magento\Elasticsearch\SearchAdapter\Aggregation\Builder\Term + Magento\Elasticsearch\SearchAdapter\Aggregation\Builder\Dynamic + + + + + + + Magento\Elasticsearch\SearchAdapter\Query\Preprocessor\Stopwords + Magento\Search\Adapter\Query\Preprocessor\Synonyms + + + + + + Magento_Elasticsearch + stopwords + + + + + + Magento\Elasticsearch\Model\Adapter\Index\Config\Converter + Magento\Elasticsearch\Model\Adapter\Index\Config\SchemaLocator + esconfig.xml + + + + + Magento\Elasticsearch\Model\Adapter\Index\Config\Reader + elasticsearch_index_config + + + + + Magento\Elasticsearch\Model\Client\Elasticsearch + + + + + + Magento\Elasticsearch\Model\DataProvider\Suggestions + Magento\Elasticsearch\Model\DataProvider\Suggestions + + + + + + \Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID + + + + + + 1 + 1 + 1 + + 1 + 1 + 1 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + diff --git a/app/code/Magento/Elasticsearch/etc/esconfig.xml b/app/code/Magento/Elasticsearch/etc/esconfig.xml new file mode 100644 index 0000000000000..0a87b58fd3a18 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/esconfig.xml @@ -0,0 +1,31 @@ + + + + + stemmer + english + german + english + spanish + french + dutch + portuguese + cjk + + + stopwords.csv + stopwords_de_DE.csv + stopwords_en_US.csv + stopwords_es_ES.csv + stopwords_fr_FR.csv + stopwords_nl_NL.csv + stopwords_pt_BR.csv + stopwords_zh_Hans_CN.csv + + diff --git a/app/code/Magento/Elasticsearch/etc/esconfig.xsd b/app/code/Magento/Elasticsearch/etc/esconfig.xsd new file mode 100644 index 0000000000000..d1442b074ac80 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/esconfig.xsd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Elasticsearch/etc/module.xml b/app/code/Magento/Elasticsearch/etc/module.xml new file mode 100644 index 0000000000000..3a0f229713bc0 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/module.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Elasticsearch/etc/search_engine.xml b/app/code/Magento/Elasticsearch/etc/search_engine.xml new file mode 100644 index 0000000000000..34b6432b15c03 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/search_engine.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords.csv new file mode 100644 index 0000000000000..2d59246d51aa9 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords.csv @@ -0,0 +1,35 @@ +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_de_DE.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_de_DE.csv new file mode 100644 index 0000000000000..4dd6c27dceac4 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_de_DE.csv @@ -0,0 +1,129 @@ +aber +als +am +an +auch +auf +aus +bei +bin +bis +bist +da +dadurch +daher +darum +das +daß +dass +dein +deine +dem +den +der +des +dessen +deshalb +die +dies +dieser +dieses +doch +dort +du +durch +ein +eine +einem +einen +einer +eines +er +es +euer +eure +für +hatte +hatten +hattest +hattet +hier +hinter +ich +ihr +ihre +im +in +ist +ja +jede +jedem +jeden +jeder +jedes +jener +jenes +jetzt +kann +kannst +können +könnt +machen +mein +meine +mit +muß +mußt +musst +müssen +müßt +nach +nachdem +nein +nicht +nun +oder +seid +sein +seine +sich +sie +sind +soll +sollen +sollst +sollt +sonst +soweit +sowie +und +unser +unsere +unter +vom +von +vor +wann +warum +was +weiter +weitere +wenn +wer +werde +werden +werdet +weshalb +wie +wieder +wieso +wir +wird +wirst +wo +woher +wohin +zu +zum +zur +über \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_en_US.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_en_US.csv new file mode 100644 index 0000000000000..2d59246d51aa9 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_en_US.csv @@ -0,0 +1,35 @@ +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_es_ES.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_es_ES.csv new file mode 100644 index 0000000000000..c369aaa3a686e --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_es_ES.csv @@ -0,0 +1,178 @@ +un +una +unas +unos +uno +sobre +todo +también +tras +otro +algún +alguno +alguna +algunos +algunas +ser +es +soy +eres +somos +sois +estoy +esta +estamos +estais +estan +como +en +para +atras +porque +por qué +estado +estaba +ante +antes +siendo +ambos +pero +por +poder +puede +puedo +podemos +podeis +pueden +fui +fue +fuimos +fueron +hacer +hago +hace +hacemos +haceis +hacen +cada +fin +incluso +primero +desde +conseguir +consigo +consigue +consigues +conseguimos +consiguen +ir +voy +va +vamos +vais +van +vaya +gueno +ha +tener +tengo +tiene +tenemos +teneis +tienen +el +la +lo +las +los +su +aqui +mio +tuyo +ellos +ellas +nos +nosotros +vosotros +vosotras +si +dentro +solo +solamente +saber +sabes +sabe +sabemos +sabeis +saben +ultimo +largo +bastante +haces +muchos +aquellos +aquellas +sus +entonces +tiempo +verdad +verdadero +verdadera +cierto +ciertos +cierta +ciertas +intentar +intento +intenta +intentas +intentamos +intentais +intentan +dos +bajo +arriba +encima +usar +uso +usas +usa +usamos +usais +usan +emplear +empleo +empleas +emplean +ampleamos +empleais +valor +muy +era +eras +eramos +eran +modo +bien +cual +cuando +donde +mientras +quien +con +entre +sin +trabajo +trabajar +trabajas +trabaja +trabajamos +trabajais +trabajan +podria +podrias +podriamos +podrian +podriais +yo +aquel \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_fr_FR.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_fr_FR.csv new file mode 100644 index 0000000000000..f01d72c2606ef --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_fr_FR.csv @@ -0,0 +1,116 @@ +alors +au +aucuns +aussi +autre +avant +avec +avoir +bon +car +ce +cela +ces +ceux +chaque +ci +comme +comment +dans +des +du +dedans +dehors +depuis +devrait +doit +donc +dos +début +elle +elles +en +encore +essai +est +et +eu +fait +faites +fois +font +hors +ici +il +ils +je +juste +la +le +les +leur +là +ma +maintenant +mais +mes +mine +moins +mon +mot +même +ni +nommés +notre +nous +ou +où +par +parce +pas +peut +peu +plupart +pour +pourquoi +quand +que +quel +quelle +quelles +quels +qui +sa +sans +ses +seulement +si +sien +son +sont +sous +soyez +sujet +sur +ta +tandis +tellement +tels +tes +ton +tous +tout +trop +très +tu +voient +vont +votre +vous +vu +ça +étaient +état +étions +été +être \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_nl_NL.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_nl_NL.csv new file mode 100644 index 0000000000000..50839d97dea60 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_nl_NL.csv @@ -0,0 +1,104 @@ +aan +af +al +alles +als +altijd +andere +ben +bij +daar +dan +dat +de +der +deze +die +dit +doch +doen +door +dus +een +eens +en +er +ge +geen +geweest +haar +had +heb +hebben +heeft +hem +het +hier +hij +hoe +hun +iemand +iets +ik +in +is +ja +je +kan +kon +kunnen +maar +me +meer +men +met +mij +mijn +moet +na +naar +niet +niets +nog +nu +of +om +omdat +ons +ook +op +over +reeds +te +tegen +toch +toen +tot +u +uit +uw +van +veel +voor +want +waren +was +wat +we +wel +werd +wezen +wie +wij +wil +worden +zal +ze +zei +zelf +zich +zij +zijn +zo +zonder +zou \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_pt_BR.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_pt_BR.csv new file mode 100644 index 0000000000000..d926132e62891 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_pt_BR.csv @@ -0,0 +1,147 @@ +último +é +acerca +agora +algmas +alguns +ali +ambos +antes +apontar +aquela +aquelas +aquele +aqueles +aqui +atrás +bem +bom +cada +caminho +cima +com +como +comprido +conhecido +corrente +das +debaixo +dentro +desde +desligado +deve +devem +deverá +direita +diz +dizer +dois +dos +e +ela +ele +eles +em +enquanto +então +está +estão +estado +estar +estará +este +estes +esteve +estive +estivemos +estiveram +eu +fará +faz +fazer +fazia +fez +fim +foi +fora +horas +iniciar +inicio +ir +irá +ista +iste +isto +ligado +maioria +maiorias +mais +mas +mesmo +meu +muito +muitos +nós +não +nome +nosso +novo +o +onde +os +ou +outro +para +parte +pegar +pelo +pessoas +pode +poderá +podia +por +porque +povo +promeiro +quê +qual +qualquer +quando +quem +quieto +são +saber +sem +ser +seu +somente +têm +tal +também +tem +tempo +tenho +tentar +tentaram +tente +tentei +teu +teve +tipo +tive +todos +trabalhar +trabalho +tu +um +uma +umas +uns +usa +usar +valor +veja +ver +verdade +verdadeiro +você \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_zh_Hans_CN.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_zh_Hans_CN.csv new file mode 100644 index 0000000000000..0b3ef8f89fb35 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_zh_Hans_CN.csv @@ -0,0 +1,125 @@ +的 +一 +不 +在 +人 +有 +是 +为 +以 +于 +上 +他 +而 +后 +之 +来 +及 +了 +因 +下 +可 +到 +由 +这 +与 +也 +此 +但 +并 +个 +其 +已 +无 +小 +我 +们 +起 +最 +再 +今 +去 +好 +只 +又 +或 +很 +亦 +某 +把 +那 +你 +乃 +它 +吧 +被 +比 +别 +趁 +当 +从 +到 +得 +打 +凡 +儿 +尔 +该 +各 +给 +跟 +和 +何 +还 +即 +几 +既 +看 +据 +距 +靠 +啦 +了 +另 +么 +每 +们 +嘛 +拿 +哪 +那 +您 +凭 +且 +却 +让 +仍 +啥 +如 +若 +使 +谁 +虽 +随 +同 +所 +她 +哇 +嗡 +往 +哪 +些 +向 +沿 +哟 +用 +于 +咱 +则 +怎 +曾 +至 +致 +着 +诸 +自 \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/i18n/en_US.csv b/app/code/Magento/Elasticsearch/i18n/en_US.csv new file mode 100644 index 0000000000000..85c9aefdb9f25 --- /dev/null +++ b/app/code/Magento/Elasticsearch/i18n/en_US.csv @@ -0,0 +1,12 @@ +"There is no such data mapper ""%1"" for interface %2","There is no such data mapper ""%1"" for interface %2" +"Data mapper ""%1"" must implement interface %2","Data mapper ""%1"" must implement interface %2" +"We were unable to perform the search because of a search engine misconfiguration.","We were unable to perform the search because of a search engine misconfiguration." +"Could not ping search engine: %1","Could not ping search engine: %1" +"Elasticsearch Server Hostname","Elasticsearch Server Hostname" +"Elasticsearch Server Port","Elasticsearch Server Port" +"Elasticsearch Index Prefix","Elasticsearch Index Prefix" +"Enable Elasticsearch HTTP Auth","Enable Elasticsearch HTTP Auth" +"Elasticsearch HTTP Username","Elasticsearch HTTP Username" +"Elasticsearch HTTP Password","Elasticsearch HTTP Password" +"Elasticsearch Server Timeout","Elasticsearch Server Timeout" +"Test Connection","Test Connection" diff --git a/app/code/Magento/Elasticsearch/registration.php b/app/code/Magento/Elasticsearch/registration.php new file mode 100644 index 0000000000000..9fc806c12e403 --- /dev/null +++ b/app/code/Magento/Elasticsearch/registration.php @@ -0,0 +1,11 @@ +toolbar->pushButtons($this, $this->buttonList); - $this->addChild('form', \Magento\Email\Block\Adminhtml\Template\Edit\Form::class); + $this->addChild( + 'form', + \Magento\Email\Block\Adminhtml\Template\Edit\Form::class, + [ + 'email_template' => $this->getEmailTemplate() + ] + ); return parent::_prepareLayout(); } @@ -367,7 +375,7 @@ public function getDeleteUrl() */ public function getEmailTemplate() { - return $this->_registryManager->registry('current_email_template'); + return $this->getData('email_template'); } /** @@ -388,7 +396,7 @@ public function getLoadUrl() */ public function getCurrentlyUsedForPaths($asJSON = true) { - /** @var $template \Magento\Email\Model\BackendTemplate */ + /** @var $template BackendTemplate */ $template = $this->getEmailTemplate(); $paths = $template->getSystemConfigPathsWhereCurrentlyUsed(); $pathsParts = $this->_getSystemConfigPathsParts($paths); diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php b/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php index e6f5645d5548d..e8096fa2c25f8 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php @@ -12,7 +12,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic { /** - * @var \Magento\Email\Model\Source\Variables + * @var \Magento\Variable\Model\Source\Variables */ protected $_variables; @@ -31,7 +31,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Data\FormFactory $formFactory * @param \Magento\Variable\Model\VariableFactory $variableFactory - * @param \Magento\Email\Model\Source\Variables $variables + * @param \Magento\Variable\Model\Source\Variables $variables * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @throws \RuntimeException @@ -41,7 +41,7 @@ public function __construct( \Magento\Framework\Registry $registry, \Magento\Framework\Data\FormFactory $formFactory, \Magento\Variable\Model\VariableFactory $variableFactory, - \Magento\Email\Model\Source\Variables $variables, + \Magento\Variable\Model\Source\Variables $variables, array $data = [], \Magento\Framework\Serialize\Serializer\Json $serializer = null ) { @@ -174,7 +174,7 @@ protected function _prepareForm() */ public function getEmailTemplate() { - return $this->_coreRegistry->registry('current_email_template'); + return $this->getData('email_template'); } /** @@ -184,16 +184,14 @@ public function getEmailTemplate() */ public function getVariables() { - $variables = []; - $variables[] = $this->_variables->toOptionArray(true); + $variables = $this->_variables->toOptionArray(true); $customVariables = $this->_variableFactory->create()->getVariablesOptionArray(true); if ($customVariables) { - $variables[] = $customVariables; + $variables = array_merge_recursive($variables, $customVariables); } - /* @var $template \Magento\Email\Model\Template */ - $template = $this->_coreRegistry->registry('current_email_template'); + $template = $this->getEmailTemplate(); if ($template->getId() && ($templateVariables = $template->getVariablesOptionArray(true))) { - $variables[] = $templateVariables; + $variables = array_merge_recursive($variables, $templateVariables); } return $variables; } diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php index a987afb1b527c..50153b2bb6520 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php @@ -23,6 +23,7 @@ abstract class Template extends \Magento\Backend\App\Action * Core registry * * @var \Magento\Framework\Registry + * @deprecated since 2.3.0 in favor of stateful global objects elimination. */ protected $_coreRegistry = null; @@ -49,12 +50,6 @@ protected function _initTemplate($idFieldName = 'template_id') if ($id) { $model->load($id); } - if (!$this->_coreRegistry->registry('email_template')) { - $this->_coreRegistry->register('email_template', $model); - } - if (!$this->_coreRegistry->registry('current_email_template')) { - $this->_coreRegistry->register('current_email_template', $model); - } return $model; } } diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/DefaultTemplate.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/DefaultTemplate.php index 4f74e653b441d..4e8fdcd0c1878 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/DefaultTemplate.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/DefaultTemplate.php @@ -66,7 +66,13 @@ public function execute() ); $templateBlock = $this->_view->getLayout()->createBlock( - \Magento\Email\Block\Adminhtml\Template\Edit::class + \Magento\Email\Block\Adminhtml\Template\Edit::class, + 'template_edit', + [ + 'data' => [ + 'email_template' => $template + ] + ] ); $template->setData('orig_template_currently_used_for', $templateBlock->getCurrentlyUsedForPaths(false)); diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php index ce6e86928a9bc..240b688402b7e 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php @@ -33,7 +33,12 @@ public function execute() $this->_addContent( $this->_view->getLayout()->createBlock( \Magento\Email\Block\Adminhtml\Template\Edit::class, - 'template_edit' + 'template_edit', + [ + 'data' => [ + 'email_template' => $template + ] + ] )->setEditMode( (bool)$this->getRequest()->getParam('id') ) diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index fa9d28074bf85..4830ecfbb74b3 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Email\Model; use Magento\Framework\App\Filesystem\DirectoryList; @@ -289,7 +290,7 @@ public function loadDefault($templateId) /** * trim copyright message */ - if (preg_match('/^/m', $templateText, $matches) && strpos($matches[0], 'Copyright') > 0) { + if (preg_match('/^/m', $templateText, $matches) && strpos($matches[0], 'Copyright') !== false) { $templateText = str_replace($matches[0], '', $templateText); } @@ -535,7 +536,7 @@ protected function cancelDesignConfig() public function setForcedArea($templateId) { if ($this->area) { - throw new \LogicException(__('Area is already set')); + throw new \LogicException(__('The area is already set.')); } $this->area = $this->emailConfig->getTemplateArea($templateId); return $this; @@ -605,7 +606,9 @@ public function getDesignConfig() public function setDesignConfig(array $config) { if (!isset($config['area']) || !isset($config['store'])) { - throw new LocalizedException(__('Design config must have area and store.')); + throw new LocalizedException( + __('The design config needs an area and a store. Verify that both are set and try again.') + ); } $this->getDesignConfig()->setData($config); return $this; diff --git a/app/code/Magento/Email/Model/Source/Variables.php b/app/code/Magento/Email/Model/Source/Variables.php deleted file mode 100644 index 4ee08b0698ec2..0000000000000 --- a/app/code/Magento/Email/Model/Source/Variables.php +++ /dev/null @@ -1,85 +0,0 @@ -_configVariables = [ - [ - 'value' => Store::XML_PATH_UNSECURE_BASE_URL, - 'label' => __('Base Unsecure URL'), - ], - ['value' => Store::XML_PATH_SECURE_BASE_URL, 'label' => __('Base Secure URL')], - ['value' => 'trans_email/ident_general/name', 'label' => __('General Contact Name')], - ['value' => 'trans_email/ident_general/email', 'label' => __('General Contact Email')], - ['value' => 'trans_email/ident_sales/name', 'label' => __('Sales Representative Contact Name')], - ['value' => 'trans_email/ident_sales/email', 'label' => __('Sales Representative Contact Email')], - ['value' => 'trans_email/ident_custom1/name', 'label' => __('Custom1 Contact Name')], - ['value' => 'trans_email/ident_custom1/email', 'label' => __('Custom1 Contact Email')], - ['value' => 'trans_email/ident_custom2/name', 'label' => __('Custom2 Contact Name')], - ['value' => 'trans_email/ident_custom2/email', 'label' => __('Custom2 Contact Email')], - ['value' => 'general/store_information/name', 'label' => __('Store Name')], - ['value' => 'general/store_information/phone', 'label' => __('Store Phone Number')], - ['value' => 'general/store_information/hours', 'label' => __('Store Hours')], - ['value' => 'general/store_information/country_id', 'label' => __('Country')], - ['value' => 'general/store_information/region_id', 'label' => __('Region/State')], - ['value' => 'general/store_information/postcode', 'label' => __('Zip/Postal Code')], - ['value' => 'general/store_information/city', 'label' => __('City')], - ['value' => 'general/store_information/street_line1', 'label' => __('Street Address 1')], - ['value' => 'general/store_information/street_line2', 'label' => __('Street Address 2')], - ['value' => 'general/store_information/merchant_vat_number', 'label' => __('VAT Number')], - ]; - } - - /** - * Retrieve option array of store contact variables - * - * @param bool $withGroup - * @return array - */ - public function toOptionArray($withGroup = false) - { - $optionArray = []; - foreach ($this->getData() as $variable) { - $optionArray[] = [ - 'value' => '{{config path="' . $variable['value'] . '"}}', - 'label' => $variable['label'], - ]; - } - if ($withGroup && $optionArray) { - $optionArray = ['label' => __('Store Contact Information'), 'value' => $optionArray]; - } - return $optionArray; - } - - /** - * Return available config variables - * - * @return array - * @codeCoverageIgnore - */ - public function getData() - { - return $this->_configVariables; - } -} diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 7ac9b944d03ed..1d8218f90a0b2 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -160,7 +160,7 @@ class Filter extends \Magento\Framework\Filter\Template private $cssInliner; /** - * @var \Magento\Email\Model\Source\Variables + * @var \Magento\Variable\Model\Source\Variables */ protected $configVariables; @@ -187,7 +187,7 @@ class Filter extends \Magento\Framework\Filter\Template * @param \Magento\Framework\App\State $appState * @param \Magento\Framework\UrlInterface $urlModel * @param \Pelago\Emogrifier $emogrifier - * @param \Magento\Email\Model\Source\Variables $configVariables + * @param \Magento\Variable\Model\Source\Variables $configVariables * @param array $variables * @param \Magento\Framework\Css\PreProcessor\Adapter\CssInliner|null $cssInliner * @@ -206,7 +206,7 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\UrlInterface $urlModel, \Pelago\Emogrifier $emogrifier, - \Magento\Email\Model\Source\Variables $configVariables, + \Magento\Variable\Model\Source\Variables $configVariables, $variables = [], \Magento\Framework\Css\PreProcessor\Adapter\CssInliner $cssInliner = null ) { @@ -516,7 +516,7 @@ public function viewDirective($construction) */ public function mediaDirective($construction) { - $params = $this->getParameters($construction[2]); + $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); return $this->_storeManager->getStore() ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; } diff --git a/app/code/Magento/Email/Setup/InstallSchema.php b/app/code/Magento/Email/Setup/InstallSchema.php deleted file mode 100644 index 1193df3f8597d..0000000000000 --- a/app/code/Magento/Email/Setup/InstallSchema.php +++ /dev/null @@ -1,123 +0,0 @@ -getConnection()->newTable( - $installer->getTable('email_template') - )->addColumn( - 'template_id', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], - 'Template ID' - )->addColumn( - 'template_code', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 150, - ['nullable' => false], - 'Template Name' - )->addColumn( - 'template_text', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - '64k', - ['nullable' => false], - 'Template Content' - )->addColumn( - 'template_styles', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - '64k', - [], - 'Templste Styles' - )->addColumn( - 'template_type', - \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, - null, - ['unsigned' => true], - 'Template Type' - )->addColumn( - 'template_subject', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 200, - ['nullable' => false], - 'Template Subject' - )->addColumn( - 'template_sender_name', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 200, - [], - 'Template Sender Name' - )->addColumn( - 'template_sender_email', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 200, - [], - 'Template Sender Email' - )->addColumn( - 'added_at', - \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, - null, - ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT], - 'Date of Template Creation' - )->addColumn( - 'modified_at', - \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, - null, - ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT_UPDATE], - 'Date of Template Modification' - )->addColumn( - 'orig_template_code', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - 200, - [], - 'Original Template Code' - )->addColumn( - 'orig_template_variables', - \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - '64k', - [], - 'Original Template Variables' - )->addIndex( - $installer->getIdxName( - 'email_template', - ['template_code'], - \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE - ), - ['template_code'], - ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE] - )->addIndex( - $installer->getIdxName('email_template', ['added_at']), - ['added_at'] - )->addIndex( - $installer->getIdxName('email_template', ['modified_at']), - ['modified_at'] - )->setComment( - 'Email Templates' - ); - - $installer->getConnection()->createTable($table); - } -} diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Edit/FormTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Edit/FormTest.php index 09538ba1932f0..3e6f41877940e 100644 --- a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Edit/FormTest.php +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Edit/FormTest.php @@ -13,10 +13,7 @@ class FormTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Email\Block\Adminhtml\Template\Edit\Form */ protected $form; - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Email\Model\Source\Variables|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Variable\Model\Source\Variables|\PHPUnit_Framework_MockObject_MockObject */ protected $variablesMock; /** @var \Magento\Variable\Model\VariableFactory|\PHPUnit_Framework_MockObject_MockObject */ @@ -30,11 +27,7 @@ class FormTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->setMethods(['registry']) - ->getMock(); - $this->variablesMock = $this->getMockBuilder(\Magento\Email\Model\Source\Variables::class) + $this->variablesMock = $this->getMockBuilder(\Magento\Variable\Model\Source\Variables::class) ->disableOriginalConstructor() ->setMethods(['toOptionArray']) ->getMock(); @@ -54,7 +47,6 @@ protected function setUp() $this->form = $objectManager->getObject( \Magento\Email\Block\Adminhtml\Template\Edit\Form::class, [ - 'registry' => $this->registryMock, 'variableFactory' => $this->variableFactoryMock, 'variables' => $this->variablesMock ] @@ -75,9 +67,7 @@ public function testGetVariables() $this->variableMock->expects($this->once()) ->method('getVariablesOptionArray') ->willReturn(['custom var 1', 'custom var 2']); - $this->registryMock->expects($this->once()) - ->method('registry') - ->willReturn($this->templateMock); + $this->form->setEmailTemplate($this->templateMock); $this->templateMock->expects($this->once()) ->method('getId') ->willReturn(1); @@ -85,7 +75,7 @@ public function testGetVariables() ->method('getVariablesOptionArray') ->willReturn(['template var 1', 'template var 2']); $this->assertEquals( - [['var1', 'var2', 'var3'], ['custom var 1', 'custom var 2'], ['template var 1', 'template var 2']], + ['var1', 'var2', 'var3', 'custom var 1', 'custom var 2', 'template var 1', 'template var 2'], $this->form->getVariables() ); } @@ -95,9 +85,7 @@ public function testGetVariables() */ public function testGetEmailTemplate() { - $this->registryMock->expects($this->once()) - ->method('registry') - ->with('current_email_template'); - $this->form->getEmailTemplate(); + $this->form->setEmailTemplate($this->templateMock); + $this->assertEquals($this->templateMock, $this->form->getEmailTemplate()); } } diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/EditTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/EditTest.php index aa531e8189cea..05e4986a79382 100644 --- a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/EditTest.php +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/EditTest.php @@ -17,11 +17,6 @@ class EditTest extends \PHPUnit\Framework\TestCase */ protected $_block; - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_registryMock; - /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -49,7 +44,6 @@ class EditTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_registryMock = $this->createMock(\Magento\Framework\Registry::class); $layoutMock = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['helper']); $helperMock = $this->createMock(\Magento\Backend\Helper\Data::class); $menuConfigMock = $this->createMock(\Magento\Backend\Model\Menu\Config::class); @@ -66,7 +60,7 @@ protected function setUp() ['getFilesystem', '__wakeup', 'getPath', 'getDirectoryRead'] ); - $viewFilesystem = $this->getMockBuilder(\Magento\Framework\View\Filesystem::class) + $viewFilesystem = $this->getMockBuilder(\Magento\Framework\View\FileSystem::class) ->setMethods(['getTemplateFileName']) ->disableOriginalConstructor() ->getMock(); @@ -80,7 +74,6 @@ protected function setUp() $params = [ 'urlBuilder' => $urlBuilder, - 'registry' => $this->_registryMock, 'layout' => $layoutMock, 'menuConfig' => $menuConfigMock, 'configStructure' => $this->_configStructureMock, @@ -157,10 +150,7 @@ public function testGetCurrentlyUsedForPaths() ->method('getSystemConfigPathsWhereCurrentlyUsed') ->will($this->returnValue($this->_fixtureConfigPath)); - $this->_registryMock->expects($this->once()) - ->method('registry') - ->with('current_email_template') - ->will($this->returnValue($templateMock)); + $this->_block->setEmailTemplate($templateMock); $actual = $this->_block->getCurrentlyUsedForPaths(false); $expected = [ diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php index b39f5772003fa..e11d7fc4db534 100644 --- a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Email\Test\Unit\Block\Adminhtml\Template; /** @@ -92,7 +90,10 @@ public function testToHtml($requestParamMap) ->with(\Magento\Email\Model\AbstractTemplate::DEFAULT_DESIGN_AREA, [$template, 'getProcessedTemplate']) ->willReturn($template->getProcessedTemplate()); - $context = $this->createPartialMock(\Magento\Backend\Block\Template\Context::class, ['getRequest', 'getEventManager', 'getScopeConfig', 'getDesignPackage', 'getStoreManager', 'getAppState']); + $context = $this->createPartialMock( + \Magento\Backend\Block\Template\Context::class, + ['getRequest', 'getEventManager', 'getScopeConfig', 'getDesignPackage', 'getStoreManager', 'getAppState'] + ); $context->expects($this->any())->method('getRequest')->willReturn($request); $context->expects($this->any())->method('getEventManager')->willReturn($eventManage); $context->expects($this->any())->method('getScopeConfig')->willReturn($scopeConfig); diff --git a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/EditTest.php b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/EditTest.php index 9b854d7779140..3804498b5f526 100644 --- a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/EditTest.php +++ b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/EditTest.php @@ -16,11 +16,6 @@ class EditTest extends \PHPUnit\Framework\TestCase */ protected $editController; - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $registryMock; - /** * @var \Magento\Backend\App\Action\Context */ @@ -71,16 +66,17 @@ class EditTest extends \PHPUnit\Framework\TestCase */ protected $pageTitleMock; + /** + * @var \Magento\Email\Model\Template|\PHPUnit_Framework_MockObject_MockObject + */ + private $templateMock; + /** * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->setMethods(['registry', 'register']) - ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() ->getMock(); @@ -114,6 +110,19 @@ protected function setUp() $this->pageTitleMock = $this->getMockBuilder(\Magento\Framework\View\Page\Title::class) ->disableOriginalConstructor() ->getMock(); + $this->templateMock = $this->getMockBuilder(\Magento\Email\Model\BackendTemplate::class) + ->setMethods(['getId', 'getTemplateCode', 'load']) + ->disableOriginalConstructor() + ->getMock(); + $this->templateMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->templateMock->expects($this->any()) + ->method('getTemplateCode') + ->willReturn('My Template'); + $this->templateMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); $this->viewMock->expects($this->atLeastOnce()) ->method('getLayout') @@ -144,33 +153,27 @@ protected function setUp() ->willReturn($this->pageTitleMock); $this->layoutMock->expects($this->once()) ->method('createBlock') - ->with(\Magento\Email\Block\Adminhtml\Template\Edit::class, 'template_edit', []) - ->willReturn($this->editBlockMock); + ->with( + \Magento\Email\Block\Adminhtml\Template\Edit::class, + 'template_edit', + [ + 'data' => [ + 'email_template' => $this->templateMock + ] + ] + )->willReturn($this->editBlockMock); $this->editBlockMock->expects($this->once()) ->method('setEditMode') ->willReturnSelf(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $templateMock = $this->getMockBuilder(\Magento\Email\Model\Template::class) - ->setMethods(['getId', 'getTemplateCode', 'load']) - ->disableOriginalConstructor() - ->getMock(); - $templateMock->expects($this->once()) - ->method('getId') - ->willReturn(1); - $templateMock->expects($this->any()) - ->method('getTemplateCode') - ->willReturn('My Template'); - $templateMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); $objectManagerMock = $this->getMockBuilder(\Magento\Framework\App\ObjectManager::class) ->disableOriginalConstructor() ->getMock(); $objectManagerMock->expects($this->once()) ->method('create') ->with(\Magento\Email\Model\BackendTemplate::class) - ->willReturn($templateMock); + ->willReturn($this->templateMock); $this->context = $objectManager->getObject( \Magento\Backend\App\Action\Context::class, [ @@ -183,7 +186,6 @@ protected function setUp() \Magento\Email\Controller\Adminhtml\Email\Template\Edit::class, [ 'context' => $this->context, - 'coreRegistry' => $this->registryMock ] ); } @@ -197,14 +199,6 @@ public function testExecuteNewTemplate() ->method('getParam') ->with('id') ->willReturn(0); - $this->registryMock->expects($this->atLeastOnce()) - ->method('registry') - ->willReturnMap( - [ - ['email_template', true], - ['current_email_template', true] - ] - ); $this->pageTitleMock->expects($this->any()) ->method('prepend') ->willReturnMap( @@ -234,14 +228,6 @@ public function testExecuteEdit() ->method('getParam') ->with('id') ->willReturn(1); - $this->registryMock->expects($this->atLeastOnce()) - ->method('registry') - ->willReturnMap( - [ - ['email_template', false], - ['current_email_template', false] - ] - ); $this->pageTitleMock->expects($this->any()) ->method('prepend') ->willReturnMap( diff --git a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/IndexTest.php b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/IndexTest.php index 05a260e8d9876..0c1e913399d72 100644 --- a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/IndexTest.php +++ b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/IndexTest.php @@ -16,11 +16,6 @@ class IndexTest extends \PHPUnit\Framework\TestCase */ protected $indexController; - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $registryMock; - /** * @var \Magento\Backend\App\Action\Context */ @@ -113,7 +108,6 @@ protected function setUp() \Magento\Email\Controller\Adminhtml\Email\Template\Index::class, [ 'context' => $this->context, - 'coreRegistry' => $this->registryMock ] ); } diff --git a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php index 143af9b34df8e..0ba717a461718 100644 --- a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php +++ b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php @@ -9,7 +9,6 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\View; -use Magento\Framework\Registry; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Config; use Magento\Framework\View\Page\Title; @@ -32,11 +31,6 @@ class PreviewTest extends \PHPUnit\Framework\TestCase */ protected $context; - /** - * @var Registry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $coreRegistryMock; - /** * @var View|\PHPUnit_Framework_MockObject_MockObject */ @@ -108,7 +102,6 @@ protected function setUp() \Magento\Email\Controller\Adminhtml\Email\Template\Preview::class, [ 'context' => $this->context, - 'coreRegistry' => $this->coreRegistryMock, ] ); } diff --git a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php index 76bbba7406609..46f3fecfb8848 100644 --- a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Test class for \Magento\Email\Model\AbstractTemplate. */ @@ -21,11 +19,6 @@ class AbstractTemplateTest extends \PHPUnit\Framework\TestCase */ private $design; - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - private $registry; - /** * @var \Magento\Store\Model\App\Emulation|\PHPUnit_Framework_MockObject_MockObject */ @@ -136,7 +129,6 @@ protected function getModelMock(array $mockedMethods = []) \Magento\Email\Model\AbstractTemplate::class, [ 'design' => $this->design, - 'registry' => $this->registry, 'appEmulation' => $this->appEmulation, 'storeManager' => $this->storeManager, 'filesystem' => $this->filesystem, @@ -244,7 +236,8 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e /** * @expectedException \LogicException */ - public function testGetProcessedTemplateException() { + public function testGetProcessedTemplateException() + { $filterTemplate = $this->getMockBuilder(\Magento\Email\Model\Template\Filter::class) ->setMethods([ 'setUseSessionInUrl', diff --git a/app/code/Magento/Email/Test/Unit/Model/BackendTemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/BackendTemplateTest.php index a3bbae826f15a..31a04b0b2bbd0 100644 --- a/app/code/Magento/Email/Test/Unit/Model/BackendTemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/BackendTemplateTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Test class for Magento\Email\Model\BackendTemplate. */ @@ -59,7 +57,9 @@ protected function setUp() $this->structureMock->expects($this->any())->method('getFieldPathsByAttribute')->willReturn(['path' => 'test']); $this->resourceModelMock = $this->createMock(\Magento\Email\Model\ResourceModel\Template::class); - $this->resourceModelMock->expects($this->any())->method('getSystemConfigByPathsAndTemplateId')->willReturn(['test_config' => 2015]); + $this->resourceModelMock->expects($this->any()) + ->method('getSystemConfigByPathsAndTemplateId') + ->willReturn(['test_config' => 2015]); /** @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject $objectManagerMock*/ $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); $objectManagerMock->expects($this->any()) diff --git a/app/code/Magento/Email/Test/Unit/Model/Source/VariablesTest.php b/app/code/Magento/Email/Test/Unit/Model/Source/VariablesTest.php deleted file mode 100644 index 1f4a1040f4450..0000000000000 --- a/app/code/Magento/Email/Test/Unit/Model/Source/VariablesTest.php +++ /dev/null @@ -1,92 +0,0 @@ -model = $helper->getObject(\Magento\Email\Model\Source\Variables::class); - $this->configVariables = [ - [ - 'value' => Store::XML_PATH_UNSECURE_BASE_URL, - 'label' => __('Base Unsecure URL'), - ], - ['value' => Store::XML_PATH_SECURE_BASE_URL, 'label' => __('Base Secure URL')], - ['value' => 'trans_email/ident_general/name', 'label' => __('General Contact Name')], - ['value' => 'trans_email/ident_general/email', 'label' => __('General Contact Email')], - ['value' => 'trans_email/ident_sales/name', 'label' => __('Sales Representative Contact Name')], - ['value' => 'trans_email/ident_sales/email', 'label' => __('Sales Representative Contact Email')], - ['value' => 'trans_email/ident_custom1/name', 'label' => __('Custom1 Contact Name')], - ['value' => 'trans_email/ident_custom1/email', 'label' => __('Custom1 Contact Email')], - ['value' => 'trans_email/ident_custom2/name', 'label' => __('Custom2 Contact Name')], - ['value' => 'trans_email/ident_custom2/email', 'label' => __('Custom2 Contact Email')], - ['value' => 'general/store_information/name', 'label' => __('Store Name')], - ['value' => 'general/store_information/phone', 'label' => __('Store Phone Number')], - ['value' => 'general/store_information/hours', 'label' => __('Store Hours')], - ['value' => 'general/store_information/country_id', 'label' => __('Country')], - ['value' => 'general/store_information/region_id', 'label' => __('Region/State')], - ['value' => 'general/store_information/postcode', 'label' => __('Zip/Postal Code')], - ['value' => 'general/store_information/city', 'label' => __('City')], - ['value' => 'general/store_information/street_line1', 'label' => __('Street Address 1')], - ['value' => 'general/store_information/street_line2', 'label' => __('Street Address 2')], - ['value' => 'general/store_information/merchant_vat_number', 'label' => __('VAT Number')], - ]; - } - - public function testToOptionArrayWithoutGroup() - { - $optionArray = $this->model->toOptionArray(); - $this->assertEquals(count($this->configVariables), count($optionArray)); - - $index = 0; - foreach ($optionArray as $variable) { - $expectedValue = '{{config path="' . $this->configVariables[$index]['value'] . '"}}'; - $expectedLabel = $this->configVariables[$index]['label']; - $this->assertEquals($expectedValue, $variable['value']); - $this->assertEquals($expectedLabel, $variable['label']->getText()); - $index++; - } - } - - public function testToOptionArrayWithGroup() - { - $optionArray = $this->model->toOptionArray(true); - $this->assertEquals('Store Contact Information', $optionArray['label']->getText()); - - $optionArrayValues = $optionArray['value']; - $this->assertEquals(count($this->configVariables), count($optionArrayValues)); - - $index = 0; - foreach ($optionArrayValues as $variable) { - $expectedValue = '{{config path="' . $this->configVariables[$index]['value'] . '"}}'; - $expectedLabel = $this->configVariables[$index]['label']; - $this->assertEquals($expectedValue, $variable['value']); - $this->assertEquals($expectedLabel, $variable['label']->getText()); - $index++; - } - } -} diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Config/FileIteratorTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Config/FileIteratorTest.php index 38429328b4432..c5165cc16793c 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/Config/FileIteratorTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Config/FileIteratorTest.php @@ -93,10 +93,8 @@ public function testIteratorNegative() { $filePath = $this->filePaths[0]; - $this->expectException( - 'UnexpectedValueException', - sprintf("Unable to determine a module, file '%s' belongs to.", $filePath) - ); + $this->expectException('UnexpectedValueException'); + $this->expectExceptionMessage(sprintf("Unable to determine a module, file '%s' belongs to.", $filePath)); $this->moduleDirResolverMock->expects($this->at(0)) ->method('getModuleName') diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php index f7ba4b7424cc6..bb7ac6934106a 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php @@ -6,8 +6,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Email\Test\Unit\Model\Template\Config; class XsdTest extends \PHPUnit\Framework\TestCase @@ -31,6 +29,7 @@ public function testMergedXml($fixtureXml, array $expectedErrors) public function mergedXmlDataProvider() { + // @codingStandardsIgnoreStart return [ 'valid' => [ '