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..2b1720ccaabae 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,24 +1,36 @@ - - + - + Please also have a look at our guidelines article before adding a new issue https://github.com/magento/magento2/wiki/Issue-reporting-guidelines +--> + +### Preconditions (*) + 1. 2. -### Steps to reproduce - +### Steps to reproduce (*) + 1. 2. 3. -### Expected result +### Expected result (*) -1. +1. [Screenshots, logs or description] -### Actual result +### Actual result (*) -1. [Screenshot, logs] - - +1. [Screenshots, logs or description] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000..33a6ef02ace11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Technical issue with the Magento 2 core components + +--- + + + +### Preconditions (*) + +1. +2. + +### Steps to reproduce (*) + +1. +2. + +### Expected result (*) + +1. [Screenshots, logs or description] +2. + +### Actual result (*) + +1. [Screenshots, logs or description] +2. diff --git a/.github/ISSUE_TEMPLATE/developer-experience-issue.md b/.github/ISSUE_TEMPLATE/developer-experience-issue.md new file mode 100644 index 0000000000000..423d4818fb31c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/developer-experience-issue.md @@ -0,0 +1,19 @@ +--- +name: Developer experience issue +about: Issues related to customization, extensibility, modularity + +--- + + + +### Summary (*) + + +### Examples (*) + + +### Proposed solution + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000..f64185773cab4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Please consider reporting directly to https://github.com/magento/community-features + +--- + + + +### Description (*) + + +### Expected behavior (*) + + +### Benefits + + +### Additional information + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1f01ba9f2640..f191bd9aaba67 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,37 @@ - + -### Description - + + +### Description (*) + ### Fixed Issues (if relevant) - + 1. magento/magento2#: Issue title 2. ... -### Manual testing scenarios - +### Manual testing scenarios (*) + 1. ... 2. ... -### Contribution checklist +### Contribution checklist (*) - [ ] Pull request has a meaningful description of its purpose - [ ] All commits are accompanied by meaningful commit messages - [ ] All new or changed code is covered with unit/integration tests (if applicable) diff --git a/.gitignore b/.gitignore index 8831ad0a17c39..68d38d9ca7817 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ atlassian* /.php_cs /.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 6247830fa8d14..d22b5a1395cae 100644 --- a/.htaccess +++ b/.htaccess @@ -355,6 +355,15 @@ 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 ErrorDocument 404 /pub/errors/404.php diff --git a/.htaccess.sample b/.htaccess.sample index 8e6e702ced716..c9ddff2cca4cf 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -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 ############################################ @@ -331,6 +332,15 @@ 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 ErrorDocument 404 /pub/errors/404.php 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..d29aa241b15b6 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 @@ -30,16 +33,19 @@ env: - TEST_SUITE=integration INTEGRATION_INDEX=2 - TEST_SUITE=integration INTEGRATION_INDEX=3 - TEST_SUITE=functional + - TEST_SUITE=graphql-api-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 + - php: 7.1 + env: TEST_SUITE=graphql-api-functional cache: apt: true directories: @@ -47,7 +53,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: @@ -56,5 +64,6 @@ script: # The scripts for grunt/phpunit type tests - if [ $TEST_SUITE == "functional" ]; then dev/tests/functional/vendor/phpunit/phpunit/phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi - - if [ $TEST_SUITE != "functional" ] && [ $TEST_SUITE != "js" ]; then phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi + - if [ $TEST_SUITE != "functional" ] && [ $TEST_SUITE != "js" ] && [ $TEST_SUITE != "graphql-api-functional" ]; then phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi - if [ $TEST_SUITE == "js" ]; then grunt $GRUNT_COMMAND; fi + - if [ $TEST_SUITE == "graphql-api-functional" ]; then phpunit -c dev/tests/api-functional; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index ef841ec0337f2..b86c7b79a0cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +2.3.0 +============= +To get detailed information about changes in Magento 2.3.0, see the [Release Notes](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html) + 2.1.0 ============= To get detailed information about changes in Magento 2.1.0, please visit [Magento Community Edition (CE) Release Notes](http://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.0CE.html "Magento Community Edition (CE) Release Notes") @@ -153,7 +157,7 @@ To get detailed information about changes in Magento 2.1.0, please visit [Magent * Updated styles * Sample Data: * Improved sample data installation UX - * Updated sample data with Product Heros, color swatches, MAP and rule based product relations + * Updated sample data with Product Heroes, color swatches, MAP and rule based product relations * Improved sample data upgrade flow * Added the ability to log errors and set the error flag during sample data installation * Various improvements: @@ -1977,7 +1981,7 @@ Tests: * [#686](https://github.com/magento/magento2/issues/686) -- Product save validation errors in the admin don't hide the overlay * [#702](https://github.com/magento/magento2/issues/702) -- Base table or view not found * [#652](https://github.com/magento/magento2/issues/652) -- Multishipping checkout not to change the Billing address js issue - * [#648](https://github.com/magento/magento2/issues/648) -- An equal (=) sign in the hash of the product page to to break the tabs functionality + * [#648](https://github.com/magento/magento2/issues/648) -- An equal (=) sign in the hash of the product page to break the tabs functionality * Service Contracts: * Refactored usage of new API of the Customer module * Implemented Service Contracts for the Sales module @@ -2280,7 +2284,7 @@ Tests: * Fixed an issue where no results were found for Coupons reports * Fixed an issue with incremental Qty setting * Fixed an issue with allowing importing of negative weight values - * Fixed an issue with Inventory - Only X left Treshold being not dependent on Qty for Item's Status to Become Out of Stock + * Fixed an issue with Inventory - Only X left Threshold being not dependent on Qty for Item's Status to Become Out of Stock * Fixed an issue where the "Catalog Search Index index was rebuilt." message was displayed when reindexing the Catalog Search index * Search module: * Integrated the Search library to the advanced search functionality @@ -2702,7 +2706,7 @@ Tests: * Ability to support extensible service data objects * No Code Duplication in Root Templates * Fixed bugs: - * Persistance session application. Loggin out the customer + * Persistence session application. Logging out the customer * Placing the order with two terms and conditions * Saving of custom option by service catalogProductCustomOptionsWriteServiceV1 * Placing the order on frontend if enter in the street address line 1 and 2 255 symbols @@ -2961,7 +2965,7 @@ Tests: * Fixed an issue with incorrect items label for the cases when there are more than one item in the category * Fixed an issue when configurable product was out of stock in Google Shopping while being in stock in the Magento backend * Fixed an issue when swipe gesture in menu widget was not supported on mobile - * Fixed an issue when it was impossible to enter alpha-numeric zip code on the stage of estimating shipping and tax rates + * Fixed an issue when it was impossible to enter alphanumeric zip code on the stage of estimating shipping and tax rates * Fixed an issue when custom price was not applied when editing an order * Fixed an issue when items were not returned to stock after unsuccessful order was placed * Fixed an issue when error message appeared "Cannot save the credit memo” while creating credit memo 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..e73da84d66f46 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -[![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) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/magento-2/localized.svg)](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](https://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](https://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,38 +21,51 @@ 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]: +[4]: -

Labels applied by the Magento team

+

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. -| Label | Description | -| ------------- |-------------| -| ![DOC](http://devdocs.magento.com/common/images/github_DOC.png) | Affects Documentation domain. | -| ![PROD](http://devdocs.magento.com/common/images/github_PROD.png) | Affects the Product team (mostly feature requests or business logic change). | -| ![TECH](http://devdocs.magento.com/common/images/github_TECH.png) | Affects Architect Group (mostly to make decisions around technology changes). | -| ![accept](http://devdocs.magento.com/common/images/github_accept.png) | The pull request has been accepted and will be merged into mainline code. | -| ![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. | + + + -

Reporting security issues

+

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. + + + -To report security vulnerabilities in Magento software or web sites, please e-mail security@magento.com. Please do not report security issues using GitHub. Be sure to encrypt your e-mail with our encryption key if it includes sensitive information. Learn more about reporting security issues here. +### Labels applied by the Magento team +We apply labels to public Pull Requests and Issues to help other participants retrieve additional information about current progress, component assignments, Magento release lines, and much more. +Please review the [Code Contributions guide](https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing.html#labels) for detailed information on labels used in Magento 2 repositories. -Stay up-to-date on the latest security news and patches for Magento by signing up for Security Alert Notifications. +## Reporting security issues -

License

+To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account [there](https://bugcrowd.com/magento) to submit and follow-up your issue. Learn more about reporting security issues [here](https://magento.com/security/reporting-magento-security-issue). -Each Magento source file included in this distribution is licensed under OSL 3.0 or the Magento Enterprise Edition (MEE) license +Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications](https://magento.com/security/sign-up). -http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) -Please see LICENSE.txt for the full text of the OSL 3.0 license or contact license@magentocommerce.com for a copy. +## License + +Each Magento source file included in this distribution is licensed under OSL 3.0 or the Magento Enterprise Edition (MEE) license. + +[Open Software License (OSL 3.0)](https://opensource.org/licenses/osl-3.0.php). +Please see [LICENSE.txt](https://github.com/magento/magento2/blob/2.3-develop/LICENSE.txt) for the full text of the OSL 3.0 license or contact license@magentocommerce.com for a copy. Subject to Licensee's payment of fees and compliance with the terms and conditions of the MEE License, the MEE License supersedes the OSL 3.0 license for each source file. -Please see LICENSE_EE.txt for the full text of the MEE License or visit http://magento.com/legal/terms/enterprise. +Please see LICENSE_EE.txt for the full text of the MEE License or visit https://magento.com/legal/terms/enterprise. + +## Community Engineering Slack + +To connect with Magento and the Community, join us on the [Magento Community Engineering Slack](https://magentocommeng.slack.com). If you are interested in joining Slack, or a specific channel, send us request at [engcom@adobe.com](mailto:engcom@adobe.com) or [self signup](https://tinyurl.com/engcom-slack). + + +We have channels for each project. These channels are recommended for new members: +- [general](https://magentocommeng.slack.com/messages/C4YS78WE6): Open chat for introductions and Magento 2 questions +- [github](https://magentocommeng.slack.com/messages/C7KB93M32): Support for GitHub issues, pull requests, and processes +- [public-backlog](https://magentocommeng.slack.com/messages/CCV3J3RV5): Discussions of the Magento 2 backlog diff --git a/app/autoload.php b/app/autoload.php index 54087d0255495..d6407083dc0b3 100644 --- a/app/autoload.php +++ b/app/autoload.php @@ -28,6 +28,9 @@ /* 'composer install' validation */ if (file_exists($vendorAutoload)) { $composerAutoloader = include $vendorAutoload; +} else if (file_exists("{$vendorDir}/autoload.php")) { + $vendorAutoload = "{$vendorDir}/autoload.php"; + $composerAutoloader = include $vendorAutoload; } else { throw new \Exception( 'Vendor autoload is not found. Please run \'composer install\' under application root directory.' diff --git a/app/bootstrap.php b/app/bootstrap.php index 6701a9f4dd51e..0b13d12cece58 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -11,15 +11,15 @@ #ini_set('display_errors', 1); /* PHP version validation */ -if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) { +if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 70103) { 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'; + echo 'Magento supports PHP 7.1.3 or later. ' . + 'Please read https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html'; } else { echo << -

Magento supports PHP 7.0.2, 7.0.4, and 7.0.6 or later. Please read - +

Magento supports PHP 7.1.3 or later. Please read + Magento System Requirements. HTML; @@ -31,8 +31,6 @@ // Sets default autoload mappings, may be overridden in Bootstrap::create \Magento\Framework\App\Bootstrap::populateAutoloader(BP, []); -require_once BP . '/app/functions.php'; - /* Custom umask value may be provided in optional mage_umask file in root */ $umaskFile = BP . '/magento_umask'; $mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002; @@ -49,12 +47,21 @@ 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'], + $profilerConfig = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER']) + ? $_SERVER['MAGE_PROFILER'] + : trim(file_get_contents(BP . '/var/profiler.flag')); + + if ($profilerConfig) { + $profilerConfig = json_decode($profilerConfig, true) ?: $profilerConfig; + } + + Magento\Framework\Profiler::applyConfig( + $profilerConfig, 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..e9b4bfa44893d 100644 --- a/app/code/Magento/AdminNotification/Block/Window.php +++ b/app/code/Magento/AdminNotification/Block/Window.php @@ -8,6 +8,8 @@ namespace Magento\AdminNotification\Block; /** + * Admin notification window block + * * @api * @since 100.0.2 */ @@ -40,7 +42,7 @@ class Window extends \Magento\Backend\Block\Template protected $_criticalCollection; /** - * @var \Magento\Adminnotification\Model\Inbox + * @var \Magento\AdminNotification\Model\Inbox */ protected $_latestItem; @@ -92,16 +94,15 @@ protected function _toHtml() /** * Retrieve latest critical item * - * @return bool|\Magento\Adminnotification\Model\Inbox + * @return bool|\Magento\AdminNotification\Model\Inbox */ 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/Index.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Index.php index 125dba405b108..22eb3b30722f4 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Index.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\AdminNotification\Controller\Adminhtml\Notification; -class Index extends \Magento\AdminNotification\Controller\Adminhtml\Notification +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\AdminNotification\Controller\Adminhtml\Notification implements HttpGetActionInterface { /** * @return void 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/Inbox.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox.php index a92eebfaec510..40b089d947136 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox.php @@ -6,6 +6,8 @@ namespace Magento\AdminNotification\Model\ResourceModel; /** + * Inbox resource model + * * @api * @since 100.0.2 */ @@ -77,8 +79,7 @@ public function getNoticeStatus(\Magento\AdminNotification\Model\Inbox $object) 'is_read=?', 0 ); - $return = $connection->fetchPairs($select); - return $return; + return $connection->fetchPairs($select); } /** 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/Model/System/Message/Media/Synchronization/Error.php b/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Error.php index 38477c015cad3..c08b13373aa8d 100644 --- a/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Error.php +++ b/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Error.php @@ -7,6 +7,8 @@ namespace Magento\AdminNotification\Model\System\Message\Media\Synchronization; /** + * Media synchronization error message class. + * * @api * @since 100.0.2 */ @@ -27,7 +29,7 @@ class Error extends \Magento\AdminNotification\Model\System\Message\Media\Abstra protected function _shouldBeDisplayed() { $data = $this->_syncFlag->getFlagData(); - return isset($data['has_errors']) && true == $data['has_errors']; + return !empty($data['has_errors']); } /** diff --git a/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Success.php b/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Success.php index 81bea14099e9e..ed882a0776734 100644 --- a/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Success.php +++ b/app/code/Magento/AdminNotification/Model/System/Message/Media/Synchronization/Success.php @@ -6,6 +6,8 @@ namespace Magento\AdminNotification\Model\System\Message\Media\Synchronization; /** + * Media synchronization success message class. + * * @api * @since 100.0.2 */ @@ -27,8 +29,8 @@ protected function _shouldBeDisplayed() { $state = $this->_syncFlag->getState(); $data = $this->_syncFlag->getFlagData(); - $hasErrors = isset($data['has_errors']) && true == $data['has_errors'] ? true : false; - return false == $hasErrors && \Magento\MediaStorage\Model\File\Storage\Flag::STATE_FINISHED == $state; + $hasErrors = !empty($data['has_errors']); + return !$hasErrors && \Magento\MediaStorage\Model\File\Storage\Flag::STATE_FINISHED == $state; } /** diff --git a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php index 3275de2a82fb7..24ef712c0f61f 100644 --- a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php +++ b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php @@ -37,7 +37,7 @@ public function __construct( } /** - * Predispath admin action controller + * Predispatch admin action controller * * @param \Magento\Framework\Event\Observer $observer * @return void 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/dev/tests/acceptance/LICENSE.txt b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/LICENSE.txt rename to app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt diff --git a/app/design/frontend/Magento/rush/LICENSE_AFL.txt b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from app/design/frontend/Magento/rush/LICENSE_AFL.txt rename to app/code/Magento/AdminNotification/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/AdminNotification/Test/Mftf/README.md b/app/code/Magento/AdminNotification/Test/Mftf/README.md new file mode 100644 index 0000000000000..33f88ba74200a --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Admin Notification Functional Tests + +The Functional Test Module for **Magento Admin Notification** module. diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php index 2fbfc43aa8775..f49911c3e7a93 100644 --- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php +++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php @@ -62,6 +62,9 @@ public function testGetIdentity($expectedSum, $cacheTypes) $this->assertEquals($expectedSum, $this->_messageModel->getIdentity()); } + /** + * @return array + */ public function getIdentityDataProvider() { $cacheTypeMock1 = $this->createPartialMock(\stdClass::class, ['getCacheType']); @@ -95,6 +98,9 @@ public function testIsDisplayed($expected, $allowed, $cacheTypes) $this->assertEquals($expected, $this->_messageModel->isDisplayed()); } + /** + * @return array + */ public function isDisplayedDataProvider() { $cacheTypesMock = $this->createPartialMock(\stdClass::class, ['getCacheType']); diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php index 2c259db868851..b490efd8e9683 100644 --- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php +++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php @@ -72,6 +72,9 @@ public function testIsDisplayed($expectedFirstRun, $data) $this->assertEquals($expectedFirstRun, $model->isDisplayed()); } + /** + * @return array + */ public function isDisplayedDataProvider() { return [ diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php index 1e71570a5e30b..c6f61fee862ba 100644 --- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php +++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php @@ -76,6 +76,9 @@ public function testIsDisplayed($expectedResult, $cached, $response) $this->assertEquals($expectedResult, $this->_messageModel->isDisplayed()); } + /** + * @return array + */ public function isDisplayedDataProvider() { return [ 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/adminhtml/menu.xml b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml index fbed5c0960b73..04d700b9f90ce 100644 --- a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml +++ b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml @@ -7,6 +7,6 @@ -->

- + 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..29d928ced2084 --- /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..b068ffffe9219 --- /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/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml index a97293547e132..0448daaf17644 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml @@ -19,20 +19,12 @@ - + \ No newline at end of file diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/js/system/messages/popup.js b/app/code/Magento/AdminNotification/view/adminhtml/web/js/system/messages/popup.js new file mode 100644 index 0000000000000..39c61d6e07d29 --- /dev/null +++ b/app/code/Magento/AdminNotification/view/adminhtml/web/js/system/messages/popup.js @@ -0,0 +1,26 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Ui/js/modal/modal' +], function ($, modal) { + 'use strict'; + + return function (data, element) { + + if (modal.modal) { + modal.modal.html($(element).html()); + } else { + modal.modal = $(element).modal({ + modalClass: data.class, + type: 'popup', + buttons: [] + }); + } + + modal.modal.modal('openModal'); + }; +}); diff --git a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php index 02413a1899cd7..d78266ab75311 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php +++ b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php @@ -5,12 +5,14 @@ */ namespace Magento\AdvancedPricingImportExport\Controller\Adminhtml\Export; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; use Magento\Framework\Controller\ResultFactory; use Magento\AdvancedPricingImportExport\Model\Export\AdvancedPricing as ExportAdvancedPricing; use Magento\Catalog\Model\Product as CatalogProduct; -class GetFilter extends ExportController +class GetFilter extends ExportController implements HttpGetActionInterface, HttpPostActionInterface { /** * Get grid-filter of entity attributes action. @@ -37,10 +39,10 @@ public function execute() ); return $resultLayout; } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } else { - $this->messageManager->addError(__('Please correct the data sent.')); + $this->messageManager->addErrorMessage(__('Please correct the data sent.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index 7ddd5e3bb2a36..fda6ae9530135 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 @@ -98,7 +104,6 @@ class AdvancedPricing extends \Magento\CatalogImportExport\Model\Export\Product * @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer * @param ImportProduct\StoreResolver $storeResolver * @param \Magento\Customer\Api\GroupRepositoryInterface $groupRepository - * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -187,6 +192,7 @@ protected function initTypeModels() * Export process * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function export() { @@ -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,52 @@ protected function getTierPrices(array $listSku, $table) } /** - * Get Website code + * Get Website code. * * @param int $websiteId * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ - 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 $customerGroupId - * @param null $allGroups + * @param int $groupId + * @param int $allGroups * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - 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..2e17e734b1e60 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -8,7 +8,6 @@ use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -use Magento\Framework\App\ResourceConnection; /** * Class AdvancedPricing @@ -394,7 +393,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 +481,8 @@ protected function deleteProductTierPrices(array $listSku, $table) $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, 0); return false; } - } else { - return false; } + return false; } /** @@ -619,6 +617,7 @@ protected function processCountNewPrices(array $tierPrices) * Get product entity link field * * @return string + * @throws \Exception */ private function getProductEntityLinkField() { diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php index 25a9fc244fe51..d939a3f7c392e 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php @@ -28,6 +28,7 @@ public function __construct($validators = []) * * @param array $value * @return bool + * @throws \Zend_Validate_Exception */ public function isValid($value) { diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/AdvancedPricingImportExport/LICENSE.txt b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/AdvancedPricingImportExport/LICENSE.txt rename to app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/LICENSE_AFL.txt b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/LICENSE_AFL.txt rename to app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/README.md b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/README.md new file mode 100644 index 0000000000000..7b4d0f3f0b12b --- /dev/null +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Advanced Pricing Import Export Functional Tests + +The Functional Test Module for **Magento Advanced Pricing Import Export** module. diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php index 48b4c58918740..57ceb7f5af275 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php @@ -151,10 +151,13 @@ protected function setUp() ] ); $this->exportConfig = $this->createMock(\Magento\ImportExport\Model\Export\Config::class); - $this->productFactory = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\ProductFactory::class, [ + $this->productFactory = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\ProductFactory::class, + [ 'create', 'getTypeId', - ]); + ] + ); $this->attrSetColFactory = $this->createPartialMock( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class, [ @@ -185,11 +188,14 @@ protected function setUp() \Magento\CatalogImportExport\Model\Import\Product\StoreResolver::class ); $this->groupRepository = $this->createMock(\Magento\Customer\Api\GroupRepositoryInterface::class); - $this->writer = $this->createPartialMock(\Magento\ImportExport\Model\Export\Adapter\AbstractAdapter::class, [ - 'setHeaderCols', - 'writeRow', - 'getContents', - ]); + $this->writer = $this->createPartialMock( + \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter::class, + [ + 'setHeaderCols', + 'writeRow', + 'getContents', + ] + ); $constructorMethods = [ 'initTypeModels', 'initAttributes', @@ -213,7 +219,7 @@ protected function setUp() '_getCustomerGroupById', 'correctExportData' ]); - $this->advancedPricing = $this->getMockbuilder( + $this->advancedPricing = $this->getMockBuilder( \Magento\AdvancedPricingImportExport\Model\Export\AdvancedPricing::class ) ->setMethods($mockMethods) @@ -347,6 +353,7 @@ protected function tearDown() * @param $object * @param $property * @return mixed + * @throws \ReflectionException */ protected function getPropertyValue($object, $property) { @@ -362,6 +369,8 @@ protected function getPropertyValue($object, $property) * @param $object * @param $property * @param $value + * @return mixed + * @throws \ReflectionException */ protected function setPropertyValue(&$object, $property, $value) { diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php index bb64acb558320..2c930237da831 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php @@ -181,6 +181,9 @@ public function testIsValidAddMessagesCall($value, $hasEmptyColumns, $customerGr $this->tierPrice->isValid($value); } + /** + * @return array + */ public function isValidResultFalseDataProvider() { return [ @@ -286,6 +289,9 @@ public function isValidResultFalseDataProvider() ]; } + /** + * @return array + */ public function isValidAddMessagesCallDataProvider() { return [ @@ -340,6 +346,7 @@ public function isValidAddMessagesCallDataProvider() * @param object $object * @param string $property * @return mixed + * @throws \ReflectionException */ protected function getPropertyValue($object, $property) { @@ -357,6 +364,7 @@ protected function getPropertyValue($object, $property) * @param string $property * @param mixed $value * @return object + * @throws \ReflectionException */ protected function setPropertyValue(&$object, $property, $value) { 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..d78c4f5e61af3 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(); @@ -103,17 +103,20 @@ public function testGetAllWebsitesValue() $this->webSiteModel->expects($this->once())->method('getBaseCurrency')->willReturn($currency); $expectedResult = AdvancedPricing::VALUE_ALL_WEBSITES . ' [' . $currencyCode . ']'; - $this->websiteString = $this->getMockBuilder( + $websiteString = $this->getMockBuilder( \Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\Website::class ) ->setMethods(['_clearMessages', '_addMessages']) ->setConstructorArgs([$this->storeResolver, $this->webSiteModel]) ->getMock(); - $result = $this->websiteString->getAllWebsitesValue(); + $result = $websiteString->getAllWebsitesValue(); $this->assertEquals($expectedResult, $result); } + /** + * @return array + */ public function isValidReturnDataProvider() { return [ diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php index d9fce98826105..5ca534284a48d 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php @@ -77,6 +77,9 @@ public function testInit() $this->validator->init(null); } + /** + * @return array + */ public function isValidDataProvider() { return [ diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php index 6d130d93ee6a5..340e81746f029 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php @@ -209,6 +209,10 @@ public function testGetEntityTypeCode() * Test method validateRow against its result. * * @dataProvider validateRowResultDataProvider + * @param array $rowData + * @param string|null $behavior + * @param bool $expectedResult + * @throws \ReflectionException */ public function testValidateRowResult($rowData, $behavior, $expectedResult) { @@ -234,6 +238,10 @@ public function testValidateRowResult($rowData, $behavior, $expectedResult) * Test method validateRow whether AddRowError is called. * * @dataProvider validateRowAddRowErrorCallDataProvider + * @param array $rowData + * @param string|null $behavior + * @param string $error + * @throws \ReflectionException */ public function testValidateRowAddRowErrorCall($rowData, $behavior, $error) { @@ -324,6 +332,13 @@ public function testSaveAdvancedPricing() * Take into consideration different data and check relative internal calls. * * @dataProvider saveAndReplaceAdvancedPricesAppendBehaviourDataProvider + * @param array $data + * @param string $tierCustomerGroupId + * @param string $groupCustomerGroupId + * @param string $tierWebsiteId + * @param string $groupWebsiteId + * @param array $expectedTierPrices + * @throws \ReflectionException */ public function testSaveAndReplaceAdvancedPricesAppendBehaviourDataAndCalls( $data, @@ -768,6 +783,9 @@ public function testSaveProductPrices($priceData, $oldSkus, $priceIn, $callNum) $this->invokeMethod($this->advancedPricing, 'saveProductPrices', [$priceData, 'table']); } + /** + * @return array + */ public function saveProductPricesDataProvider() { return [ @@ -839,6 +857,9 @@ public function testDeleteProductTierPrices( ); } + /** + * @return array + */ public function deleteProductTierPricesDataProvider() { return [ @@ -921,6 +942,9 @@ public function testProcessCountExistingPrices( $this->invokeMethod($this->advancedPricing, 'processCountExistingPrices', [$prices, 'table']); } + /** + * @return array + */ public function processCountExistingPricesDataProvider() { return [ @@ -947,6 +971,7 @@ public function processCountExistingPricesDataProvider() * @param $object * @param $property * @return mixed + * @throws \ReflectionException */ protected function getPropertyValue($object, $property) { @@ -963,6 +988,8 @@ protected function getPropertyValue($object, $property) * @param $object * @param $property * @param $value + * @return mixed + * @throws \ReflectionException */ protected function setPropertyValue(&$object, $property, $value) { @@ -980,8 +1007,8 @@ protected function setPropertyValue(&$object, $property, $value) * @param object $object * @param string $method * @param array $args - * - * @return mixed the method result. + * @return mixed + * @throws \ReflectionException */ private function invokeMethod($object, $method, $args = []) { @@ -998,6 +1025,7 @@ private function invokeMethod($object, $method, $args = []) * @param array $methods * * @return \PHPUnit_Framework_MockObject_MockObject + * @throws \ReflectionException */ private function getAdvancedPricingMock($methods = []) { 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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/AdminNotification/LICENSE.txt b/app/code/Magento/AdvancedSearch/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/AdminNotification/LICENSE.txt rename to app/code/Magento/AdvancedSearch/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/AdminNotification/LICENSE_AFL.txt b/app/code/Magento/AdvancedSearch/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/AdminNotification/LICENSE_AFL.txt rename to app/code/Magento/AdvancedSearch/LICENSE_AFL.txt 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..c0c224766eb3c --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php @@ -0,0 +1,162 @@ +scopeConfig = $scopeConfig; + $this->searchLayer = $layerResolver->get(); + $this->recommendationsFactory = $recommendationsFactory; + $this->queryResultFactory = $queryResultFactory; + } + + /** + * Is Results Count Enabled + * + * @return bool + */ + public function isResultsCountEnabled() + { + return $this->scopeConfig->isSetFlag( + 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; + } + + /** + * Return Search 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; + } + + /** + * Is Search Recommendations Enabled + * + * @return bool + */ + private function isSearchRecommendationsEnabled() + { + return $this->scopeConfig->isSetFlag( + self::CONFIG_IS_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Return Search Recommendations Count + * + * @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..b20872da2f8e7 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php @@ -0,0 +1,220 @@ +storeManager = $storeManager; + $this->metadataPool = $metadataPool; + $this->tableResolver = $tableResolver ?: ObjectManager::getInstance()->get(IndexScopeResolverInterface::class); + $this->dimensionCollectionFactory = $dimensionCollectionFactory + ?: ObjectManager::getInstance()->get(DimensionCollectionFactory::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(); + $catalogProductIndexPriceSelect = []; + + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + if (!isset($dimensions[WebsiteDimensionProvider::DIMENSION_NAME]) || + $this->websiteId === null || + $dimensions[WebsiteDimensionProvider::DIMENSION_NAME]->getValue() === $this->websiteId) { + $select = $connection->select()->from( + $this->tableResolver->resolve('catalog_product_index_price', $dimensions), + ['entity_id', 'customer_group_id', 'website_id', 'min_price'] + ); + if ($productIds) { + $select->where('entity_id IN (?)', $productIds); + } + $catalogProductIndexPriceSelect[] = $select; + } + } + + $catalogProductIndexPriceUnionSelect = $connection->select()->union($catalogProductIndexPriceSelect); + + $result = []; + foreach ($connection->fetchAll($catalogProductIndexPriceUnionSelect) 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) + { + $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + + $this->websiteId = $websiteId; + $priceProductsIndexData = $this->_getCatalogProductPriceData($productIds); + $this->websiteId = null; + + 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..1f37e40842f54 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Model/ResourceModel/IndexTest.php @@ -0,0 +1,103 @@ +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); + + $indexScopeResolverMock = $this->createMock( + \Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver::class + ); + $traversableMock = $this->createMock(\Traversable::class); + $dimensionsMock = $this->createMock(\Magento\Framework\Indexer\MultiDimensionProvider::class); + $dimensionsMock->method('getIterator')->willReturn($traversableMock); + $dimensionFactoryMock = $this->createMock( + \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + ); + $dimensionFactoryMock->method('create')->willReturn($dimensionsMock); + + $this->model = new Index( + $this->resourceContextMock, + $this->storeManagerMock, + $this->metadataPoolMock, + 'connectionName', + $indexScopeResolverMock, + $dimensionFactoryMock + ); + } + + public function testGetPriceIndexDataUsesFrontendPriceIndexerTable() + { + $storeId = 1; + $storeMock = $this->createMock(StoreInterface::class); + $storeMock->expects($this->any())->method('getId')->willReturn($storeId); + $storeMock->method('getWebsiteId')->willReturn(1); + $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(); + $selectMock->expects($this->any())->method('union')->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..c6a5af07e60a7 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/composer.json @@ -0,0 +1,31 @@ +{ + "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": [ + "OSL-3.0", + "AFL-3.0" + ], + "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..2dd8c68e2d5fd --- /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..8addf187744fd --- /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..21e19fd58825b --- /dev/null +++ b/app/code/Magento/AdvancedSearch/etc/di.xml @@ -0,0 +1,43 @@ + + + + + + Magento\AdvancedSearch\Model\Recommendations\DataProvider + Related search terms + + + + + Magento\AdvancedSearch\Model\SuggestedQueries + Did you mean + + + + + Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + + + + + + Magento\AdvancedSearch\Model\DataProvider\Suggestions + + + + + + + Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver + + + + + 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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorization/LICENSE.txt b/app/code/Magento/Amqp/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorization/LICENSE.txt rename to app/code/Magento/Amqp/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Analytics/LICENSE_AFL.txt b/app/code/Magento/Amqp/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Analytics/LICENSE_AFL.txt rename to app/code/Magento/Amqp/LICENSE_AFL.txt 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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorizenet/LICENSE.txt b/app/code/Magento/Amqp/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorizenet/LICENSE.txt rename to app/code/Magento/Amqp/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorization/LICENSE_AFL.txt b/app/code/Magento/Amqp/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorization/LICENSE_AFL.txt rename to app/code/Magento/Amqp/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Amqp/Test/Mftf/README.md b/app/code/Magento/Amqp/Test/Mftf/README.md new file mode 100644 index 0000000000000..12d1bbc3a4890 --- /dev/null +++ b/app/code/Magento/Amqp/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Amqp Functional Tests + +The Functional Test Module for **Magento Amqp** module. 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..5b19ba055d059 --- /dev/null +++ b/app/code/Magento/Amqp/Test/Unit/Setup/ConfigOptionsListTest.php @@ -0,0 +1,234 @@ +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)); + } + + /** + * @return array + */ + 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..b50e951b46f81 --- /dev/null +++ b/app/code/Magento/Amqp/composer.json @@ -0,0 +1,26 @@ +{ + "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": [ + "OSL-3.0", + "AFL-3.0" + ], + "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..87666cb880e54 --- /dev/null +++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php @@ -0,0 +1,58 @@ +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..9068654fa944f --- /dev/null +++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php @@ -0,0 +1,71 @@ +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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backend/LICENSE.txt b/app/code/Magento/Analytics/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backend/LICENSE.txt rename to app/code/Magento/Analytics/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorizenet/LICENSE_AFL.txt b/app/code/Magento/Analytics/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Authorizenet/LICENSE_AFL.txt rename to app/code/Magento/Analytics/LICENSE_AFL.txt 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 sprintf('Content-Type: %s', self::CONTENT_MEDIA_TYPE); + } + + /** + * @inheritdoc + */ + public function getContentMediaType(): string + { + return self::CONTENT_MEDIA_TYPE; + } +} 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; + $converterMediaType = $this->converter->getContentMediaType(); + + /** Content-Type header may not only contain media-type declaration */ + if ($response->getBody() && is_int(strripos($response->getHeader('Content-Type'), $converterMediaType))) { + $responseBody = $this->converter->fromBody($response->getBody()); + } else { + $responseBody = []; + } + + 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..c05357400d075 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php @@ -0,0 +1,116 @@ +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. Content-Type: %s', + !empty($response->getBody()) ? $response->getBody() : 'Response body is empty', + $response->getHeader('Content-Type') + ) + ); + } + } + + 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.' + . ' Content-Type: %s', + !empty($response->getBody()) ? $response->getBody() : 'Response body is empty', + $response->getHeader('Content-Type') + ) + ); + } + } + + 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..59878ff9c0814 --- /dev/null +++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php @@ -0,0 +1,114 @@ +analyticsToken = $analyticsToken; + $this->httpClient = $httpClient; + $this->config = $config; + $this->logger = $logger; + $this->flagManager = $flagManager; + $this->responseResolver = $responseResolver; + } + + /** + * Executes update request to MBI api in case store url was changed + * + * @return bool + */ + public function execute() + { + $result = false; + if ($this->analyticsToken->isTokenExist()) { + $response = $this->httpClient->request( + ZendClient::PUT, + $this->config->getValue($this->updateUrlPath), + [ + "url" => $this->flagManager + ->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE), + "new-url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL), + "access-token" => $this->analyticsToken->getToken(), + ] + ); + $result = $this->responseResolver->getResult($response); + if (!$result) { + $this->logger->warning( + sprintf( + 'Update of the subscription for MBI service has been failed: %s. Content-Type: %s', + !empty($response->getBody()) ? $response->getBody() : 'Response body is empty', + $response->getHeader('Content-Type') + ) + ); + } + } + + 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..efddcb501aabb --- /dev/null +++ b/app/code/Magento/Analytics/Model/Cryptographer.php @@ -0,0 +1,140 @@ +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 = array_map( + 'strtolower', + openssl_get_cipher_methods() + ); + $cipherMethod = strtolower($cipherMethod); + + 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..3af168886a447 --- /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 (!empty($attributeData['distinct'])) { + $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..b4b7adebf7459 --- /dev/null +++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php @@ -0,0 +1,304 @@ +resourceConnection = $resourceConnection; + } + + /** + * Get join condition + * + * @return array + */ + public function getJoins() + { + return $this->joins; + } + + /** + * Set joins conditions + * + * @param array $joins + * @return $this + */ + public function setJoins($joins) + { + $this->joins = $joins; + + return $this; + } + + /** + * Get connection name + * + * @return string + */ + public function getConnectionName() + { + return $this->connectionName; + } + + /** + * Set connection name + * + * @param string $connectionName + * @return $this + */ + public function setConnectionName($connectionName) + { + $this->connectionName = $connectionName; + + return $this; + } + + /** + * Get columns + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Set columns + * + * @param array $columns + * @return $this + */ + public function setColumns($columns) + { + $this->columns = $columns; + + return $this; + } + + /** + * Get filters + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Set filters + * + * @param array $filters + * @return $this + */ + public function setFilters($filters) + { + $this->filters = $filters; + + return $this; + } + + /** + * Get from condition + * + * @return array + */ + public function getFrom() + { + return $this->from; + } + + /** + * Set from condition + * + * @param array $from + * @return $this + */ + public function setFrom($from) + { + $this->from = $from; + + return $this; + } + + /** + * 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 $this + */ + public function setGroup($group) + { + $this->group = $group; + + return $this; + } + + /** + * Get parameters + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Set parameters + * + * @param array $params + * @return $this + */ + public function setParams($params) + { + $this->params = $params; + + return $this; + } + + /** + * Get having condition + * + * @return array + */ + public function getHaving() + { + return $this->having; + } + + /** + * Set having condition + * + * @param array $having + * @return $this + */ + public function setHaving($having) + { + $this->having = $having; + + return $this; + } +} 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..8966d018dc6b9 --- /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 $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/Mftf/Data/UserData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml new file mode 100644 index 0000000000000..83f27def4b4e8 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml @@ -0,0 +1,33 @@ + + + + + + noreport + No + Report + noreport@example.com + 123123q + 123123q + en_US + true + 123123q + + + restrictedWebUser + restricted + webUser + restrictedWebUser@example.com + 123123q + 123123q + en_US + true + 123123q + + diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml new file mode 100644 index 0000000000000..099cc71321b84 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml @@ -0,0 +1,171 @@ + + + + + + noreport + 123123q + + Magento_Backend::dashboard + Magento_Sales::sales + Magento_Sales::sales_operation + Magento_Sales::sales_order + Magento_Sales::actions + Magento_Sales::create + Magento_Sales::actions_view + Magento_Sales::email + Magento_Sales::reorder + Magento_Sales::actions_edit + Magento_Sales::cancel + Magento_Sales::review_payment + Magento_Sales::capture + Magento_Sales::invoice + Magento_Sales::creditmemo + Magento_Sales::hold + Magento_Sales::unhold + Magento_Sales::ship + Magento_Sales::comment + Magento_Sales::emails + Magento_Sales::sales_invoice + Magento_Sales::shipment + Magento_Sales::sales_creditmemo + Magento_Paypal::billing_agreement + Magento_Paypal::billing_agreement_actions + Magento_Paypal::billing_agreement_actions_view + Magento_Paypal::actions_manage + Magento_Paypal::use + Magento_Sales::transactions + Magento_Sales::transactions_fetch + Magento_Catalog::catalog + Magento_Catalog::catalog_inventory + Magento_Catalog::products + Magento_Catalog::categories + Magento_Customer::customer + Magento_Customer::manage + Magento_Customer::online + Magento_Cart::cart + Magento_Cart::manage + Magento_Backend::myaccount + Magento_Backend::marketing + Magento_CatalogRule::promo + Magento_CatalogRule::promo_catalog + Magento_SalesRule::quote + Magento_Backend::marketing_communications + Magento_Email::template + Magento_Newsletter::template + Magento_Newsletter::queue + Magento_Newsletter::subscriber + Magento_Backend::marketing_seo + Magento_Search::search + Magento_Search::synonyms + Magento_UrlRewrite::urlrewrite + Magento_Sitemap::sitemap + Magento_Backend::marketing_user_content + Magento_Review::reviews_all + Magento_Review::pending + Magento_Backend::content + Magento_Backend::content_elements + Magento_Cms::page + Magento_Cms::save + Magento_Cms::page_delete + Magento_Cms::block + Magento_Widget::widget_instance + Magento_Cms::media_gallery + Magento_Backend::design + Magento_Theme::theme + Magento_Backend::schedule + Magento_Backend::content_translation + Magento_Backend::stores + Magento_Backend::stores_settings + Magento_Backend::store + Magento_Config::config + Magento_Payment::payment + Magento_Cms::config_cms + Magento_GoogleAnalytics::google + Magento_Downloadable::downloadable + Magento_Contact::contact + Magento_CatalogInventory::cataloginventory + Magento_Payment::payment_services + Magento_Newsletter::newsletter + Magento_Catalog::config_catalog + Magento_CatalogSearch::config_catalog_search + Magento_Shipping::config_shipping + Magento_Shipping::shipping_policy + Magento_Shipping::carriers + Magento_Multishipping::config_multishipping + Magento_Config::config_general + Magento_Config::web + Magento_Config::config_design + Magento_Paypal::paypal + Magento_Customer::config_customer + Magento_Tax::config_tax + Magento_Checkout::checkout + Magento_Persistent::persistent + Magento_Sales::config_sales + Magento_Sales::sales_email + Magento_Sales::sales_pdf + Magento_Reports::reports + Magento_Sitemap::config_sitemap + Magento_Wishlist::config_wishlist + Magento_Config::config_system + Magento_SalesRule::config_promo + Magento_Config::advanced + Magento_Config::config_admin + Magento_Config::trans_email + Magento_Config::dev + Magento_Config::currency + Magento_Rss::rss + Magento_Config::sendfriend + Magento_NewRelicReporting::config_newrelicreporting + Magento_CheckoutAgreements::checkoutagreement + Magento_Sales::order_statuses + Magento_Tax::manage_tax + Magento_CurrencySymbol::system_currency + Magento_CurrencySymbol::currency_rates + Magento_CurrencySymbol::symbols + Magento_Backend::stores_attributes + Magento_Catalog::attributes_attributes + Magento_Catalog::update_attributes + Magento_Catalog::sets + Magento_Review::ratings + Magento_Swatches::iframe + Magento_Backend::stores_other_settings + Magento_Customer::group + Magento_Backend::system + Magento_Backend::convert + Magento_ImportExport::import + Magento_ImportExport::export + Magento_TaxImportExport::import_export + Magento_ImportExport::history + Magento_Backend::extensions + Magento_Backend::local + Magento_Backend::custom + Magento_Backend::tools + Magento_Backend::cache + Magento_Backend::setup_wizard + Magento_Backup::backup + Magento_Backup::rollback + Magento_Indexer::index + Magento_Indexer::changeMode + Magento_User::acl + Magento_User::acl_users + Magento_User::locks + Magento_User::acl_roles + Magento_Backend::system_other_settings + Magento_AdminNotification::adminnotification + Magento_AdminNotification::show_toolbar + Magento_AdminNotification::show_list + Magento_AdminNotification::mark_as_read + Magento_AdminNotification::adminnotification_remove + Magento_Variable::variable + Magento_EncryptionKey::crypt_key + Magento_Backend::global_search + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backup/LICENSE.txt b/app/code/Magento/Analytics/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backup/LICENSE.txt rename to app/code/Magento/Analytics/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backend/LICENSE_AFL.txt b/app/code/Magento/Analytics/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backend/LICENSE_AFL.txt rename to app/code/Magento/Analytics/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml new file mode 100644 index 0000000000000..c4ced12e67e07 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml @@ -0,0 +1,12 @@ + + + + +
+ + diff --git a/app/code/Magento/Analytics/Test/Mftf/README.md b/app/code/Magento/Analytics/Test/Mftf/README.md new file mode 100644 index 0000000000000..cdeb48941e6a4 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Analytics Functional Tests + +The Functional Test Module for **Magento Analytics** module. diff --git a/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml b/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml new file mode 100644 index 0000000000000..2e5f2b762a7b1 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml @@ -0,0 +1,21 @@ + + + +
+ + + + + + + + + + +
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml new file mode 100644 index 0000000000000..914cb59b64e4e --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml @@ -0,0 +1,31 @@ + + + + + + + + + <description value="An admin user cannot save a blank industry setting on the Advanced Reporting configuration page."/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-63981"/> + <group value="analytics"/> + </annotations> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="--Please Select--" stepKey="selectAdvancedReportingIndustry"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingBlankIndustryError}}" userInput="Please select an industry." stepKey="seeBlankIndustryErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml new file mode 100644 index 0000000000000..1c1a3b27b06af --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigurationEnableDisableAnalyticsTest"> + <annotations> + <features value="Analytics"/> + <stories value="Enable/disable Advanced Reporting"/> + <title value="Enable Disable Advanced Reporting"/> + <description value="An admin user can enable/disable Advanced Reporting."/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-66465"/> + <group value="analytics"/> + </annotations> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service" stepKey="seeAdvancedReportingServiceLabelEnabled"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="seeAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceStatus}}" userInput="Subscription status: Pending" stepKey="seeAdvancedReportingServiceStatusEnabled"/> + <!--Disable Advanced Reporting--> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service" stepKey="seeAdvancedReportingServiceLabelDisabled"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Disable" stepKey="selectAdvancedReportingServiceDisabled"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton2"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess2"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Disable" stepKey="seeAdvancedReportingServiceDisabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceStatus}}" userInput="Subscription status: Disabled" stepKey="seeAdvancedReportingServiceStatusDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml new file mode 100644 index 0000000000000..bb682c4468012 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigurationIndustryTest"> + <annotations> + <features value="Analytics"/> + <stories value="Set Magento Advanced reporting industry"/> + <title value="Set Magento Advanced reporting industry"/> + <description value="An admin user can change the industry setting on the Advanced Reporting configuration page."/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-63898"/> + <group value="analytics"/> + </annotations> + + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml new file mode 100644 index 0000000000000..58e809ec45c4a --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigurationPermissionTest"> + <annotations> + <features value="Analytics"/> + <stories value="Advanced Reporting configuration permission"/> + <title value="Advanced Reporting configuration permission"/> + <description value="An admin user without Analytics permissions should not be able to see the Advanced Reporting configuration page."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-82648"/> + <group value="analytics"/> + </annotations> + <before> + <createData entity="adminNoReportRole" stepKey="noReportUserRole"/> + <createData entity="adminNoReport" stepKey="noReportUser"/> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="amOnAdminUsersPage"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="$$noReportUser.username$$" stepKey="fillUsernameSearch"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad time="10" stepKey="wait1"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="$$noReportUser.username$$" stepKey="seeFoundUsername"/> + <click selector="{{AdminUserGridSection.searchResultFirstRow}}" stepKey="clickFoundUsername"/> + <waitForPageLoad time="30" stepKey="wait2"/> + <seeInField selector="{{AdminEditUserSection.usernameTextField}}" userInput="$$noReportUser.username$$" stepKey="seeUsernameInField"/> + <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillCurrentPassword"/> + <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRoleTab"/> + + <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="$$noReportUserRole.rolename$$" stepKey="fillRoleNameSearch"/> + <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearchButtonUserRole"/> + <waitForPageLoad time="10" stepKey="wait3"/> + <see selector="{{AdminEditUserSection.roleNameInFirstRow}}" userInput="$$noReportUserRole.rolename$$" stepKey="seeFoundRoleName"/> + <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="clickFoundRoleName"/> + <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad time="10" stepKey="wait4"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the user." stepKey="seeSuccess"/> + + <amOnPage url="{{AdminConfigPage.url}}" stepKey="amOnAdminConfig"/> + <conditionalClick selector="{{AdminConfigSection.generalTab}}" dependentSelector="{{AdminConfigSection.generalTabOpened}}" visible="false" stepKey="openGeneralTabIfClosed"/> + <scrollTo selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="scrollToMenuItem"/> + <!--<see stepKey="seeAdvancedReportingConfigMenuItem" selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/>--> + <seeElementInDOM selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="seeAdvancedReportingConfigMenuItem"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin2"/> + + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="$$noReportUser.username$$" stepKey="fillUsernameNoReport"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="$$noReportUser.password$$" stepKey="fillPasswordNoReport"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn2"/> + <waitForPageLoad time="10" stepKey="wait5"/> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="amOnAdminConfig2"/> + <conditionalClick selector="{{AdminConfigSection.generalTab}}" dependentSelector="{{AdminConfigSection.generalTabOpened}}" visible="false" stepKey="openGeneralTabIfClosed2"/> + <dontSeeElementInDOM selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="dontSeeAdvancedReportingConfigMenuItem"/> + </test> +</tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml new file mode 100644 index 0000000000000..58e62500b8203 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigurationTimeToSendDataTest"> + <annotations> + <features value="Analytics"/> + <stories value="Time of the day to collect data"/> + <title value="Time of the day to collect data"/> + <description value="An admin user can change the time of the day to collect data setting on the Advanced Reporting configuration page."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-66464"/> + <group value="analytics"/> + </annotations> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingHour}}" userInput="11" stepKey="selectAdvancedReportingHour"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingMinute}}" userInput="11" stepKey="selectAdvancedReportingMinute"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingSeconds}}" userInput="00" stepKey="selectAdvancedReportingSeconds"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> + </test> +</tests> 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..407e323aeaae6 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Block\Adminhtml\System\Config; + +use Magento\Analytics\Block\Adminhtml\System\Config\AdditionalComment; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\Data\Form; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class AdditionalCommentTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AdditionalComment + */ + private $additionalComment; + + /** + * @var AbstractElement|\PHPUnit_Framework_MockObject_MockObject + */ + private $abstractElementMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var Form|\PHPUnit_Framework_MockObject_MockObject + */ + private $formMock; + + protected function setUp() + { + $this->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..d567d65882350 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Block\Adminhtml\System\Config; + +use Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\Data\Form; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class CollectionTimeLabelTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CollectionTimeLabel + */ + private $collectionTimeLabel; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $timeZoneMock; + + /** + * @var AbstractElement|\PHPUnit_Framework_MockObject_MockObject + */ + private $abstractElementMock; + + /** + * @var ResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $localeResolver; + + protected function setUp() + { + $this->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..78ff581f3de9d --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Block\Adminhtml\System\Config; + +use Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel; +use Magento\Analytics\Model\SubscriptionStatusProvider; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\Data\Form; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class SubscriptionStatusLabelTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SubscriptionStatusLabel + */ + private $subscriptionStatusLabel; + + /** + * @var AbstractElement|\PHPUnit_Framework_MockObject_MockObject + */ + private $abstractElementMock; + + /** + * @var SubscriptionStatusProvider|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriptionStatusProviderMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var Form|\PHPUnit_Framework_MockObject_MockObject + */ + private $formMock; + + protected function setUp() + { + $this->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..6a0cecc781062 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Block\Adminhtml\System\Config; + +use Magento\Analytics\Block\Adminhtml\System\Config\Vertical; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\Data\Form; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class VerticalTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Vertical + */ + private $vertical; + + /** + * @var AbstractElement|\PHPUnit_Framework_MockObject_MockObject + */ + private $abstractElementMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var Form|\PHPUnit_Framework_MockObject_MockObject + */ + private $formMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Controller\Adminhtml\BIEssentials; + +use Magento\Analytics\Controller\Adminhtml\BIEssentials\SignUp; +use Magento\Backend\Model\View\Result\RedirectFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class SignUpTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var SignUp + */ + private $signUpController; + + /** + * @var RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @var Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $redirectMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Controller\Adminhtml\Reports; + +use Magento\Analytics\Controller\Adminhtml\Reports\Show; +use Magento\Analytics\Model\Exception\State\SubscriptionUpdateException; +use Magento\Analytics\Model\ReportUrlProvider; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShowTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ReportUrlProvider|\PHPUnit_Framework_MockObject_MockObject + */ + private $reportUrlProviderMock; + + /** + * @var Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $redirectMock; + + /** + * @var ResultFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $messageManagerMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Show + */ + private $showController; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Controller\Adminhtml\Subscription; + +use Magento\Analytics\Controller\Adminhtml\Subscription\Retry; +use Magento\Analytics\Model\Config\Backend\Enabled\SubscriptionHandler; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\Phrase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class RetryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ResultFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + /** + * @var Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectMock; + + /** + * @var SubscriptionHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriptionHandlerMock; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $messageManagerMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Retry + */ + private $retryController; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Cron; + +use Magento\Analytics\Cron\CollectData; +use Magento\Analytics\Model\ExportDataHandlerInterface; +use Magento\Analytics\Model\SubscriptionStatusProvider; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class CollectDataTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ExportDataHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $exportDataHandlerMock; + + /** + * @var SubscriptionStatusProvider|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriptionStatusMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var CollectData + */ + private $collectData; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Cron; + +use Magento\Analytics\Cron\SignUp; +use Magento\Analytics\Model\Config\Backend\Enabled\SubscriptionHandler; +use Magento\Analytics\Model\Connector; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\FlagManager; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SignUpTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Connector|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectorMock; + + /** + * @var WriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configWriterMock; + + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $reinitableConfigMock; + + /** + * @var SignUp + */ + private $signUp; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Cron; + +use Magento\Analytics\Cron\Update; +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\Baseurl\SubscriptionUpdateHandler; +use Magento\Analytics\Model\Connector; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\FlagManager; + +class UpdateTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Connector|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectorMock; + + /** + * @var WriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configWriterMock; + + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $reinitableConfigMock; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var Update + */ + private $update; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class AnalyticsTokenTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $reinitableConfigMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var WriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configWriterMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var AnalyticsToken + */ + private $tokenModel; + + /** + * @var string + */ + private $tokenPath = 'analytics/general/token'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Model\Config\Backend\Baseurl; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\Baseurl\SubscriptionUpdateHandler; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\FlagManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class SubscriptionUpdateHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $reinitableConfigMock; + + /** + * @var WriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configWriterMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var SubscriptionUpdateHandler + */ + private $subscriptionUpdateHandler; + + /** + * @var int + */ + private $attemptsInitValue = 48; + + /** + * @var string + */ + private $cronExpression = '0 * * * *'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Config\Backend; + +use Magento\Analytics\Model\Config\Backend\CollectionTime; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Psr\Log\LoggerInterface; + +class CollectionTimeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var WriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configWriterMock; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var CollectionTime + */ + private $collectionTime; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Model\Config\Backend\Enabled; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\CollectionTime; +use Magento\Analytics\Model\Config\Backend\Enabled\SubscriptionHandler; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\FlagManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class SubscriptionHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var WriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configWriterMock; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $tokenMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var int + */ + private $attemptsInitValue = 10; + + /** + * @var SubscriptionHandler + */ + private $subscriptionHandler; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Model\Config\Backend; + +use Magento\Analytics\Model\Config\Backend\Enabled; +use Magento\Analytics\Model\Config\Backend\Enabled\SubscriptionHandler; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Psr\Log\LoggerInterface; + +class EnabledTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SubscriptionHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriptionHandlerMock; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var Value|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Enabled + */ + private $enabledModel; + + /** + * @var int + */ + private $valueEnabled = 1; + + /** + * @var int + */ + private $valueDisabled = 0; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Config\Backend; + +/** + * A unit test for testing of the backend model for verticals configuration. + */ +class VerticalTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\Model\Config\Backend\Vertical + */ + private $subject; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Config; + +use Magento\Analytics\Model\Config\Mapper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MapperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Mapper + */ + private $mapper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Config; + +use Magento\Analytics\Model\Config\Mapper; +use Magento\Analytics\Model\Config\Reader; +use Magento\Framework\Config\ReaderInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ReaderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Mapper|\PHPUnit_Framework_MockObject_MockObject + */ + private $mapperMock; + + /** + * @var ReaderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $readerXmlMock; + + /** + * @var ReaderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $readerDbMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Reader + */ + private $reader; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Config\Source; + +/** + * A unit test for testing of the source model for verticals configuration. + */ +class VerticalTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\Model\Config\Source\Vertical + */ + private $subject; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\Config; +use Magento\Framework\Config\DataInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DataInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataInterfaceMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Config + */ + private $config; + + /** + * @return void + */ + protected function setUp() + { + $this->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..92f79c2bf6dee --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector\Http\Client; + +use Magento\Analytics\Model\Connector\Http\ConverterInterface; +use Magento\Analytics\Model\Connector\Http\JsonConverter; +use Magento\Framework\HTTP\Adapter\CurlFactory; +use Magento\Framework\HTTP\ResponseFactory; + +/** + * A unit test for testing of the CURL HTTP client. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CurlTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\Model\Connector\Http\Client\Curl + */ + private $curl; + + /** + * @var \Magento\Framework\HTTP\Adapter\Curl|\PHPUnit_Framework_MockObject_MockObject + */ + private $curlAdapterMock; + + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var ResponseFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseFactoryMock; + + /** + * @var ConverterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $converterMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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', + 'method' => \Magento\Framework\HTTP\ZendClient::POST, + ] + ] + ]; + } + + /** + * @param array $data + * @return void + * @throws \Zend_Http_Exception + * @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'], + [$this->converterMock->getContentTypeHeader()], + 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'], + [$this->converterMock->getContentTypeHeader()], + $data['version'] + ) + ); + } + + /** + * @param array $data + * @return void + * @throws \Zend_Http_Exception + * @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'], + [$this->converterMock->getContentTypeHeader()], + 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'], + [$this->converterMock->getContentTypeHeader()], + $data['version'] + ) + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function createJsonConverter() + { + $converterMock = $this->getMockBuilder(JsonConverter::class) + ->setMethodsExcept(['getContentTypeHeader']) + ->disableOriginalConstructor() + ->getMock(); + $converterMock->expects($this->any())->method('toBody')->willReturnCallback(function ($value) { + return json_encode($value); + }); + 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..d3258c8ae9caa --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector\Http; + +use Magento\Analytics\Model\Connector\Http\JsonConverter; +use Magento\Framework\Serialize\Serializer\Json; + +class JsonConverterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var Json|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializerMock; + + /** + * @var JsonConverter + */ + private $converter; + + /** + * @return void + */ + protected function setUp() + { + $this->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] + ); + } + + /** + * @return void + */ + public function testConverterContainsHeader() + { + $this->assertEquals( + 'Content-Type: ' . JsonConverter::CONTENT_MEDIA_TYPE, + $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')); + } + + /** + * @return array + */ + public function convertBodyDataProvider() + { + return [ + [null, ['body']], + [['unserializedBody'], ['unserializedBody']] + ]; + } + + /** + * return void + */ + 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..2564240c4fa11 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Analytics\Test\Unit\Model\Connector\Http; + +use Magento\Analytics\Model\Connector\Http\ConverterInterface; +use Magento\Analytics\Model\Connector\Http\ResponseHandlerInterface; +use Magento\Analytics\Model\Connector\Http\ResponseResolver; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ResponseResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var ConverterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $converterMock; + + /** + * @var ResponseHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $successResponseHandlerMock; + + /** + * @var ResponseHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $notFoundResponseHandlerMock; + + /** + * @var ResponseResolver + */ + private $responseResolver; + + /** + * @return void + */ + protected function setUp() + { + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->converterMock = $this->getMockBuilder(ConverterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->successResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class) + ->getMockForAbstractClass(); + $this->notFoundResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class) + ->getMockForAbstractClass(); + $this->responseResolver = $this->objectManagerHelper->getObject( + ResponseResolver::class, + [ + 'converter' => $this->converterMock, + 'responseHandlers' => [ + 201 => $this->successResponseHandlerMock, + 404 => $this->notFoundResponseHandlerMock, + ] + ] + ); + } + + /** + * @return void + * @throws \Zend_Http_Exception + */ + public function testGetResultHandleResponseSuccess() + { + $expectedBody = ['test' => 'testValue']; + $response = new \Zend_Http_Response(201, ['Content-Type' => 'application/json'], json_encode($expectedBody)); + $this->converterMock + ->method('getContentMediaType') + ->willReturn('application/json'); + + $this->successResponseHandlerMock + ->expects($this->once()) + ->method('handleResponse') + ->with($expectedBody) + ->willReturn(true); + $this->notFoundResponseHandlerMock + ->expects($this->never()) + ->method('handleResponse'); + $this->converterMock + ->method('fromBody') + ->willReturn($expectedBody); + $this->assertTrue($this->responseResolver->getResult($response)); + } + + /** + * @return void + * @throws \Zend_Http_Exception + */ + public function testGetResultHandleResponseUnexpectedContentType() + { + $expectedBody = 'testString'; + $response = new \Zend_Http_Response(201, ['Content-Type' => 'plain/text'], $expectedBody); + $this->converterMock + ->method('getContentMediaType') + ->willReturn('application/json'); + $this->converterMock + ->expects($this->never()) + ->method('fromBody'); + $this->successResponseHandlerMock + ->expects($this->once()) + ->method('handleResponse') + ->with([]) + ->willReturn(false); + $this->notFoundResponseHandlerMock + ->expects($this->never()) + ->method('handleResponse'); + $this->assertFalse($this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Connector\Http\JsonConverter; +use Magento\Analytics\Model\Connector\Http\ResponseResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Psr\Log\LoggerInterface; +use Magento\Analytics\Model\Connector\NotifyDataChangedCommand; +use Magento\Analytics\Model\Connector\Http\ClientInterface; + +class NotifyDataChangedCommandTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var NotifyDataChangedCommand + */ + private $notifyDataChangedCommand; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var ClientInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $httpClientMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public $configMock; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Connector\Http\ClientInterface; +use Magento\Analytics\Model\Connector\Http\ResponseResolver; +use Magento\Analytics\Model\Connector\OTPRequest; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Psr\Log\LoggerInterface; + +/** + * A unit test for testing of the representation of a 'OTP' request. + */ +class OTPRequestTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var OTPRequest + */ + private $subject; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var ClientInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $httpClientMock; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var ResponseResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseResolverMock; + + /** + * @return void + */ + public function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector\ResponseHandler; + +use Magento\Analytics\Model\Connector\ResponseHandler\OTP; + +class OTPTest extends \PHPUnit\Framework\TestCase +{ + public function testHandleResult() + { + $OTPHandler = new OTP(); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector\ResponseHandler; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\Enabled\SubscriptionHandler; +use Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp; +use Magento\Analytics\Model\SubscriptionStatusProvider; + +class ReSignUpTest extends \PHPUnit\Framework\TestCase +{ + public function testHandleResult() + { + $analyticsToken = $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector\ResponseHandler; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Connector\ResponseHandler\SignUp; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class SignUpTest extends \PHPUnit\Framework\TestCase +{ + public function testHandleResult() + { + $accessToken = 'access-token-123'; + $analyticsToken = $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector\ResponseHandler; + +use Magento\Analytics\Model\Connector\ResponseHandler\Update; + +class UpdateTest extends \PHPUnit\Framework\TestCase +{ + public function testHandleResult() + { + $updateHandler = new Update(); + $this->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..c113b2dc275dd --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector; + +use Magento\Analytics\Model\Connector\Http\ClientInterface; +use Magento\Analytics\Model\Connector\Http\JsonConverter; +use Magento\Analytics\Model\Connector\Http\ResponseResolver; +use Magento\Analytics\Model\Connector\SignUpCommand; +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\IntegrationManager; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Integration\Model\Oauth\Token as IntegrationToken; +use Psr\Log\LoggerInterface; + +class SignUpCommandTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SignUpCommand + */ + private $signUpCommand; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var IntegrationManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $integrationManagerMock; + + /** + * @var IntegrationToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $integrationToken; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var ClientInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $httpClientMock; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var ResponseResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseResolverMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 + ); + } + + /** + * @throws \Zend_Http_Exception + * @return void + */ + 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()); + } + + /** + * @return void + */ + public function testExecuteFailureCannotGenerateToken() + { + $this->integrationManagerMock->expects($this->once()) + ->method('generateToken') + ->willReturn(false); + $this->integrationManagerMock->expects($this->never()) + ->method('activateIntegration'); + $this->assertFalse($this->signUpCommand->execute()); + } + + /** + * @throws \Zend_Http_Exception + * @return void + */ + 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', + '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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Connector; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\Baseurl\SubscriptionUpdateHandler; +use Magento\Analytics\Model\Connector\Http\ResponseResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\FlagManager; +use Magento\Framework\HTTP\ZendClient; +use Psr\Log\LoggerInterface; +use Magento\Analytics\Model\Connector\UpdateCommand; +use Magento\Analytics\Model\Connector\Http\ClientInterface; + +class UpdateCommandTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var UpdateCommand + */ + private $updateCommand; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var ClientInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $httpClientMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public $configMock; + + /** + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var ResponseResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseResolverMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\Connector; +use Magento\Framework\ObjectManagerInterface; +use Magento\Analytics\Model\Connector\SignUpCommand; + +class ConnectorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var Connector + */ + private $connector; + + /** + * @var SignUpCommand|\PHPUnit_Framework_MockObject_MockObject + */ + private $signUpCommandMock; + + /** + * @var array + */ + private $commands; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Cryptographer; +use Magento\Analytics\Model\EncodedContext; +use Magento\Analytics\Model\EncodedContextFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class CryptographerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var EncodedContextFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $encodedContextFactoryMock; + + /** + * @var EncodedContext|\PHPUnit_Framework_MockObject_MockObject + */ + private $encodedContextMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Cryptographer + */ + private $cryptographer; + + /** + * @var string + */ + private $key; + + /** + * @var array + */ + private $initializationVectors; + + /** + * @var + */ + private $source; + + /** + * @var string + */ + private $cipherMethod = 'AES-256-CBC'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\EncodedContext; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class EncodedContextTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\Connector; +use Magento\Analytics\Model\ExportDataHandler; +use Magento\Analytics\Model\ExportDataHandlerNotification; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ExportDataHandlerNotificationTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @return void + */ + public function setUp() + { + $this->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..cf00556cfe590 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php @@ -0,0 +1,267 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\Cryptographer; +use Magento\Analytics\Model\EncodedContext; +use Magento\Analytics\Model\ExportDataHandler; +use Magento\Analytics\Model\FileRecorder; +use Magento\Analytics\Model\ReportWriterInterface; +use Magento\Framework\Archive; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ExportDataHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + private $filesystemMock; + + /** + * @var Archive|\PHPUnit_Framework_MockObject_MockObject + */ + private $archiveMock; + + /** + * @var ReportWriterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $reportWriterMock; + + /** + * @var Cryptographer|\PHPUnit_Framework_MockObject_MockObject + */ + private $cryptographerMock; + + /** + * @var FileRecorder|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileRecorderMock; + + /** + * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $directoryMock; + + /** + * @var EncodedContext|\PHPUnit_Framework_MockObject_MockObject + */ + private $encodedContextMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var ExportDataHandler + */ + private $exportDataHandler; + + /** + * @var string + */ + private $subdirectoryPath = 'analytics/'; + + /** + * @var string + */ + private $archiveName = 'data.tgz'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 + ); + + $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 isn\'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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\FileInfo; +use Magento\Analytics\Model\FileInfoFactory; +use Magento\Analytics\Model\FileInfoManager; +use Magento\Framework\FlagManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class FileInfoManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var FileInfoFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoFactoryMock; + + /** + * @var FileInfo|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var FileInfoManager + */ + private $fileInfoManager; + + /** + * @var string + */ + private $flagCode = 'analytics_file_info'; + + /** + * @var array + */ + private $encodedParameters = [ + 'initializationVector' + ]; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\FileInfo; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class FileInfoTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\EncodedContext; +use Magento\Analytics\Model\FileInfo; +use Magento\Analytics\Model\FileInfoFactory; +use Magento\Analytics\Model\FileInfoManager; +use Magento\Analytics\Model\FileRecorder; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class FileRecorderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FileInfoManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoManagerMock; + + /** + * @var FileInfoFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoFactoryMock; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + private $filesystemMock; + + /** + * @var FileInfo|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoMock; + + /** + * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $directoryMock; + + /** + * @var EncodedContext|\PHPUnit_Framework_MockObject_MockObject + */ + private $encodedContextMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var FileRecorder + */ + private $fileRecorder; + + /** + * @var string + */ + private $fileSubdirectoryPath = 'analytics_subdir/'; + + /** + * @var string + */ + private $encodedFileName = 'filename.tgz'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Integration\Api\IntegrationServiceInterface; +use Magento\Config\Model\Config; +use Magento\Integration\Model\Integration; +use Magento\Analytics\Model\IntegrationManager; +use Magento\Integration\Api\OauthServiceInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class IntegrationManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var IntegrationServiceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $integrationServiceMock; + + /** + * @var OauthServiceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $oauthServiceMock; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var Integration|\PHPUnit_Framework_MockObject_MockObject + */ + private $integrationMock; + + /** + * @var IntegrationManager + */ + private $integrationManager; + + public function setUp() + { + $objectManagerHelper = new ObjectManagerHelper($this); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Api\Data\LinkInterface; +use Magento\Analytics\Api\Data\LinkInterfaceFactory; +use Magento\Analytics\Model\FileInfo; +use Magento\Analytics\Model\FileInfoManager; +use Magento\Analytics\Model\LinkProvider; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +class LinkProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var LinkInterfaceFactory | \PHPUnit_Framework_MockObject_MockObject + */ + private $linkInterfaceFactoryMock; + + /** + * @var FileInfoManager | \PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoManagerMock; + + /** + * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerInterfaceMock; + + /** + * @var LinkInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $linkInterfaceMock; + + /** + * @var FileInfo | \PHPUnit_Framework_MockObject_MockObject + */ + private $fileInfoMock; + + /** + * @var Store | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var LinkProvider + */ + private $linkProvider; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\Plugin; + +use Magento\Analytics\Model\Config\Backend\Baseurl\SubscriptionUpdateHandler; +use Magento\Analytics\Model\Plugin\BaseUrlConfigPlugin; +use Magento\Analytics\Model\SubscriptionStatusProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; + +class BaseUrlConfigPluginTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SubscriptionUpdateHandler | \PHPUnit_Framework_MockObject_MockObject + */ + private $subscriptionUpdateHandlerMock; + + /** + * @var Value | \PHPUnit_Framework_MockObject_MockObject + */ + private $configValueMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var BaseUrlConfigPlugin + */ + private $plugin; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\Baseurl\SubscriptionUpdateHandler; +use Magento\Analytics\Model\Connector\OTPRequest; +use Magento\Analytics\Model\Exception\State\SubscriptionUpdateException; +use Magento\Analytics\Model\ReportUrlProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\FlagManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ReportUrlProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var OTPRequest|\PHPUnit_Framework_MockObject_MockObject + */ + private $otpRequestMock; + + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var ReportUrlProvider + */ + private $reportUrlProvider; + + /** + * @var string + */ + private $urlReportConfigPath = 'path/url/report'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\ConfigInterface; +use Magento\Analytics\Model\ProviderFactory; +use Magento\Analytics\Model\ReportWriter; +use Magento\Analytics\ReportXml\DB\ReportValidator; +use Magento\Analytics\ReportXml\ReportProvider; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ReportWriterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configInterfaceMock; + + /** + * @var ReportValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $reportValidatorMock; + + /** + * @var ProviderFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $providerFactoryMock; + + /** + * @var ReportProvider|\PHPUnit_Framework_MockObject_MockObject + */ + private $reportProviderMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $directoryMock; + + /** + * @var ReportWriter + */ + private $reportWriter; + + /** + * @var string + */ + private $reportName = 'testReport'; + + /** + * @var string + */ + private $providerName = 'testProvider'; + + /** + * @var string + */ + private $providerClass = 'Magento\Analytics\Provider'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Model\ReportXml; + +use Magento\Analytics\Model\ReportXml\ModuleIterator; +use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ModuleIteratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ModuleManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $moduleManagerMock; + + /** + * @var ModuleIterator|\PHPUnit_Framework_MockObject_MockObject + */ + private $moduleIterator; + + public function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\StoreConfigurationProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; + +class StoreConfigurationProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var string[] + */ + private $configPaths; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var WebsiteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $websiteMock; + + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var StoreConfigurationProvider|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeConfigurationProvider; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model; + +use Magento\Analytics\Model\AnalyticsToken; +use Magento\Analytics\Model\Config\Backend\Baseurl\SubscriptionUpdateHandler; +use Magento\Analytics\Model\Config\Backend\Enabled\SubscriptionHandler; +use Magento\Analytics\Model\SubscriptionStatusProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\FlagManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class SubscriptionStatusProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var AnalyticsToken|\PHPUnit_Framework_MockObject_MockObject + */ + private $analyticsTokenMock; + + /** + * @var FlagManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $flagManagerMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var SubscriptionStatusProvider + */ + private $statusProvider; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\Model\System\Message; + +use Magento\Analytics\Model\SubscriptionStatusProvider; +use Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\UrlInterface; + +class NotificationAboutFailedSubscriptionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|SubscriptionStatusProvider + */ + private $subscriptionStatusMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|UrlInterface + */ + private $urlBuilderMock; + + /** + * @var NotificationAboutFailedSubscription + */ + private $notification; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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('<a href="%s">Retry Synchronization</a>', $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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\Config\Converter; + +/** + * A unit test for testing of the reports configuration converter (XML to PHP array). + */ +class XmlTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\ReportXml\Config\Converter\Xml + */ + private $subject; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\Config; + +use Magento\Analytics\ReportXml\Config\Mapper; + +class MapperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Mapper + */ + private $mapper; + + protected function setUp() + { + $this->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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi: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">10</condition> + </filter> + </source> + </report> + <report name="test_report_2" connection="default"> + <source name="customer_entity" alias="customers"> + <attribute name="email" /> + <filter glue="and"> + <condition attribute="dob" operator="null" /> + </filter> + </source> + </report> +</config> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +use Magento\Analytics\ReportXml\Config; +use Magento\Framework\Config\DataInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DataInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Config + */ + private $config; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +use Magento\Analytics\ReportXml\ConnectionFactory; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\Pdo\Mysql as MysqlPdoAdapter; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ConnectionFactoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var ConnectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionNewMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var ConnectionFactory + */ + private $connectionFactory; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB\Assembler; + +/** + * A unit test for testing of the 'filter' assembler. + */ +class FilterAssemblerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler + */ + private $subject; + + /** + * @var \Magento\Analytics\ReportXml\DB\NameResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $nameResolverMock; + + /** + * @var \Magento\Analytics\ReportXml\DB\SelectBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectBuilderMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \Magento\Analytics\ReportXml\DB\ConditionResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $conditionResolverMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB\Assembler; + +use Magento\Framework\App\ResourceConnection; + +/** + * A unit test for testing of the 'from' assembler. + */ +class FromAssemblerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\ReportXml\DB\Assembler\FromAssembler + */ + private $subject; + + /** + * @var \Magento\Analytics\ReportXml\DB\NameResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $nameResolverMock; + + /** + * @var \Magento\Analytics\ReportXml\DB\SelectBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectBuilderMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \Magento\Analytics\ReportXml\DB\ColumnsResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $columnsResolverMock; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnection; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB\Assembler; + +use Magento\Framework\App\ResourceConnection; + +/** + * A unit test for testing of the 'join' assembler. + */ +class JoinAssemblerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler + */ + private $subject; + + /** + * @var \Magento\Analytics\ReportXml\DB\NameResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $nameResolverMock; + + /** + * @var \Magento\Analytics\ReportXml\DB\SelectBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectBuilderMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \Magento\Analytics\ReportXml\DB\ColumnsResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $columnsResolverMock; + + /** + * @var \Magento\Analytics\ReportXml\DB\ConditionResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $conditionResolverMock; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnection; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB; + +use Magento\Analytics\ReportXml\DB\ColumnsResolver; +use Magento\Analytics\ReportXml\DB\NameResolver; +use Magento\Analytics\ReportXml\DB\SelectBuilder; +use Magento\Framework\DB\Sql\ColumnValueExpression; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ColumnsResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SelectBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectBuilderMock; + + /** + * @var ColumnsResolver + */ + private $columnsResolver; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB; + +use Magento\Analytics\ReportXml\DB\ConditionResolver; +use Magento\Analytics\ReportXml\DB\SelectBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Sql\Expression; + +class ConditionResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var ConditionResolver + */ + private $conditionResolver; + + /** + * @var SelectBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectBuilderMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB; + +use Magento\Analytics\ReportXml\DB\NameResolver; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class NameResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var NameResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $nameResolverMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var NameResolver + */ + private $nameResolver; + + /** + * @return void + */ + protected function setUp() + { + $this->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..ac141fae4be66 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB; + +use Magento\Analytics\ReportXml\ConnectionFactory; +use Magento\Analytics\ReportXml\DB\ReportValidator; +use Magento\Analytics\ReportXml\Query; +use Magento\Analytics\ReportXml\QueryFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ReportValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ConnectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionFactoryMock; + + /** + * @var QueryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryFactoryMock; + + /** + * @var Query|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var ReportValidator + */ + private $reportValidator; + + /** + * @return void + */ + protected function setUp() + { + $this->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..a4362d583dfbc --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml\DB; + +use Magento\Analytics\ReportXml\DB\SelectBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; + +class SelectBuilderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SelectBuilder + */ + private $selectBuilder; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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) + ->setFrom($from) + ->setColumns($columns) + ->setFilters([$filter]) + ->setJoins($joins) + ->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +use Magento\Analytics\ReportXml\IteratorFactory; +use Magento\Framework\ObjectManagerInterface; + +class IteratorFactoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var \IteratorIterator|\PHPUnit_Framework_MockObject_MockObject + */ + private $iteratorIteratorMock; + + /** + * @var IteratorFactory + */ + private $iteratorFactory; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +/** + * A unit test for testing of the query factory. + */ +class QueryFactoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\ReportXml\QueryFactory + */ + private $subject; + + /** + * @var \Magento\Analytics\ReportXml\Query|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryMock; + + /** + * @var \Magento\Analytics\ReportXml\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var \Magento\Analytics\ReportXml\DB\Assembler\AssemblerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $assemblerMock; + + /** + * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryCacheMock; + + /** + * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var \Magento\Analytics\ReportXml\SelectHydrator|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectHydratorMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \Magento\Analytics\ReportXml\DB\SelectBuilderFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectBuilderFactoryMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +use Magento\Analytics\ReportXml\Query; +use Magento\Analytics\ReportXml\SelectHydrator as selectHydrator; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class QueryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var selectHydrator|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectHydratorMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Query + */ + private $query; + + /** + * @var string + */ + private $connectionName = 'test_connection'; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +/** + * A unit test for testing of the reports provider. + */ +class ReportProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Analytics\ReportXml\ReportProvider + */ + private $subject; + + /** + * @var \Magento\Analytics\ReportXml\Query|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryMock; + + /** + * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var \IteratorIterator|\PHPUnit_Framework_MockObject_MockObject + */ + private $iteratorMock; + + /** + * @var \Magento\Framework\DB\Statement\Pdo\Mysql|\PHPUnit_Framework_MockObject_MockObject + */ + private $statementMock; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var \Magento\Analytics\ReportXml\QueryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryFactoryMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \Magento\Analytics\ReportXml\ConnectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionFactoryMock; + + /** + * @var \Magento\Analytics\ReportXml\IteratorFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $iteratorFactoryMock; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Analytics\Test\Unit\ReportXml; + +use Magento\Analytics\ReportXml\SelectHydrator; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\JsonSerializableExpression; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class SelectHydratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SelectHydrator + */ + private $selectHydrator; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @return void + */ + protected function setUp() + { + $this->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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Analytics::analytics" title="Analytics" translate="title" sortOrder="10"> + <resource id="Magento_Analytics::analytics_api" title="API" translate="title" sortOrder="10"/> + </resource> + <resource id="Magento_Backend::stores"> + <resource id="Magento_Backend::stores_settings"> + <resource id="Magento_Config::config" title="Configuration" translate="title" sortOrder="20"> + <resource id="Magento_Analytics::analytics_settings" title="Analytics" translate="title" sortOrder="150" /> + </resource> + </resource> + </resource> + <resource id="Magento_Reports::report"> + <resource id="Magento_Analytics::business_intelligence" title="Business Intelligence" translate="title" sortOrder="90"> + <resource id="Magento_Analytics::advanced_reporting" title="Advanced Reporting" translate="title" sortOrder="10" /> + <resource id="Magento_Analytics::bi_essentials" title="BI Essentials" translate="title" sortOrder="20" /> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\Notification\MessageList"> + <arguments> + <argument name="messages" xsi:type="array"> + <item name="analytics" xsi:type="string">Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription</item> + </argument> + </arguments> + </type> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_Analytics::business_intelligence" title="Business Intelligence" translate="title" module="Magento_Analytics" + sortOrder="90" parent="Magento_Reports::report" resource="Magento_Analytics::business_intelligence" /> + <add id="Magento_Analytics::advanced_reporting" title="Advanced Reporting" translate="title" module="Magento_Analytics" + sortOrder="10" parent="Magento_Analytics::business_intelligence" action="analytics/reports/show" + target="_blank" resource="Magento_Analytics::advanced_reporting" /> + <add id="Magento_Analytics::bi_essentials" title="BI Essentials" translate="title" module="Magento_Analytics" + sortOrder="20" parent="Magento_Analytics::business_intelligence" action="analytics/biessentials/signup" + target="_blank" resource="Magento_Analytics::bi_essentials" /> + </menu> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="analytics" frontName="analytics"> + <module name="Magento_Analytics" /> + </route> + </router> +</config> 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..c7da840b7e665 --- /dev/null +++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="analytics" translate="label" type="text" sortOrder="1150" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Advanced Reporting</label> + <tab>general</tab> + <resource>Magento_Analytics::analytics_settings</resource> + <group id="general" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Advanced Reporting</label> + <comment><![CDATA[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. </br> For more information, see our <a href="https://magento.com/legal/terms/cloud-terms"> + terms and conditions</a>.]]></comment> + <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Advanced Reporting Service</label> + <source_model>Magento\Config\Model\Config\Source\Enabledisable</source_model> + <backend_model>Magento\Analytics\Model\Config\Backend\Enabled</backend_model> + <frontend_model>Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel</frontend_model> + <config_path>analytics/subscription/enabled</config_path> + </field> + <field id="collection_time" translate="label" type="time" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Time of day to send data</label> + <frontend_model>Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel</frontend_model> + <backend_model>Magento\Analytics\Model\Config\Backend\CollectionTime</backend_model> + </field> + <field id="vertical" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> + <hint>Industry Data</hint> + <label>Industry</label> + <comment>In order to personalize your Advanced Reporting experience, please select your industry.</comment> + <source_model>Magento\Analytics\Model\Config\Source\Vertical</source_model> + <backend_model>Magento\Analytics\Model\Config\Backend\Vertical</backend_model> + <frontend_model>Magento\Analytics\Block\Adminhtml\System\Config\Vertical</frontend_model> + <depends> + <field id="analytics/general/enabled">1</field> + </depends> + </field> + <field id="additional_comment" translate="label comment" type="label" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> + <label><![CDATA[<strong>Get more insights from Magento Business Intelligence</strong>]]></label> + <comment><![CDATA[Magento Business Intelligence provides you with a simple and clear path to + becoming more data driven.</br> Learn more about <a target="_blank" + href="https://dashboard.rjmetrics.com/v2/magento/signup/">Magento BI Essentials and BI Pro</a> tiers.]]></comment> + <frontend_model>Magento\Analytics\Block\Adminhtml\System\Config\AdditionalComment</frontend_model> + </field> + </group> + </section> + </system> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Analytics:etc/analytics.xsd"> + <file name="modules"> + <providers> + <reportProvider name="modules" class="Magento\Analytics\ReportXml\ReportProvider"> + <parameters> + <name>modules</name> + </parameters> + </reportProvider> + </providers> + </file> + <file name="store_config"> + <providers> + <customProvider name="store_config" class="Magento\Analytics\Model\StoreConfigurationProvider"/> + </providers> + </file> + <file name="stores"> + <providers> + <reportProvider name="stores" class="Magento\Analytics\ReportXml\ReportProvider"> + <parameters> + <name>stores</name> + </parameters> + </reportProvider> + </providers> + </file> + <file name="websites"> + <providers> + <reportProvider name="websites" class="Magento\Analytics\ReportXml\ReportProvider"> + <parameters> + <name>websites</name> + </parameters> + </reportProvider> + </providers> + </file> + <file name="groups"> + <providers> + <reportProvider name="groups" class="Magento\Analytics\ReportXml\ReportProvider"> + <parameters> + <name>groups</name> + </parameters> + </reportProvider> + </providers> + </file> +</config> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="config"> + <xs:complexType> + <xs:sequence> + <xs:element name="file" type="fileDeclaration" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + <xs:unique name="uniqueFileName"> + <xs:selector xpath="file" /> + <xs:field xpath="@name" /> + </xs:unique> + </xs:element> + <xs:complexType name="fileDeclaration"> + <xs:sequence> + <xs:element name="providers" type="providers" minOccurs="1" /> + </xs:sequence> + <xs:attribute name="name" type="fileName" use="required" /> + <xs:attribute name="prefix" type="xs:string" /> + </xs:complexType> + <xs:complexType name="providers"> + <xs:choice> + <xs:sequence> + <xs:element name="reportProvider" type="reportProvider" /> + <xs:element name="customProvider" type="customProvider" minOccurs="0" /> + </xs:sequence> + <xs:element name="customProvider" type="customProvider" /> + </xs:choice> + </xs:complexType> + <xs:complexType name="provider" abstract="true"> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="class" type="xs:string" use="required"/> + </xs:complexType> + <xs:complexType name="reportProvider"> + <xs:complexContent> + <xs:extension base="provider"> + <xs:all> + <xs:element name="parameters"> + <xs:complexType> + <xs:sequence> + <xs:element name="name" type="notEmptyString" maxOccurs="1" /> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:all> + </xs:extension> + </xs:complexContent> + </xs:complexType> + <xs:complexType name="customProvider"> + <xs:complexContent> + <xs:extension base="provider" /> + </xs:complexContent> + </xs:complexType> + <xs:simpleType name="fileName"> + <xs:annotation> + <xs:documentation> + File name attribute can has only [a-zA-Z0-9/_]. + </xs:documentation> + </xs:annotation> + <xs:restriction base="xs:string"> + <xs:pattern value="[a-zA-Z0-9/_]+" /> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="notEmptyString"> + <xs:annotation> + <xs:documentation> + Value is required. + </xs:documentation> + </xs:annotation> + <xs:restriction base="xs:string"> + <xs:minLength value="1" /> + </xs:restriction> + </xs:simpleType> +</xs:schema> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <analytics> + <url> + <signup>https://advancedreporting.rjmetrics.com/signup</signup> + <update>https://advancedreporting.rjmetrics.com/update</update> + <bi_essentials>https://dashboard.rjmetrics.com/v2/magento/signup</bi_essentials> + <otp>https://advancedreporting.rjmetrics.com/otp</otp> + <report>https://advancedreporting.rjmetrics.com/report</report> + <notify_data_changed>https://advancedreporting.rjmetrics.com/report</notify_data_changed> + </url> + <integration_name>Magento Analytics user</integration_name> + <general> + <collection_time>02,00,00</collection_time> + </general> + </analytics> + </default> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> + <group id="default"> + <job name="analytics_subscribe" instance="Magento\Analytics\Cron\SignUp" method="execute" /> + <job name="analytics_update" instance="Magento\Analytics\Cron\Update" method="execute" /> + <job name="analytics_collect_data" instance="Magento\Analytics\Cron\CollectData" method="execute" /> + </group> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\Analytics\ReportXML\ConfigInterface" type="Magento\Analytics\ReportXML\Config" /> + <preference for="Magento\Analytics\Model\ConfigInterface" type="Magento\Analytics\Model\Config" /> + <preference for="Magento\Analytics\Model\ReportWriterInterface" type="Magento\Analytics\Model\ReportWriter" /> + <preference for="Magento\Analytics\Api\LinkProviderInterface" type="Magento\Analytics\Model\LinkProvider" /> + <preference for="Magento\Analytics\Api\Data\LinkInterface" type="Magento\Analytics\Model\Link" /> + <preference for="Magento\Analytics\Model\Connector\Http\ClientInterface" type="Magento\Analytics\Model\Connector\Http\Client\Curl" /> + <preference for="Magento\Analytics\Model\ExportDataHandlerInterface" type="Magento\Analytics\Model\ExportDataHandlerNotification" /> + <preference for="Magento\Analytics\Model\Connector\Http\ConverterInterface" type="Magento\Analytics\Model\Connector\Http\JsonConverter" /> + <type name="Magento\Analytics\Model\Connector"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="signUp" xsi:type="string">Magento\Analytics\Model\Connector\SignUpCommand</item> + <item name="update" xsi:type="string">Magento\Analytics\Model\Connector\UpdateCommand</item> + <item name="notifyDataChanged" xsi:type="string">Magento\Analytics\Model\Connector\NotifyDataChangedCommand</item> + </argument> + </arguments> + </type> + <!--Configuration for \Magento\Analytics\ReportXml\Config--> + <type name="Magento\Analytics\ReportXml\Config"> + <arguments> + <argument name="data" xsi:type="object">Magento\Analytics\ReportXml\Config\Data</argument> + </arguments> + </type> + <virtualType name="Magento\Analytics\ReportXml\Config\Data" type="Magento\Framework\Config\Data"> + <arguments> + <argument name="reader" xsi:type="object">Magento\Analytics\ReportXml\Config\Reader</argument> + <argument name="cacheId" xsi:type="string">Magento_Analytics_ReportXml_CacheId</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Analytics\ReportXml\Config\SchemaLocator" type="Magento\Framework\Config\SchemaLocator"> + <arguments> + <argument name="realPath" xsi:type="string">urn:magento:module:Magento_Analytics:etc/reports.xsd</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Analytics\ReportXml\Config\Reader\Xml" type="Magento\Framework\Config\Reader\Filesystem"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Analytics\ReportXml\Config\Converter\Xml</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Analytics\ReportXml\Config\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">reports.xml</argument> + <argument name="idAttributes" xsi:type="array"> + <item name="/config/report" xsi:type="string">name</item> + <item name="/config/report/source/link-source" xsi:type="array"> + <item name="name" xsi:type="string">name</item> + <item name="alias" xsi:type="string">alias</item> + </item> + <item name="/config/report/source/attribute" xsi:type="string">name</item> + <item name="/config/report/source/link-source/attribute" xsi:type="string">name</item> + <!-- filter conditions for main source--> + <item name="/config/report/source(/filter)+" xsi:type="string">glue</item> + <item name="/config/report/source(/filter)+/condition" xsi:type="array"> + <item name="attribute" xsi:type="string">attribute</item> + <item name="operator" xsi:type="string">operator</item> + </item> + <!-- filter conditions for joined source--> + <item name="/config/report/source/link-source(/filter)+" xsi:type="string">glue</item> + <item name="/config/report/source/link-source(/filter)+/condition" xsi:type="array"> + <item name="attribute" xsi:type="string">attribute</item> + <item name="operator" xsi:type="string">operator</item> + </item> + <!-- join conditions for joined source--> + <item name="/config/report/source/link-source/using" xsi:type="string">glue</item> + <item name="/config/report/source/link-source/using/condition" xsi:type="array"> + <item name="attribute" xsi:type="string">attribute</item> + <item name="operator" xsi:type="string">operator</item> + </item> + <item name="/config/report/source/link-source/using(/filter)+" xsi:type="string">glue</item> + <item name="/config/report/source/link-source/using(/filter)+/condition" xsi:type="array"> + <item name="attribute" xsi:type="string">attribute</item> + <item name="operator" xsi:type="string">operator</item> + </item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Analytics\ReportXml\Config\Reader"> + <arguments> + <argument name="readers" xsi:type="array"> + <item name="xml" xsi:type="object">Magento\Analytics\ReportXml\Config\Reader\Xml</item> + </argument> + </arguments> + </type> + <!--Configuration for \Magento\Analytics\Model\Config--> + <type name="Magento\Analytics\Model\Config"> + <arguments> + <argument name="data" xsi:type="object">Magento\Analytics\Model\Config\Data</argument> + </arguments> + </type> + <virtualType name="Magento\Analytics\Model\Config\Data" type="Magento\Framework\Config\Data"> + <arguments> + <argument name="reader" xsi:type="object">Magento\Analytics\Model\Config\Reader</argument> + <argument name="cacheId" xsi:type="string">Magento_Analytics_CacheId</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Analytics\Model\Config\SchemaLocator" type="Magento\Framework\Config\SchemaLocator"> + <arguments> + <argument name="realPath" xsi:type="string">urn:magento:module:Magento_Analytics:etc/analytics.xsd</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Analytics\Model\Config\Reader\Xml" type="Magento\Framework\Config\Reader\Filesystem"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Analytics\ReportXml\Config\Converter\Xml</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Analytics\Model\Config\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">analytics.xml</argument> + <argument name="idAttributes" xsi:type="array"> + <item name="/config/file" xsi:type="string">name</item> + </argument> + </arguments> + </virtualType> + <!-- --> + <type name="Magento\Analytics\ReportXml\QueryFactory"> + <arguments> + <argument name="assemblers" xsi:type="array"> + <item name="from" xsi:type="object">Magento\Analytics\ReportXml\DB\Assembler\FromAssembler</item> + <item name="filter" xsi:type="object">Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler</item> + <item name="join" xsi:type="object">Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler</item> + </argument> + </arguments> + </type> + <type name="Magento\Analytics\Model\Config\Reader"> + <arguments> + <argument name="readers" xsi:type="array"> + <item name="xml" xsi:type="object">Magento\Analytics\Model\Config\Reader\Xml</item> + </argument> + </arguments> + </type> + <type name="Magento\Analytics\Model\StoreConfigurationProvider"> + <arguments> + <argument name="configPaths" xsi:type="array"> + <item name="0" xsi:type="string">web/unsecure/base_url</item> + <item name="1" xsi:type="string">currency/options/base</item> + <item name="2" xsi:type="string">general/locale/timezone</item> + <item name="3" xsi:type="string">general/country/default</item> + <item name="4" xsi:type="string">carriers/dhl/title</item> + <item name="5" xsi:type="string">carriers/dhl/active</item> + <item name="6" xsi:type="string">carriers/fedex/title</item> + <item name="7" xsi:type="string">carriers/fedex/active</item> + <item name="8" xsi:type="string">carriers/flatrate/title</item> + <item name="9" xsi:type="string">carriers/flatrate/active</item> + <item name="10" xsi:type="string">carriers/tablerate/title</item> + <item name="11" xsi:type="string">carriers/tablerate/active</item> + <item name="12" xsi:type="string">carriers/freeshipping/title</item> + <item name="13" xsi:type="string">carriers/freeshipping/active</item> + <item name="14" xsi:type="string">carriers/ups/title</item> + <item name="15" xsi:type="string">carriers/ups/active</item> + <item name="16" xsi:type="string">carriers/usps/title</item> + <item name="17" xsi:type="string">carriers/usps/active</item> + <item name="18" xsi:type="string">payment/free/title</item> + <item name="19" xsi:type="string">payment/free/active</item> + <item name="20" xsi:type="string">payment/checkmo/title</item> + <item name="21" xsi:type="string">payment/checkmo/active</item> + <item name="22" xsi:type="string">payment/purchaseorder/title</item> + <item name="23" xsi:type="string">payment/purchaseorder/active</item> + <item name="24" xsi:type="string">payment/banktransfer/title</item> + <item name="25" xsi:type="string">payment/banktransfer/active</item> + <item name="26" xsi:type="string">payment/cashondelivery/title</item> + <item name="27" xsi:type="string">payment/cashondelivery/active</item> + <item name="28" xsi:type="string">payment/authorizenet_directpost/title</item> + <item name="29" xsi:type="string">payment/authorizenet_directpost/active</item> + <item name="30" xsi:type="string">payment/paypal_billing_agreement/title</item> + <item name="31" xsi:type="string">payment/paypal_billing_agreement/active</item> + <item name="32" xsi:type="string">payment/braintree/title</item> + <item name="33" xsi:type="string">payment/braintree/active</item> + <item name="34" xsi:type="string">payment/braintree_paypal/title</item> + <item name="35" xsi:type="string">payment/braintree_paypal/active</item> + <item name="36" xsi:type="string">analytics/general/vertical</item> + </argument> + </arguments> + </type> + <type name="Magento\Analytics\Model\Config\Source\Vertical"> + <arguments> + <argument name="verticals" xsi:type="array"> + <item name="0" xsi:type="string" translatable="true">Apps and Games</item> + <item name="1" xsi:type="string" translatable="true">Athletic/Sporting Goods</item> + <item name="2" xsi:type="string" translatable="true">Art and Design</item> + <item name="3" xsi:type="string" translatable="true">Auto Parts</item> + <item name="4" xsi:type="string" translatable="true">Baby/Children’s Apparel, Gear and Toys</item> + <item name="5" xsi:type="string" translatable="true">Beauty and Cosmetics</item> + <item name="6" xsi:type="string" translatable="true">Books, Music and Magazines</item> + <item name="7" xsi:type="string" translatable="true">Crafts and Stationery</item> + <item name="8" xsi:type="string" translatable="true">Consumer Electronics</item> + <item name="9" xsi:type="string" translatable="true">Deal Site</item> + <item name="10" xsi:type="string" translatable="true">Fashion Apparel and Accessories</item> + <item name="11" xsi:type="string" translatable="true">Food, Beverage and Grocery</item> + <item name="12" xsi:type="string" translatable="true">Home Goods and Furniture</item> + <item name="13" xsi:type="string" translatable="true">Home Improvement</item> + <item name="14" xsi:type="string" translatable="true">Jewelry and Watches</item> + <item name="15" xsi:type="string" translatable="true">Mass Merchant</item> + <item name="16" xsi:type="string" translatable="true">Office Supplies</item> + <item name="17" xsi:type="string" translatable="true">Outdoor and Camping Gear</item> + <item name="18" xsi:type="string" translatable="true">Pet Goods</item> + <item name="19" xsi:type="string" translatable="true">Pharma and Medical Devices</item> + <item name="20" xsi:type="string" translatable="true">Technology B2B</item> + <item name="21" xsi:type="string" translatable="true">Other</item> + </argument> + </arguments> + </type> + <type name="Magento\Config\Model\Config\Backend\Baseurl"> + <plugin name="updateAnalyticsSubscription" type="Magento\Analytics\Model\Plugin\BaseUrlConfigPlugin" /> + </type> + <virtualType name="SignUpResponseResolver" type="Magento\Analytics\Model\Connector\Http\ResponseResolver"> + <arguments> + <argument name="responseHandlers" xsi:type="array"> + <item name="201" xsi:type="object">\Magento\Analytics\Model\Connector\ResponseHandler\SignUp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="UpdateResponseResolver" type="Magento\Analytics\Model\Connector\Http\ResponseResolver"> + <arguments> + <argument name="responseHandlers" xsi:type="array"> + <item name="201" xsi:type="object">Magento\Analytics\Model\Connector\ResponseHandler\Update</item> + <item name="401" xsi:type="object">Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="OtpResponseResolver" type="Magento\Analytics\Model\Connector\Http\ResponseResolver"> + <arguments> + <argument name="responseHandlers" xsi:type="array"> + <item name="201" xsi:type="object">Magento\Analytics\Model\Connector\ResponseHandler\OTP</item> + <item name="401" xsi:type="object">Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="NotifyDataChangedResponseResolver" type="Magento\Analytics\Model\Connector\Http\ResponseResolver"> + <arguments> + <argument name="responseHandlers" xsi:type="array"> + <item name="401" xsi:type="object">Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Analytics\Model\Connector\SignUpCommand"> + <arguments> + <argument name="responseResolver" xsi:type="object">SignUpResponseResolver</argument> + </arguments> + </type> + <type name="Magento\Analytics\Model\Connector\UpdateCommand"> + <arguments> + <argument name="responseResolver" xsi:type="object">UpdateResponseResolver</argument> + </arguments> + </type> + <type name="Magento\Analytics\Model\Connector\OTPRequest"> + <arguments> + <argument name="responseResolver" xsi:type="object">OtpResponseResolver</argument> + </arguments> + </type> + <type name="Magento\Analytics\Model\Connector\NotifyDataChangedCommand"> + <arguments> + <argument name="responseResolver" xsi:type="object">NotifyDataChangedResponseResolver</argument> + </arguments> + </type> + <type name="Magento\Config\Model\Config\TypePool"> + <arguments> + <argument name="sensitive" xsi:type="array"> + <item name="analytics/url/signup" xsi:type="string">1</item> + <item name="analytics/url/update" xsi:type="string">1</item> + <item name="analytics/url/bi_essentials" xsi:type="string">1</item> + <item name="analytics/url/otp" xsi:type="string">1</item> + <item name="analytics/url/report" xsi:type="string">1</item> + <item name="analytics/url/notify_data_changed" xsi:type="string">1</item> + <item name="analytics/general/token" xsi:type="string">1</item> + </argument> + <argument name="environment" xsi:type="array"> + <item name="crontab/default/jobs/analytics_collect_data/schedule/cron_expr" xsi:type="string">1</item> + <item name="crontab/default/jobs/analytics_update/schedule/cron_expr" xsi:type="string">1</item> + <item name="crontab/default/jobs/analytics_subscribe/schedule/cron_expr" xsi:type="string">1</item> + </argument> + </arguments> + </type> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_Analytics" > + <sequence> + <module name="Magento_Integration"/> + <module name="Magento_Backend"/> + <module name="Magento_Store"/> + <module name="Magento_Config"/> + </sequence> + </module> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Analytics:etc/reports.xsd"> + <report name="modules" connection="default" iterator="Magento\Analytics\Model\ReportXml\ModuleIterator"> + <source name="setup_module"> + <attribute name="module" alias="module_name"/> + <attribute name="schema_version"/> + <attribute name="data_version"/> + </source> + </report> + <report name="config_data" connection="default"> + <source name="core_config_data"> + <attribute name="path"/> + <attribute name="value"/> + </source> + </report> + <report name="stores" connection="default"> + <source name="store"> + <attribute name="store_id"/> + <attribute name="code"/> + <attribute name="group_id"/> + <attribute name="name"/> + <attribute name="is_active"/> + </source> + </report> + <report name="websites" connection="default"> + <source name="store_website"> + <attribute name="website_id"/> + <attribute name="code"/> + <attribute name="name"/> + <attribute name="default_group_id"/> + <attribute name="is_default"/> + </source> + </report> + <report name="groups" connection="default"> + <source name="store_group"> + <attribute name="group_id"/> + <attribute name="website_id"/> + <attribute name="name"/> + <attribute name="default_store_id"/> + </source> + </report> +</config> \ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="config"> + <xs:complexType> + <xs:sequence> + <xs:element name="report" type="reportDeclaration" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:complexType name="reportDeclaration"> + <xs:sequence> + <xs:element name="source" type="sourceDeclaration" minOccurs="1" maxOccurs="1" /> + <xs:any minOccurs="0"/> + </xs:sequence> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="connection" type="xs:string"/> + <xs:attribute name="iterator" type="xs:string"/> + </xs:complexType> + <xs:complexType name="sourceDeclaration"> + <xs:choice minOccurs="1" maxOccurs="unbounded"> + <xs:element name="attribute" type="attributeDeclaration" minOccurs="1" maxOccurs="unbounded" /> + <xs:element name="link-source" type="linkSourceDeclaration" minOccurs="0" maxOccurs="61" /> + <xs:element name="filter" type="filterDeclaration" minOccurs="0" maxOccurs="unbounded" /> + </xs:choice> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="alias" type="xs:string"/> + </xs:complexType> + <xs:complexType name="linkSourceDeclaration"> + <xs:choice minOccurs="1" maxOccurs="unbounded"> + <xs:element name="attribute" type="attributeDeclaration" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="filter" type="filterDeclaration" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="using" type="filterDeclaration" minOccurs="1" maxOccurs="unbounded" /> + </xs:choice> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="alias" type="xs:string"/> + <xs:attribute name="link-type" type="xs:string"/> + </xs:complexType> + <xs:complexType name="attributeDeclaration"> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="alias" type="xs:string"/> + <xs:attribute name="function" type="functionDeclaration"/> + <xs:attribute name="group" type="xs:boolean" default="false"/> + <xs:attribute name="distinct" type="xs:boolean" default="false"/> + </xs:complexType> + <xs:complexType name="filterDeclaration"> + <xs:choice minOccurs="1" maxOccurs="unbounded"> + <xs:element name="filter" type="filterDeclaration" minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="condition" type="conditionDeclaration" minOccurs="1" maxOccurs="unbounded" /> + </xs:choice> + <xs:attribute name="glue" type="glueType" default="and" /> + </xs:complexType> + <xs:complexType name="conditionDeclaration" mixed="true"> + <xs:attribute name="attribute" type="xs:string" use="required" /> + <xs:attribute name="operator" type="xs:string" use="required" /> + <xs:attribute name="type" type="valueType" default="value" /> + </xs:complexType> + <xs:simpleType name="valueType"> + <xs:restriction base="xs:string"> + <xs:enumeration value="value" /> + <xs:enumeration value="variable" /> + <xs:enumeration value="identifier" /> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="functionDeclaration"> + <xs:restriction base="xs:string"> + <xs:enumeration value="count" /> + <xs:enumeration value="lower" /> + <xs:enumeration value="date" /> + <xs:enumeration value="sum" /> + <xs:enumeration value="max" /> + <xs:enumeration value="avg" /> + <xs:enumeration value="min" /> + <xs:enumeration value="sha1" /> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="glueType"> + <xs:restriction base="xs:string"> + <xs:enumeration value="and" /> + <xs:enumeration value="or" /> + </xs:restriction> + </xs:simpleType> +</xs:schema> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> + <route url="/V1/analytics/link" method="GET" secure="true"> + <service class="Magento\Analytics\Api\LinkProviderInterface" method="get"/> + <resources> + <resource ref="Magento_Analytics::analytics_api" /> + </resources> + </route> +</routes> 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. </br> For more information, see our + <a href=""https://magento.com/legal/terms/privacy"">terms and conditions</a>. + ","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. </br> For more information, see our + <a href=""https://magento.com/legal/terms/privacy"">terms and conditions</a>." +"Advanced Reporting Service","Advanced Reporting Service" +Industry,Industry +"Time of day to send data","Time of day to send data" +"<strong>Get more insights from Magento Business Intelligence</strong>","<strong>Get more insights from Magento Business Intelligence</strong>" +"Magento Business Intelligence provides you with a simple and clear path to + becoming more data driven.</br> Learn more about <a target=""_blank"" + href=""https://dashboard.rjmetrics.com/v2/magento/signup/"">BI Essentials</a> tier.","Magento Business Intelligence provides you with a simple and clear path to + becoming more data driven.</br> Learn more about <a target=""_blank"" + href=""https://dashboard.rjmetrics.com/v2/magento/signup/"">BI Essentials</a> 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" +"<p>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.</p><p>To opt in later: You can always turn on Advanced + Reporting in you Admin Panel.</p>","<p>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.</p><p>To opt in later: You can always turn on Advanced + Reporting in you Admin Panel.</p>" +"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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_Analytics', + __DIR__ +); diff --git a/app/code/Magento/Analytics/view/adminhtml/layout/adminhtml_dashboard_index.xml b/app/code/Magento/Analytics/view/adminhtml/layout/adminhtml_dashboard_index.xml new file mode 100644 index 0000000000000..545098de52dcc --- /dev/null +++ b/app/code/Magento/Analytics/view/adminhtml/layout/adminhtml_dashboard_index.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block template="Magento_Analytics::dashboard/section.phtml" + class="Magento\Backend\Block\Template" + name="analytics_service_external_link" + before="-"/> + </referenceContainer> + </body> +</page> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// @codingStandardsIgnoreFile +?> + +<section class="dashboard-advanced-reports" data-index="dashboard-advanced-reports"> + <div class="dashboard-advanced-reports-description"> + <header class="dashboard-advanced-reports-title"> + <?= $block->escapeHtml(__('Advanced Reporting')) ?> + </header> + <div class="dashboard-advanced-reports-content"> + <?= $block->escapeHtml(__('Gain new insights and take command of your business\' performance,' . + ' using our dynamic product, order, and customer reports tailored to your customer data.')) ?> + </div> + </div> + <div class="dashboard-advanced-reports-actions"> + <a href="<?= $block->escapeUrl($block->getUrl('analytics/reports/show')) ?>" + target="_blank" + class="action action-advanced-reports" + data-index="analytics-service-link" + title="<?= $block->escapeHtmlAttr(__('Go to Advanced Reporting')) ?>"> + <span><?= $block->escapeHtml(__('Go to Advanced Reporting')) ?></span> + </a> + </div> +</section> diff --git a/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php new file mode 100644 index 0000000000000..76410794900e2 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api; + +/** + * Interface BulkStatusInterface. + * + * Bulk summary data with list of operations items short data. + * + * @api + */ +interface BulkStatusInterface extends \Magento\Framework\Bulk\BulkStatusInterface +{ + /** + * Get Bulk summary data with list of operations items full data. + * + * @param string $bulkUuid + * @return \Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getBulkDetailedStatus($bulkUuid); + + /** + * Get Bulk summary data with list of operations items short data. + * + * @param string $bulkUuid + * @return \Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getBulkShortStatus($bulkUuid); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php new file mode 100644 index 0000000000000..c7edd5c8ff9cd --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Interface AsyncResponseInterface + * Temporary data object to give response from webapi async router + * + * @api + */ +interface AsyncResponseInterface +{ + const BULK_UUID = 'bulk_uuid'; + const REQUEST_ITEMS = 'request_items'; + const ERRORS = 'errors'; + + /** + * Gets the bulk uuid. + * + * @return string Bulk Uuid. + */ + public function getBulkUuid(); + + /** + * Sets the bulk uuid. + * + * @param string $bulkUuid + * @return $this + */ + public function setBulkUuid($bulkUuid); + + /** + * Gets the list of request items with status data. + * + * @return \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface[] + */ + public function getRequestItems(); + + /** + * Sets the list of request items with status data. + * + * @param \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface[] $requestItems + * @return $this + */ + public function setRequestItems($requestItems); + + /** + * @param bool $isErrors + * @return $this + */ + public function setErrors($isErrors = false); + + /** + * Is there errors during processing bulk + * + * @return boolean + */ + public function isErrors(); + + /** + * Retrieve existing extension attributes object. + * + * @return \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface|null + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php new file mode 100644 index 0000000000000..f8b7e389d387d --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Interface BulkStatusInterface + * + * Bulk summary data with list of operations items summary data. + * + * @api + */ +interface BulkOperationsStatusInterface extends BulkSummaryInterface +{ + + const OPERATIONS_LIST = 'operations_list'; + + /** + * Retrieve list of operation with statuses (short data). + * + * @return \Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface[] + */ + public function getOperationsList(); + + /** + * Set operations list. + * + * @param \Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface[] $operationStatusList + * @return $this + */ + public function setOperationsList($operationStatusList); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php new file mode 100644 index 0000000000000..a433ec0953a83 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Interface BulkSummaryInterface + * @api + * @since 100.2.0 + */ +interface BulkSummaryInterface extends \Magento\Framework\Bulk\BulkSummaryInterface +{ + const USER_TYPE = 'user_type'; + + /** + * Retrieve existing extension attributes object. + * + * @return \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface|null + * @since 100.2.0 + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface $extensionAttributes + * @return $this + * @since 100.2.0 + */ + public function setExtensionAttributes( + \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface $extensionAttributes + ); + + /** + * Get user type + * + * @return int + */ + public function getUserType(); + + /** + * Set user type + * + * @param int $userType + * @return $this + */ + public function setUserType($userType); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php new file mode 100644 index 0000000000000..6e39177630857 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Interface BulkStatusInterface + * + * Bulk summary data with list of operations items full data. + * + * @api + */ +interface DetailedBulkOperationsStatusInterface extends BulkSummaryInterface +{ + + const OPERATIONS_LIST = 'operations_list'; + + /** + * Retrieve operations list. + * + * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + */ + public function getOperationsList(); + + /** + * Set operations list. + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface[] $operationStatusList + * @return $this + */ + public function setOperationsList($operationStatusList); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php new file mode 100644 index 0000000000000..3294078c2c1ea --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * ItemStatusInterface interface + * Temporary object with status of requested item. + * Indicate if entity param was Accepted|Rejected to bulk schedule + * + * @api + */ +interface ItemStatusInterface +{ + const ENTITY_ID = 'entity_id'; + const DATA_HASH = 'data_hash'; + const STATUS = 'status'; + const ERROR_MESSAGE = 'error_message'; + const ERROR_CODE = 'error_code'; + + const STATUS_ACCEPTED = 'accepted'; + const STATUS_REJECTED = 'rejected'; + + /** + * Get entity Id. + * + * @return int + */ + public function getId(); + + /** + * Sets entity Id. + * + * @param int $entityId + * @return $this + */ + public function setId($entityId); + + /** + * Get hash of entity data. + * + * @return string md5 hash of entity params array. + */ + public function getDataHash(); + + /** + * Sets hash of entity data. + * + * @param string $hash md5 hash of entity params array. + * @return $this + */ + public function setDataHash($hash); + + /** + * Get status. + * + * @return string accepted|rejected + */ + public function getStatus(); + + /** + * Sets entity status. + * + * @param string $status accepted|rejected + * @return $this + */ + public function setStatus($status = self::STATUS_ACCEPTED); + + /** + * Get error information. + * + * @return string|null + */ + public function getErrorMessage(); + + /** + * Sets error information. + * + * @param string|null|\Exception $error + * @return $this + */ + public function setErrorMessage($error = null); + + /** + * Get error code. + * + * @return int|null + */ + public function getErrorCode(); + + /** + * Sets error information. + * + * @param int|null|\Exception $errorCode Default: null + * @return $this + */ + public function setErrorCode($errorCode = null); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/OperationInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/OperationInterface.php new file mode 100644 index 0000000000000..95366a96a87b2 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/OperationInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Class OperationInterface + * @api + * @since 100.2.0 + */ +interface OperationInterface extends \Magento\Framework\Bulk\OperationInterface +{ + /** + * Retrieve existing extension attributes object. + * + * @return \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterface|null + * @since 100.2.0 + */ + public function getExtensionAttributes(); + + /** + * Set an extension attributes object. + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterface $extensionAttributes + * @return $this + * @since 100.2.0 + */ + public function setExtensionAttributes( + \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterface $extensionAttributes + ); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/OperationListInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/OperationListInterface.php new file mode 100644 index 0000000000000..474ca6b94885d --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/OperationListInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * List of bulk operations. Used for mass save of operations via entity manager. + * @api + * @since 100.2.0 + */ +interface OperationListInterface +{ + /** + * Get list of operations. + * + * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + * @since 100.2.0 + */ + public function getItems(); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php new file mode 100644 index 0000000000000..c3d221b7ef4f8 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Bulk operation search result interface. + * + * An bulk is a group of queue messages. An bulk operation item is a queue message. + * @api + */ +interface OperationSearchResultsInterface extends \Magento\Framework\Api\SearchResultsInterface +{ + /** + * Get list of operations. + * + * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + */ + public function getItems(); + + /** + * Set list of operations. + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface[] $items + * @return $this + */ + public function setItems(array $items); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php new file mode 100644 index 0000000000000..3b9f53b34162a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api\Data; + +/** + * Getter Class OperationsStatusInterface + * Instead of OperationInterface this class don't provide all operation data + * and not responsive to set any data, just to get operation data + * without serialized_data and result_serialized_data + * + * @api + */ +interface SummaryOperationStatusInterface +{ + /** + * Operation id + * + * @return int + */ + public function getId(); + + /** + * Get operation status + * + * OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED + * + * @return int + */ + public function getStatus(); + + /** + * Get result message + * + * @return string + */ + public function getResultMessage(); + + /** + * Get error code + * + * @return int + */ + public function getErrorCode(); +} diff --git a/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php b/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php new file mode 100644 index 0000000000000..17547321b827f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api; + +/** + * Bulk operation item repository interface. + * + * An bulk is a group of queue messages. An bulk operation item is a queue message. + * @api + */ +interface OperationRepositoryInterface +{ + /** + * Lists the bulk operation items that match specified search criteria. + * + * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria + * @return \Magento\AsynchronousOperations\Api\Data\OperationSearchResultsInterface + */ + public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); +} diff --git a/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/BackButton.php b/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/BackButton.php new file mode 100644 index 0000000000000..c2591987da012 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Block/Adminhtml/Bulk/Details/BackButton.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\UrlInterface; + +/** + * Back button configuration provider + */ +class BackButton implements ButtonProviderInterface +{ + /** + * URL builder + * + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param UrlInterface $urlBuilder + */ + public function __construct( + UrlInterface $urlBuilder + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\Bulk\OperationInterface; + +/** + * Back button configuration provider + */ +class DoneButton implements ButtonProviderInterface +{ + /** + * @var \Magento\Framework\Bulk\BulkStatusInterface + */ + private $bulkStatus; + + /** + * @var \Magento\Framework\App\RequestInterface + */ + private $request; + + /** + * @param \Magento\Framework\Bulk\BulkStatusInterface $bulkStatus + * @param \Magento\Framework\App\RequestInterface $request + */ + public function __construct( + \Magento\Framework\Bulk\BulkStatusInterface $bulkStatus, + \Magento\Framework\App\RequestInterface $request + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; + +/** + * Retry button configuration provider + */ +class RetryButton implements ButtonProviderInterface +{ + /** + * @var \Magento\AsynchronousOperations\Model\Operation\Details + */ + private $details; + + /** + * @var \Magento\Framework\App\RequestInterface + */ + private $request; + + /** + * @var string + */ + private $targetName; + + /** + * RetryButton constructor. + * + * @param \Magento\AsynchronousOperations\Model\Operation\Details $details + * @param \Magento\Framework\App\RequestInterface $request + * @param string $targetName + */ + public function __construct( + \Magento\AsynchronousOperations\Model\Operation\Details $details, + \Magento\Framework\App\RequestInterface $request, + $targetName = 'bulk_details_form.bulk_details_form' + ) { + $this->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..a450187dd094b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Controller\Adminhtml\Bulk; + +/** + * Class View Operation Details Controller + */ +class Details extends \Magento\Backend\App\Action implements \Magento\Framework\App\Action\HttpGetActionInterface +{ + /** + * @var \Magento\Framework\View\Result\PageFactory + */ + private $resultPageFactory; + + /** + * @var \Magento\AsynchronousOperations\Model\AccessValidator + */ + private $accessValidator; + + /** + * @var string + */ + private $menuId; + + /** + * Details constructor. + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param \Magento\AsynchronousOperations\Model\AccessValidator $accessValidator + * @param string $menuId + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\AsynchronousOperations\Model\AccessValidator $accessValidator, + $menuId = 'Magento_AsynchronousOperations::system_magento_logging_bulk_operations' + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Controller\Adminhtml\Bulk; + +use Magento\AsynchronousOperations\Model\BulkManagement; +use Magento\AsynchronousOperations\Model\BulkNotificationManagement; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Backend\App\Action; +use Magento\AsynchronousOperations\Model\AccessValidator; +use Magento\Framework\Controller\ResultFactory; + +/** + * Class Bulk Retry Controller + */ +class Retry extends Action +{ + /** + * @var BulkManagement + */ + private $bulkManagement; + + /** + * @var BulkNotificationManagement + */ + private $notificationManagement; + + /** + * @var \Magento\AsynchronousOperations\Model\AccessValidator + */ + private $accessValidator; + + /** + * Retry constructor. + * @param Context $context + * @param BulkManagement $bulkManagement + * @param BulkNotificationManagement $notificationManagement + * @param AccessValidator $accessValidator + */ + public function __construct( + Context $context, + BulkManagement $bulkManagement, + BulkNotificationManagement $notificationManagement, + AccessValidator $accessValidator + ) { + parent::__construct($context); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Controller\Adminhtml\Index; + +class Index extends \Magento\Backend\App\Action +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Logging::system_magento_logging_bulk_operations'; + + /** + * @var \Magento\Framework\View\Result\PageFactory + */ + private $resultPageFactory; + + /** + * @var string + */ + private $menuId; + + /** + * Details constructor. + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param string $menuId + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + $menuId = 'Magento_AsynchronousOperations::system_magento_logging_bulk_operations' + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Controller\Adminhtml\Notification; + +use Magento\AsynchronousOperations\Model\BulkNotificationManagement; +use Magento\Backend\App\Action\Context; +use Magento\Backend\App\Action; +use Magento\Framework\Controller\ResultFactory; + +/** + * Class Bulk Notification Dismiss Controller + */ +class Dismiss extends Action +{ + /** + * @var BulkNotificationManagement + */ + private $notificationManagement; + + /** + * Class constructor. + * + * @param Context $context + * @param BulkNotificationManagement $notificationManagement + */ + public function __construct( + Context $context, + BulkNotificationManagement $notificationManagement + ) { + parent::__construct($context); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Cron; + +use Magento\Framework\App\ResourceConnection; +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\App\Config\ScopeConfigInterface; + +class BulkCleanup +{ + /** + * @var DateTime + */ + private $dateTime; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var \Magento\Framework\Stdlib\DateTime\DateTime + */ + private $date; + + /** + * BulkCleanup constructor. + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + * @param DateTime $dateTime + * @param ScopeConfigInterface $scopeConfig + * @param DateTime\DateTime $time + */ + public function __construct( + MetadataPool $metadataPool, + ResourceConnection $resourceConnection, + DateTime $dateTime, + ScopeConfigInterface $scopeConfig, + \Magento\Framework\Stdlib\DateTime\DateTime $time + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model; + +/** + * Class AccessValidator + */ +class AccessValidator +{ + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + + /** + * @var \Magento\Framework\EntityManager\EntityManager + */ + private $entityManager; + + /** + * @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory + */ + private $bulkSummaryFactory; + + /** + * AccessValidator constructor. + * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param \Magento\Framework\EntityManager\EntityManager $entityManager + * @param \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory + */ + public function __construct( + \Magento\Authorization\Model\UserContextInterface $userContext, + \Magento\Framework\EntityManager\EntityManager $entityManager, + \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\AsyncResponseInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Api\ExtensibleDataInterface; + +class AsyncResponse extends DataObject implements AsyncResponseInterface, ExtensibleDataInterface +{ + /** + * @inheritDoc + */ + public function getBulkUuid() + { + return $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\BulkDescription; + +use Magento\Framework\Bulk\BulkSummaryInterface; + +/** + * Class for grid options + */ +class Options implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * @var \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory + */ + private $bulkCollectionFactory; + + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + + /** + * Options constructor. + * @param \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory $bulkCollection + * @param \Magento\Authorization\Model\UserContextInterface $userContext + */ + public function __construct( + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory $bulkCollection, + \Magento\Authorization\Model\UserContextInterface $userContext + ) { + $this->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..faf01921e5737 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php @@ -0,0 +1,220 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\MessageQueue\BulkPublisherInterface; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory; +use Magento\Authorization\Model\UserContextInterface; + +/** + * Class BulkManagement + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface +{ + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var BulkSummaryInterfaceFactory + */ + private $bulkSummaryFactory; + + /** + * @var CollectionFactory + */ + private $operationCollectionFactory; + + /** + * @var BulkPublisherInterface + */ + private $publisher; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * BulkManagement constructor. + * @param EntityManager $entityManager + * @param BulkSummaryInterfaceFactory $bulkSummaryFactory + * @param CollectionFactory $operationCollectionFactory + * @param BulkPublisherInterface $publisher + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + * @param \Psr\Log\LoggerInterface $logger + * @param UserContextInterface $userContext + */ + public function __construct( + EntityManager $entityManager, + BulkSummaryInterfaceFactory $bulkSummaryFactory, + CollectionFactory $operationCollectionFactory, + BulkPublisherInterface $publisher, + MetadataPool $metadataPool, + ResourceConnection $resourceConnection, + \Psr\Log\LoggerInterface $logger, + UserContextInterface $userContext = null + ) { + $this->entityManager = $entityManager; + $this->bulkSummaryFactory= $bulkSummaryFactory; + $this->operationCollectionFactory = $operationCollectionFactory; + $this->metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->publisher = $publisher; + $this->logger = $logger; + $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); + } + + /** + * @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(); + $userType = $this->userContext->getUserType(); + if ($userType === null) { + $userType = UserContextInterface::USER_TYPE_ADMIN; + } + 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->setUserType($userType); + $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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory as BulkCollectionFactory; +use Magento\Framework\Data\Collection; + +/** + * Class for bulk notification manager + */ +class BulkNotificationManagement +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var BulkCollectionFactory + */ + private $bulkCollectionFactory; + + /** + * BulkManagement constructor. + * + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + * @param BulkCollectionFactory $bulkCollectionFactory + * @param \Psr\Log\LoggerInterface $logger + */ + public function __construct( + MetadataPool $metadataPool, + ResourceConnection $resourceConnection, + BulkCollectionFactory $bulkCollectionFactory, + \Psr\Log\LoggerInterface $logger + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\EntityManager\EntityManager; +use Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterfaceFactory as BulkStatusShortFactory; +use Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterfaceFactory as BulkStatusDetailedFactory; +use Magento\AsynchronousOperations\Api\Data\OperationDetailsInterfaceFactory; +use Magento\AsynchronousOperations\Api\BulkStatusInterface; +use Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory; + +/** + * Class BulkStatus + */ +class BulkOperationsStatus implements BulkStatusInterface +{ + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var BulkStatusDetailedFactory + */ + private $bulkDetailedFactory; + + /** + * @var BulkStatusShortFactory + */ + private $bulkShortFactory; + + /** + * @var BulkStatus + */ + private $bulkStatus; + + /** + * @var CollectionFactory + */ + private $operationCollectionFactory; + + /** + * Init dependencies. + * + * @param BulkStatus $bulkStatus + * @param CollectionFactory $operationCollection + * @param BulkStatusDetailedFactory $bulkDetailedFactory + * @param BulkStatusShortFactory $bulkShortFactory + * @param \Magento\Framework\EntityManager\EntityManager $entityManager + */ + public function __construct( + BulkStatus $bulkStatus, + CollectionFactory $operationCollection, + BulkStatusDetailedFactory $bulkDetailedFactory, + BulkStatusShortFactory $bulkShortFactory, + EntityManager $entityManager + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\AsynchronousOperations\Model\BulkStatus\CalculatedStatusSql; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Class BulkStatus + */ +class BulkStatus implements \Magento\Framework\Bulk\BulkStatusInterface +{ + /** + * @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory + */ + private $bulkCollectionFactory; + + /** + * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory + */ + private $operationCollectionFactory; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var CalculatedStatusSql + */ + private $calculatedStatusSql; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * BulkStatus constructor. + * @param ResourceModel\Bulk\CollectionFactory $bulkCollection + * @param ResourceModel\Operation\CollectionFactory $operationCollection + * @param ResourceConnection $resourceConnection + * @param CalculatedStatusSql $calculatedStatusSql + * @param MetadataPool $metadataPool + */ + public function __construct( + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory $bulkCollection, + \Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory $operationCollection, + ResourceConnection $resourceConnection, + CalculatedStatusSql $calculatedStatusSql, + MetadataPool $metadataPool + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model\BulkStatus; + +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; + +class CalculatedStatusSql +{ + /** + * Get sql to calculate bulk status + * + * @param string $operationTableName + * @return \Zend_Db_Expr + */ + public function get($operationTableName) + { + return new \Zend_Db_Expr( + '(IF( + (SELECT count(*) + FROM ' . $operationTableName . ' + WHERE bulk_uuid = main_table.uuid + ) = 0, + ' . BulkSummaryInterface::NOT_STARTED . ', + (SELECT MAX(status) FROM ' . $operationTableName . ' WHERE bulk_uuid = main_table.uuid) + ))' + ); + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Detailed.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Detailed.php new file mode 100644 index 0000000000000..334abb33fd106 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Detailed.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model\BulkStatus; + +use Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterface; +use Magento\AsynchronousOperations\Model\BulkSummary; + +class Detailed extends BulkSummary implements DetailedBulkOperationsStatusInterface +{ + /** + * @inheritDoc + */ + public function getOperationsList() + { + return $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\BulkStatus; + +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; + +/** + * Class Options + */ +class Options implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * @return array + */ + public function toOptionArray() + { + return [ + [ + 'value' => 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model\BulkStatus; + +use Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterface; +use Magento\AsynchronousOperations\Model\BulkSummary; + +class Short extends BulkSummary implements BulkOperationsStatusInterface +{ + /** + * @inheritDoc + */ + public function getOperationsList() + { + return $this->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..1d834ad10b2e7 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\Framework\DataObject; + +/** + * Class BulkSummary + */ +class BulkSummary extends DataObject implements BulkSummaryInterface, \Magento\Framework\Api\ExtensibleDataInterface +{ + /** + * @inheritDoc + */ + public function getBulkId() + { + return $this->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 getUserType() + { + return $this->getData(self::USER_TYPE); + } + + /** + * @inheritDoc + */ + public function setUserType($userType) + { + return $this->setData(self::USER_TYPE, $userType); + } + + /** + * @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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; + +/** + * Class for accessing to Webapi_Async configuration. + * + * @api + */ +interface ConfigInterface +{ + /**#@+ + * Constants for Webapi Asynchronous Config generation + */ + const CACHE_ID = 'webapi_async_config'; + const TOPIC_PREFIX = 'async.'; + const DEFAULT_CONSUMER_INSTANCE = MassConsumer::class; + const DEFAULT_CONSUMER_CONNECTION = 'amqp'; + const DEFAULT_CONSUMER_MAX_MESSAGE = null; + const SERVICE_PARAM_KEY_INTERFACE = 'interface'; + const SERVICE_PARAM_KEY_METHOD = 'method'; + const SERVICE_PARAM_KEY_TOPIC = 'topic'; + const DEFAULT_HANDLER_NAME = 'async'; + const SYSTEM_TOPIC_NAME = 'async.system.required.wrapper.topic'; + const SYSTEM_TOPIC_CONFIGURATION = [ + CommunicationConfig::TOPIC_NAME => 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\Entity; + +use Magento\Framework\EntityManager\MapperInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * @deprecated 100.2.0 + */ +class BulkSummaryMapper implements MapperInterface +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + */ + public function __construct( + MetadataPool $metadataPool, + ResourceConnection $resourceConnection + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\ItemStatusInterface; +use Magento\Framework\DataObject; + +class ItemStatus extends DataObject implements ItemStatusInterface +{ + /** + * @inheritDoc + */ + public function getId() + { + return $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\App\ResourceConnection; +use Psr\Log\LoggerInterface; +use Magento\Framework\MessageQueue\MessageLockException; +use Magento\Framework\MessageQueue\ConnectionLostException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\MessageQueue\CallbackInvoker; +use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; +use Magento\Framework\MessageQueue\EnvelopeInterface; +use Magento\Framework\MessageQueue\QueueInterface; +use Magento\Framework\MessageQueue\LockInterface; +use Magento\Framework\MessageQueue\MessageController; +use Magento\Framework\MessageQueue\ConsumerInterface; + +/** + * Class Consumer used to process OperationInterface messages. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MassConsumer implements ConsumerInterface +{ + /** + * @var \Magento\Framework\MessageQueue\CallbackInvoker + */ + private $invoker; + + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resource; + + /** + * @var \Magento\Framework\MessageQueue\ConsumerConfigurationInterface + */ + private $configuration; + + /** + * @var \Magento\Framework\MessageQueue\MessageController + */ + private $messageController; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var OperationProcessor + */ + private $operationProcessor; + + /** + * Initialize dependencies. + * + * @param CallbackInvoker $invoker + * @param ResourceConnection $resource + * @param MessageController $messageController + * @param ConsumerConfigurationInterface $configuration + * @param OperationProcessorFactory $operationProcessorFactory + * @param LoggerInterface $logger + */ + public function __construct( + CallbackInvoker $invoker, + ResourceConnection $resource, + MessageController $messageController, + ConsumerConfigurationInterface $configuration, + OperationProcessorFactory $operationProcessorFactory, + LoggerInterface $logger + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\MessageQueue\MessageValidator; +use Magento\Framework\MessageQueue\MessageEncoder; +use Magento\Framework\MessageQueue\Publisher\ConfigInterface as PublisherConfig; +use Magento\Framework\MessageQueue\Bulk\ExchangeRepository; +use Magento\Framework\MessageQueue\EnvelopeFactory; +use Magento\AsynchronousOperations\Model\ConfigInterface as AsyncConfig; +use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\MessageQueue\MessageIdGeneratorInterface; + +/** + * Class MassPublisher used for encoding topic entities to OperationInterface and publish them. + */ +class MassPublisher implements PublisherInterface +{ + /** + * @var \Magento\Framework\MessageQueue\Bulk\ExchangeRepository + */ + private $exchangeRepository; + + /** + * @var \Magento\Framework\MessageQueue\EnvelopeFactory + */ + private $envelopeFactory; + + /** + * @var \Magento\Framework\MessageQueue\MessageEncoder + */ + private $messageEncoder; + + /** + * @var \Magento\Framework\MessageQueue\MessageValidator + */ + private $messageValidator; + + /** + * @var \Magento\Framework\MessageQueue\Publisher\ConfigInterface + */ + private $publisherConfig; + + /** + * @var \Magento\Framework\MessageQueue\MessageIdGeneratorInterface + */ + private $messageIdGenerator; + + /** + * Initialize dependencies. + * + * @param \Magento\Framework\MessageQueue\Bulk\ExchangeRepository $exchangeRepository + * @param \Magento\Framework\MessageQueue\EnvelopeFactory $envelopeFactory + * @param \Magento\Framework\MessageQueue\MessageEncoder $messageEncoder + * @param \Magento\Framework\MessageQueue\MessageValidator $messageValidator + * @param \Magento\Framework\MessageQueue\Publisher\ConfigInterface $publisherConfig + * @param \Magento\Framework\MessageQueue\MessageIdGeneratorInterface $messageIdGenerator + */ + public function __construct( + ExchangeRepository $exchangeRepository, + EnvelopeFactory $envelopeFactory, + MessageEncoder $messageEncoder, + MessageValidator $messageValidator, + PublisherConfig $publisherConfig, + MessageIdGeneratorInterface $messageIdGenerator + ) { + $this->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..eae92e1663fc8 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\AsynchronousOperations\Api\Data\ItemStatusInterfaceFactory; +use Magento\AsynchronousOperations\Api\Data\AsyncResponseInterface; +use Magento\AsynchronousOperations\Api\Data\AsyncResponseInterfaceFactory; +use Magento\AsynchronousOperations\Api\Data\ItemStatusInterface; +use Magento\Framework\Bulk\BulkManagementInterface; +use Magento\Framework\Exception\BulkException; +use Psr\Log\LoggerInterface; +use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; +use Magento\Authorization\Model\UserContextInterface; + +/** + * Class MassSchedule used for adding multiple entities as Operations to Bulk Management with the status tracking + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Suppressed without refactoring to not introduce BiC + */ +class MassSchedule +{ + /** + * @var \Magento\Framework\DataObject\IdentityGeneratorInterface + */ + private $identityService; + + /** + * @var AsyncResponseInterfaceFactory + */ + private $asyncResponseFactory; + + /** + * @var ItemStatusInterfaceFactory + */ + private $itemStatusInterfaceFactory; + + /** + * @var \Magento\Framework\Bulk\BulkManagementInterface + */ + private $bulkManagement; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var OperationRepository + */ + private $operationRepository; + + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + + /** + * Initialize dependencies. + * + * @param IdentityGeneratorInterface $identityService + * @param ItemStatusInterfaceFactory $itemStatusInterfaceFactory + * @param AsyncResponseInterfaceFactory $asyncResponseFactory + * @param BulkManagementInterface $bulkManagement + * @param LoggerInterface $logger + * @param OperationRepository $operationRepository + * @param UserContextInterface $userContext + */ + public function __construct( + IdentityGeneratorInterface $identityService, + ItemStatusInterfaceFactory $itemStatusInterfaceFactory, + AsyncResponseInterfaceFactory $asyncResponseFactory, + BulkManagementInterface $bulkManagement, + LoggerInterface $logger, + OperationRepository $operationRepository, + UserContextInterface $userContext = null + ) { + $this->identityService = $identityService; + $this->itemStatusInterfaceFactory = $itemStatusInterfaceFactory; + $this->asyncResponseFactory = $asyncResponseFactory; + $this->bulkManagement = $bulkManagement; + $this->logger = $logger; + $this->operationRepository = $operationRepository; + $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); + } + + /** + * Schedule new bulk operation based on the list of entities + * + * @param string $topicName + * @param array $entitiesArray + * @param string $groupId + * @param string $userId + * @return AsyncResponseInterface + * @throws BulkException + * @throws LocalizedException + */ + public function publishMass($topicName, array $entitiesArray, $groupId = null, $userId = null) + { + $bulkDescription = __('Topic %1', $topicName); + + if ($userId == null) { + $userId = $this->userContext->getUserId(); + } + + 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..dbe6ecc1b6b1f --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/Operation.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\DataObject; + +/** + * Class Operation + */ +class Operation extends DataObject implements OperationInterface +{ + /** + * @inheritDoc + */ + public function getId() + { + return $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model\Operation; + +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\Bulk\BulkStatusInterface; + +class Details +{ + /** + * @var array + */ + private $operationCache = []; + + /** + * @var \Magento\Framework\Bulk\BulkStatusInterface + */ + private $bulkStatus; + + /** + * @var null + */ + private $bulkUuid; + + /** + * Map between status codes and human readable indexes + * + * @var array + */ + private $statusMap = [ + OperationInterface::STATUS_TYPE_COMPLETE => '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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model; + +/** + * List of bulk operations. + */ +class OperationList implements \Magento\AsynchronousOperations\Api\Data\OperationListInterface +{ + /** + * @var array + */ + private $items; + + /** + * @param array $items [optional] + */ + public function __construct(array $items = []) + { + $this->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..f204f63ed032b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\EntityManager\EntityManager; + +/** + * Class OperationManagement + */ +class OperationManagement implements \Magento\Framework\Bulk\OperationManagementInterface +{ + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * OperationManagement constructor. + * + * @param EntityManager $entityManager + * @param OperationInterfaceFactory $operationFactory + * @param \Psr\Log\LoggerInterface $logger + */ + public function __construct( + EntityManager $entityManager, + OperationInterfaceFactory $operationFactory, + \Psr\Log\LoggerInterface $logger + ) { + $this->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); + $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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\Serialize\Serializer\Json; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\Bulk\OperationManagementInterface; +use Magento\AsynchronousOperations\Model\ConfigInterface as AsyncConfig; +use Magento\Framework\MessageQueue\MessageValidator; +use Magento\Framework\MessageQueue\MessageEncoder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; +use Psr\Log\LoggerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\TemporaryStateExceptionInterface; +use Magento\Framework\DB\Adapter\ConnectionException; +use Magento\Framework\DB\Adapter\DeadlockException; +use Magento\Framework\DB\Adapter\LockWaitException; +use Magento\Framework\Webapi\ServiceOutputProcessor; +use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; + +/** + * Class OperationProcessor + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class OperationProcessor +{ + /** + * @var Json + */ + private $jsonHelper; + + /** + * @var OperationManagementInterface + */ + private $operationManagement; + + /** + * @var MessageEncoder + */ + private $messageEncoder; + + /** + * @var MessageValidator + */ + private $messageValidator; + + /** + * @var ConsumerConfigurationInterface + */ + private $configuration; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ServiceOutputProcessor + */ + private $serviceOutputProcessor; + + /** + * @var CommunicationConfig + */ + private $communicationConfig; + + /** + * OperationProcessor constructor. + * + * @param MessageValidator $messageValidator + * @param MessageEncoder $messageEncoder + * @param ConsumerConfigurationInterface $configuration + * @param Json $jsonHelper + * @param OperationManagementInterface $operationManagement + * @param LoggerInterface $logger + * @param \Magento\Framework\Webapi\ServiceOutputProcessor $serviceOutputProcessor + * @param \Magento\Framework\Communication\ConfigInterface $communicationConfig + */ + public function __construct( + MessageValidator $messageValidator, + MessageEncoder $messageEncoder, + ConsumerConfigurationInterface $configuration, + Json $jsonHelper, + OperationManagementInterface $operationManagement, + ServiceOutputProcessor $serviceOutputProcessor, + CommunicationConfig $communicationConfig, + LoggerInterface $logger + ) { + $this->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/OperationRepository.php b/app/code/Magento/AsynchronousOperations/Model/OperationRepository.php new file mode 100644 index 0000000000000..ec76ff6519757 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationRepository.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\AsynchronousOperations\Api\Data\OperationSearchResultsInterfaceFactory as SearchResultFactory; +use Magento\AsynchronousOperations\Api\Data\OperationExtensionInterfaceFactory; +use Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Repository class for @see \Magento\AsynchronousOperations\Api\OperationRepositoryInterface + */ +class OperationRepository implements \Magento\AsynchronousOperations\Api\OperationRepositoryInterface +{ + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var SearchResultFactory + */ + private $searchResultFactory; + + /** + * @var JoinProcessorInterface + */ + private $joinProcessor; + + /** + * @var \Magento\AsynchronousOperations\Api\Data\OperationExtensionInterfaceFactory + */ + private $operationExtensionFactory; + + /** + * @var CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * OperationRepository constructor. + * + * @param EntityManager $entityManager + * @param CollectionFactory $collectionFactory + * @param SearchResultFactory $searchResultFactory + * @param JoinProcessorInterface $joinProcessor + * @param OperationExtensionInterfaceFactory $operationExtension + * @param CollectionProcessorInterface $collectionProcessor + * @param \Psr\Log\LoggerInterface $logger + */ + public function __construct( + EntityManager $entityManager, + CollectionFactory $collectionFactory, + SearchResultFactory $searchResultFactory, + JoinProcessorInterface $joinProcessor, + OperationExtensionInterfaceFactory $operationExtension, + CollectionProcessorInterface $collectionProcessor, + \Psr\Log\LoggerInterface $logger + ) { + $this->entityManager = $entityManager; + $this->collectionFactory = $collectionFactory; + $this->searchResultFactory = $searchResultFactory; + $this->joinProcessor = $joinProcessor; + $this->operationExtensionFactory = $operationExtension; + $this->collectionProcessor = $collectionProcessor; + $this->logger = $logger; + $this->collectionProcessor = $collectionProcessor; + } + + /** + * @inheritDoc + */ + public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) + { + /** @var \Magento\AsynchronousOperations\Api\Data\OperationSearchResultsInterface $searchResult */ + $searchResult = $this->searchResultFactory->create(); + + /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Collection $collection */ + $collection = $this->collectionFactory->create(); + $this->joinProcessor->process($collection, \Magento\AsynchronousOperations\Api\Data\OperationInterface::class); + $this->collectionProcessor->process($searchCriteria, $collection); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setTotalCount($collection->getSize()); + $searchResult->setItems($collection->getItems()); + + return $searchResult; + } +} 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model; + +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Class OperationShortDetails + */ +class OperationStatus extends DataObject implements SummaryOperationStatusInterface, ExtensibleDataInterface +{ + /** + * @inheritDoc + */ + public function getId() + { + return $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model\ResourceModel; + +/** + * Class Bulk + */ +class Bulk extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +{ + /** + * Initialize banner sales rule resource model + * + * @return void + */ + protected function _construct() + { + $this->_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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\ResourceModel\Bulk; + +/** + * Class Collection + * @codeCoverageIgnore + */ +class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection +{ + /** + * Define collection item type and corresponding table + * + * @return void + */ + protected function _construct() + { + $this->_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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model\ResourceModel; + +/** + * Class Operation + */ +class Operation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +{ + /** + * Initialize banner sales rule resource model + * + * @return void + */ + protected function _construct() + { + $this->_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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\ResourceModel\Operation; + +use Magento\Framework\EntityManager\Operation\CheckIfExistsInterface; +use Magento\Framework\App\ResourceConnection; + +/** + * CheckIfExists operation for list of bulk operations. + */ +class CheckIfExists implements CheckIfExistsInterface +{ + /** + * Always returns false because all operations will be saved using insertOnDuplicate query. + * + * @param object $entity + * @param array $arguments + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute($entity, $arguments = []) + { + return false; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/Collection.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/Collection.php new file mode 100644 index 0000000000000..18c5f09794c77 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/Collection.php @@ -0,0 +1,28 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\ResourceModel\Operation; + +/** + * Class Collection + * @codeCoverageIgnore + */ +class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection +{ + /** + * Define collection item type and corresponding table + * + * @return void + */ + protected function _construct() + { + $this->_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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\ResourceModel\Operation; + +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\EntityManager\TypeResolver; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Phrase; + +/** + * Create operation for list of bulk operations. + */ +class Create implements \Magento\Framework\EntityManager\Operation\CreateInterface +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var TypeResolver + */ + private $typeResolver; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param MetadataPool $metadataPool + * @param TypeResolver $typeResolver + * @param ResourceConnection $resourceConnection + */ + public function __construct( + MetadataPool $metadataPool, + TypeResolver $typeResolver, + ResourceConnection $resourceConnection + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model\ResourceModel\Operation; + +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\MessageQueue\MessageValidator; +use Magento\Framework\MessageQueue\MessageEncoder; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\EntityManager\EntityManager; + +/** + * Create operation for list of bulk operations. + */ +class OperationRepository +{ + /** + * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var MessageEncoder + */ + private $messageEncoder; + + /** + * @var MessageValidator + */ + private $messageValidator; + + /** + * @param OperationInterfaceFactory $operationFactory + * @param EntityManager $entityManager + * @param MessageValidator $messageValidator + * @param MessageEncoder $messageEncoder + * @param Json $jsonSerializer + */ + public function __construct( + OperationInterfaceFactory $operationFactory, + EntityManager $entityManager, + MessageValidator $messageValidator, + MessageEncoder $messageEncoder, + Json $jsonSerializer + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Model\ResourceModel\System\Message\Collection\Synchronized; + +/** + * Class Plugin to add bulks related notification messages to Synchronized Collection + */ +class Plugin +{ + /** + * @var \Magento\AdminNotification\Model\System\MessageFactory + */ + private $messageFactory; + + /** + * @var \Magento\Framework\Bulk\BulkStatusInterface + */ + private $bulkStatus; + + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + + /** + * @var \Magento\AsynchronousOperations\Model\Operation\Details + */ + private $operationDetails; + + /** + * @var \Magento\AsynchronousOperations\Model\BulkNotificationManagement + */ + private $bulkNotificationManagement; + + /** + * @var \Magento\Framework\AuthorizationInterface + */ + private $authorization; + + /** + * @var \Magento\AsynchronousOperations\Model\StatusMapper + */ + private $statusMapper; + + /** + * Plugin constructor. + * + * @param \Magento\AdminNotification\Model\System\MessageFactory $messageFactory + * @param \Magento\Framework\Bulk\BulkStatusInterface $bulkStatus + * @param \Magento\AsynchronousOperations\Model\BulkNotificationManagement $bulkNotificationManagement + * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param \Magento\AsynchronousOperations\Model\Operation\Details $operationDetails + * @param \Magento\Framework\AuthorizationInterface $authorization + * @param \Magento\AsynchronousOperations\Model\StatusMapper $statusMapper + */ + public function __construct( + \Magento\AdminNotification\Model\System\MessageFactory $messageFactory, + \Magento\Framework\Bulk\BulkStatusInterface $bulkStatus, + \Magento\AsynchronousOperations\Model\BulkNotificationManagement $bulkNotificationManagement, + \Magento\Authorization\Model\UserContextInterface $userContext, + \Magento\AsynchronousOperations\Model\Operation\Details $operationDetails, + \Magento\Framework\AuthorizationInterface $authorization, + \Magento\AsynchronousOperations\Model\StatusMapper $statusMapper + ) { + $this->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 .= '<strong>' + . __('%1 item(s) failed to update', $operationDetails['operations_failed']) + . '</strong>'; + } + 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Model; + +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\Bulk\BulkSummaryInterface; + +/** + * Class StatusMapper + */ +class StatusMapper +{ + /** + * Map operation status to bulk summary status + * + * @param int $operationStatus + * @return null|int + */ + public function operationStatusToBulkSummaryStatus($operationStatus) + { + $statusMapping = [ + OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED => 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/Mftf/README.md b/app/code/Magento/AsynchronousOperations/Test/Mftf/README.md new file mode 100644 index 0000000000000..2f73e44149f2c --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Asynchronous Operations Functional Tests + +The Functional Test Module for **Magento Asynchronous Operations** module. 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Block\Adminhtml\Bulk\Details; + +class BackButtonTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\BackButton + */ + protected $block; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $urlBuilderMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Block\Adminhtml\Bulk\Details; + +use Magento\Framework\Bulk\OperationInterface; + +class DoneButtonTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\DoneButton + */ + protected $block; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $bulkStatusMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $requestMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Block\Adminhtml\Bulk\Details; + +class RetryButtonTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\RetryButton + */ + protected $block; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $detailsMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $requestMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Controller\Adminhtml\Bulk; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DetailsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $viewMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\AsynchronousOperations\Controller\Adminhtml\Bulk\Details + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Controller\Adminhtml\Bulk; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Backend\Model\View\Result\RedirectFactory; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\AsynchronousOperations\Controller\Adminhtml\Bulk\Retry; +use Magento\AsynchronousOperations\Model\BulkManagement; +use Magento\AsynchronousOperations\Model\BulkNotificationManagement; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\Result\Json; + +class RetryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Retry + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkManagementMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $notificationManagementMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $jsonResultMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Controller\Adminhtml\Index; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class IndexTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $viewMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\AsynchronousOperations\Controller\Adminhtml\Index\Index + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Controller\Adminhtml\Notification; + +use Magento\AsynchronousOperations\Model\BulkNotificationManagement; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\AsynchronousOperations\Controller\Adminhtml\Notification\Dismiss; +use Magento\Framework\Controller\Result\Json; + +class DismissTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Dismiss + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $notificationManagementMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $jsonResultMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + $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->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Controller\Cron; + +class BulkCleanupTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var \Magento\AsynchronousOperations\Cron\BulkCleanup + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $timeMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model; + +class AccessValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Model\AccessValidator + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $userContextMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $entityManagerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkSummaryFactoryMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model\BulkDescription; + +class OptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Model\BulkDescription\Options + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkCollectionFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $userContextMock; + + protected function setUp() + { + $this->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..e5951fb129e7e --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php @@ -0,0 +1,298 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model; + +/** + * Unit test for BulkManagement model. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BulkManagementTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\EntityManager\EntityManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityManager; + + /** + * @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory + * |\PHPUnit_Framework_MockObject_MockObject + */ + private $bulkSummaryFactory; + + /** + * @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory + * |\PHPUnit_Framework_MockObject_MockObject + */ + private $operationCollectionFactory; + + /** + * @var \Magento\Framework\MessageQueue\BulkPublisherInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $publisher; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPool; + + /** + * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnection; + + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $logger; + + /** + * @var \Magento\AsynchronousOperations\Model\BulkManagement + */ + private $bulkManagement; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->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; + $userType = \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN; + $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('setUserType')->with($userType)->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..a5a75736d2441 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php @@ -0,0 +1,270 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model; + +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BulkStatusTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Model\BulkStatus + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkCollectionFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $operationCollectionFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $operationMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $calculatedStatusSqlMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkDetailedFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkShortFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $entityMetadataMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $entityManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + protected function setUp() + { + $this->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)); + } + + /** + * @return array + */ + 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..9543911c037d8 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/Entity/BulkSummaryMapperTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model\Entity; + +use Magento\AsynchronousOperations\Model\Entity\BulkSummaryMapper; + +/** + * Class BulkSummaryMapperTest + */ +class BulkSummaryMapperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Model\Entity\BulkSummaryMapper + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $entityMetadataMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + protected function setUp() + { + $this->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)); + } + + /** + * @return array + */ + 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model\Operation; + +use Magento\Framework\Bulk\OperationInterface; + +class DetailsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkStatusMock; + + /** + * @var \Magento\AsynchronousOperations\Model\Operation\Details + */ + private $model; + + protected function setUp() + { + $this->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..4c3e240da2a8a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Model; + +/** + * Class OperationManagementTest + */ +class OperationManagementTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Model\OperationManagement + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $entityManagerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $operationFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $operationMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + protected function setUp() + { + $this->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\OperationInterfaceFactory::class, + ['create'] + ); + $this->operationMock = + $this->createMock(\Magento\AsynchronousOperations\Api\Data\OperationInterface::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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model\ResourceModel\Operation; + +/** + * Unit test for Create operation. + */ +class CreateTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPool; + + /** + * @var \Magento\Framework\EntityManager\TypeResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $typeResolver; + + /** + * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnection; + + /** + * @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation\Create + */ + private $create; + + /** + * Set up. + * + * @return void + */ + protected function setUp() + { + $this->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..6a51258b34afc --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php @@ -0,0 +1,172 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Model\ResourceModel\System\Message\Collection\Synchronized; + +use Magento\AsynchronousOperations\Model\ResourceModel\System\Message\Collection\Synchronized\Plugin; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Bulk\BulkStatusInterface; +use Magento\AsynchronousOperations\Model\BulkNotificationManagement; +use Magento\AsynchronousOperations\Model\Operation\Details; +use Magento\Framework\AuthorizationInterface; +use Magento\AdminNotification\Model\ResourceModel\System\Message\Collection\Synchronized; + +/** + * Class PluginTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PluginTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Plugin + */ + private $plugin; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $messagefactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkStatusMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkNotificationMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $userContextMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $operationsDetailsMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $messageMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $collectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $statusMapper; + + /** + * @var string + */ + private $resourceName = 'Magento_Logging::system_magento_logging_bulk_operations'; + + protected function setUp() + { + $this->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']); + } + + /** + * @return array + */ + 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Model; + +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\Bulk\BulkSummaryInterface; + +/** + * Class StatusMapperTest + */ +class StatusMapperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Model\StatusMapper + */ + private $model; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Test\Unit\Ui\Component\AdminNotification; + +use Magento\Framework\AuthorizationInterface; + +class PluginTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\AsynchronousOperations\Ui\Component\AdminNotification\Plugin + */ + private $plugin; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Ui\Component\Listing\Column; + +use Magento\AsynchronousOperations\Model\BulkSummary; + +class ActionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + /** + * @var \Magento\AsynchronousOperations\Ui\Component\Listing\Column\Actions + */ + private $actionColumn; + + /** + * Set up + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Ui\Component\Listing\Column; + +use Magento\AsynchronousOperations\Model\BulkSummary; +use Magento\Framework\Bulk\BulkSummaryInterface; + +class NotificationActionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + /** + * @var \Magento\AsynchronousOperations\Ui\Component\Listing\Column\NotificationActions + */ + private $actionColumn; + + /** + * Set up + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Ui\Component\Listing\Column; + +use Magento\AsynchronousOperations\Model\BulkSummary; +use Magento\Framework\Bulk\BulkSummaryInterface; + +class NotificationDismissActionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + /** + * @var \Magento\AsynchronousOperations\Ui\Component\Listing\Column\NotificationDismissActions + */ + private $actionColumn; + + /** + * Set up + */ + protected function setUp() + { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Test\Unit\Ui\Component\Operation; + +use Magento\AsynchronousOperations\Ui\Component\Operation\DataProvider; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class DataProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DataProvider + */ + private $dataProvider; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkCollectionFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkCollectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $operationDetailsMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bulkMock; + + /** + * Set up + * + * @return void + */ + protected function setUp() + { + $helper = new ObjectManager($this); + + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Ui\Component\AdminNotification; + +/** + * Class Plugin to eliminate Bulk related links in the notification area + */ +class Plugin +{ + /** + * @var \Magento\Framework\AuthorizationInterface + */ + private $authorization; + + /** + * @var bool + */ + private $isAllowed; + + /** + * Plugin constructor. + * @param \Magento\Framework\AuthorizationInterface $authorization + */ + public function __construct( + \Magento\Framework\AuthorizationInterface $authorization + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Ui\Component\DataProvider\Bulk; + +use Magento\Framework\App\RequestInterface; + +/** + * Class IdentifierResolver + */ +class IdentifierResolver +{ + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + */ + public function __construct( + RequestInterface $request + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Failed; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Psr\Log\LoggerInterface as Logger; +use Magento\AsynchronousOperations\Ui\Component\DataProvider\Bulk\IdentifierResolver; +use Magento\Framework\Bulk\OperationInterface; + +/** + * Class SearchResult + */ +class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult +{ + /** + * @var IdentifierResolver + */ + private $identifierResolver; + + /** + * @var \Magento\Framework\Json\Helper\Data + */ + private $jsonHelper; + + /** + * SearchResult constructor. + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param IdentifierResolver $identifierResolver + * @param \Magento\Framework\Json\Helper\Data $jsonHelper + * @param string $mainTable + * @param null $resourceModel + * @param string $identifierName identifier field name for collection items + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + IdentifierResolver $identifierResolver, + \Magento\Framework\Json\Helper\Data $jsonHelper, + $mainTable = 'magento_operation', + $resourceModel = null, + $identifierName = 'id' + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Retriable; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Psr\Log\LoggerInterface as Logger; +use Magento\AsynchronousOperations\Ui\Component\DataProvider\Bulk\IdentifierResolver; +use Magento\Framework\Bulk\OperationInterface; + +/** + * Class SearchResult + */ +class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult +{ + /** + * @var IdentifierResolver + */ + private $identifierResolver; + + /** + * SearchResult constructor. + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param IdentifierResolver $identifierResolver + * @param string $mainTable + * @param null $resourceModel + * @param string $identifierName + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + IdentifierResolver $identifierResolver, + $mainTable = 'magento_operation', + $resourceModel = null, + $identifierName = 'id' + ) { + $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', '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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Ui\Component\DataProvider; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Psr\Log\LoggerInterface as Logger; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Bulk\BulkSummaryInterface; +use Magento\AsynchronousOperations\Model\StatusMapper; +use Magento\AsynchronousOperations\Model\BulkStatus\CalculatedStatusSql; + +/** + * Class SearchResult + */ +class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult +{ + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var StatusMapper + */ + private $statusMapper; + + /** + * @var array|int + */ + private $operationStatus; + + /** + * @var CalculatedStatusSql + */ + private $calculatedStatusSql; + + /** + * SearchResult constructor. + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param UserContextInterface $userContextInterface + * @param StatusMapper $statusMapper + * @param CalculatedStatusSql $calculatedStatusSql + * @param string $mainTable + * @param null $resourceModel + * @param string $identifierName + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + UserContextInterface $userContextInterface, + StatusMapper $statusMapper, + CalculatedStatusSql $calculatedStatusSql, + $mainTable = 'magento_bulk', + $resourceModel = null, + $identifierName = 'uuid' + ) { + $this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Ui\Component\Listing\Column; + +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Actions + */ +class Actions extends Column +{ + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + $dataSource = parent::prepareDataSource($dataSource); + + if (empty($dataSource['data']['items'])) { + return $dataSource; + } + + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Ui\Component\Listing\Column; + +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\Bulk\BulkSummaryInterface; + +/** + * Class Actions + */ +class NotificationActions extends Column +{ + /** + * {@inheritdoc} + */ + public function prepareDataSource(array $dataSource) + { + $dataSource = parent::prepareDataSource($dataSource); + + if (empty($dataSource['data']['items'])) { + return $dataSource; + } + + foreach ($dataSource['data']['items'] as &$item) { + if (isset($item['uuid'])) { + $item[$this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AsynchronousOperations\Ui\Component\Listing\Column; + +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\Bulk\BulkSummaryInterface; + +/** + * Class NotificationDismissActions + */ +class NotificationDismissActions extends Column +{ + /** + * {@inheritdoc} + */ + public function prepareDataSource(array $dataSource) + { + $dataSource = parent::prepareDataSource($dataSource); + + if (empty($dataSource['data']['items'])) { + return $dataSource; + } + + foreach ($dataSource['data']['items'] as &$item) { + if (isset($item['status']) + && ($item['status'] === BulkSummaryInterface::FINISHED_SUCCESSFULLY + || $item['status'] === BulkSummaryInterface::FINISHED_WITH_FAILURE) + ) { + $item[$this->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AsynchronousOperations\Ui\Component\Operation; + +/** + * Class DataProvider + */ +class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider +{ + /** + * @var \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\Collection + */ + protected $collection; + + /** + * @var \Magento\AsynchronousOperations\Model\Operation\Details + */ + private $operationDetails; + + /** + * @var \Magento\Framework\App\RequestInterface $request, + */ + private $request; + + /** + * DataProvider constructor. + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory $bulkCollectionFactory + * @param \Magento\AsynchronousOperations\Model\Operation\Details $operationDetails + * @param \Magento\Framework\App\RequestInterface $request + * @param array $meta + * @param array $data + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + \Magento\AsynchronousOperations\Model\ResourceModel\Bulk\CollectionFactory $bulkCollectionFactory, + \Magento\AsynchronousOperations\Model\Operation\Details $operationDetails, + \Magento\Framework\App\RequestInterface $request, + array $meta = [], + array $data = [] + ) { + $this->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..18927b5f4ecca --- /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": "*", + "php": "~7.1.3||~7.2.0" + }, + "suggest": { + "magento/module-admin-notification": "*", + "magento/module-logging": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Backend::system"> + <resource id="Magento_Logging::magento_logging" title="Action Log" translate="title" sortOrder="70"> + <resource id="Magento_Logging::magento_logging_events" title="Report" translate="title" sortOrder="10"/> + <resource id="Magento_Logging::system_magento_logging_bulk_operations" title="Bulk Actions" translate="title" sortOrder="15"/> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\AdminNotification\Model\ResourceModel\System\Message\Collection\Synchronized"> + <plugin name="afterToArray" type="Magento\AsynchronousOperations\Model\ResourceModel\System\Message\Collection\Synchronized\Plugin" /> + </type> + <type name="Magento\AdminNotification\Ui\Component\DataProvider\DataProvider"> + <plugin name="afterGetMeta" type="Magento\AsynchronousOperations\Ui\Component\AdminNotification\Plugin" /> + </type> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_AsynchronousOperations::system_magento_logging" + title="Action Logs" + translate="title" + module="Magento_AsynchronousOperations" + sortOrder="70" parent="Magento_Backend::system" + dependsOnModule="Magento_AsynchronousOperations" + resource="Magento_Logging::magento_logging"/> + <add id="Magento_AsynchronousOperations::system_magento_logging_bulk_operations" + title="Bulk Actions" + translate="title" + module="Magento_AsynchronousOperations" + sortOrder="25" + parent="Magento_AsynchronousOperations::system_magento_logging" + action="bulk/index/" + resource="Magento_Logging::system_magento_logging_bulk_operations"/> + </menu> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="bulk" frontName="bulk"> + <module name="Magento_AsynchronousOperations" before="Magento_Backend" /> + </route> + </router> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <tab>advanced</tab> + <group id="bulk" translate="label" showInDefault="1" showInWebsite="0" showInStore="0" sortOrder="600"> + <label>Bulk Actions</label> + <field id="lifetime" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Days Saved in Log</label> + </field> + </group> + </section> + </system> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <bulk> + <lifetime>60</lifetime> + </bulk> + </system> + </default> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> + <group id="default"> + <job name="bulk_cleanup" instance="Magento\AsynchronousOperations\Cron\BulkCleanup" method="execute"> + <schedule>* * * * *</schedule> + </job> + </group> +</config> 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..5cd55408838fe --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="magento_bulk" resource="default" engine="innodb" + comment="Bulk entity that represents set of related asynchronous operations"> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Bulk Internal ID (must not be exposed)"/> + <column xsi:type="varbinary" name="uuid" nullable="true" length="39" + comment="Bulk UUID (can be exposed to reference bulk entity)"/> + <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="ID of the WebAPI user that performed an action"/> + <column xsi:type="int" name="user_type" nullable="true" comment="Which type of user"/> + <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Bulk Description"/> + <column xsi:type="int" name="operation_count" padding="10" unsigned="true" nullable="false" identity="false" + comment="Total number of operations scheduled within this bulk"/> + <column xsi:type="timestamp" name="start_time" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" + comment="Bulk start time"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MAGENTO_BULK_UUID"> + <column name="uuid"/> + </constraint> + <index referenceId="MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID" indexType="btree"> + <column name="user_id"/> + </index> + </table> + <table name="magento_operation" resource="default" engine="innodb" comment="Operation entity"> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Operation ID"/> + <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> + <column xsi:type="varchar" name="topic_name" nullable="true" length="255" + comment="Name of the related message queue topic"/> + <column xsi:type="blob" name="serialized_data" nullable="true" + comment="Data (serialized) required to perform an operation"/> + <column xsi:type="blob" name="result_serialized_data" nullable="true" + comment="Result data (serialized) after perform an operation"/> + <column xsi:type="smallint" name="status" padding="6" unsigned="false" nullable="true" identity="false" + default="0" comment="Operation status (OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED)"/> + <column xsi:type="smallint" name="error_code" padding="6" unsigned="false" nullable="true" identity="false" + comment="Code of the error that appeared during operation execution (used to aggregate related failed operations)"/> + <column xsi:type="varchar" name="result_message" nullable="true" length="255" + comment="Operation result message"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID" table="magento_operation" + column="bulk_uuid" referenceTable="magento_bulk" referenceColumn="uuid" onDelete="CASCADE"/> + <index referenceId="MAGENTO_OPERATION_BULK_UUID_ERROR_CODE" indexType="btree"> + <column name="bulk_uuid"/> + <column name="error_code"/> + </index> + </table> + <table name="magento_acknowledged_bulk" resource="default" engine="innodb" + comment="Bulk that was viewed by user from notification area"> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Internal ID"/> + <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID_MAGENTO_BULK_UUID" + table="magento_acknowledged_bulk" column="bulk_uuid" referenceTable="magento_bulk" + referenceColumn="uuid" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID"> + <column name="bulk_uuid"/> + </constraint> + </table> +</schema> 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..9b6c0709e1916 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json @@ -0,0 +1,52 @@ +{ + "magento_bulk": { + "column": { + "id": true, + "uuid": true, + "user_id": true, + "user_type": true, + "description": true, + "operation_count": true, + "start_time": true + }, + "index": { + "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true, + "MAGENTO_BULK_USER_ID": true + }, + "constraint": { + "PRIMARY": true, + "MAGENTO_BULK_UUID": true, + "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": 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..42b62ff8ea374 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/di.xml @@ -0,0 +1,108 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface" type="Magento\AsynchronousOperations\Model\BulkSummary" /> + <preference for="Magento\AsynchronousOperations\Api\Data\OperationInterface" type="Magento\AsynchronousOperations\Model\Operation" /> + <preference for="Magento\AsynchronousOperations\Api\Data\OperationListInterface" type="Magento\AsynchronousOperations\Model\OperationList" /> + <preference for="Magento\Framework\Bulk\BulkManagementInterface" type="Magento\AsynchronousOperations\Model\BulkManagement" /> + <preference for="Magento\AsynchronousOperations\Api\BulkStatusInterface" type="Magento\AsynchronousOperations\Model\BulkOperationsStatus" /> + <preference for="Magento\Framework\Bulk\BulkStatusInterface" type="Magento\AsynchronousOperations\Model\BulkStatus" /> + <preference for="Magento\Framework\Bulk\OperationManagementInterface" type="Magento\AsynchronousOperations\Model\OperationManagement" /> + <preference for="Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface" type="Magento\AsynchronousOperations\Model\OperationStatus" /> + <preference for="Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterface" type="Magento\AsynchronousOperations\Model\BulkStatus\Detailed" /> + <preference for="Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterface" type="Magento\AsynchronousOperations\Model\BulkStatus\Short" /> + <preference for="Magento\AsynchronousOperations\Api\Data\OperationSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\AsynchronousOperations\Api\OperationRepositoryInterface" type="Magento\AsynchronousOperations\Model\OperationRepository" /> + <type name="Magento\Framework\EntityManager\MetadataPool"> + <arguments> + <argument name="metadata" xsi:type="array"> + <item name="Magento\AsynchronousOperations\Api\Data\OperationInterface" xsi:type="array"> + <item name="entityTableName" xsi:type="string">magento_operation</item> + <item name="identifierField" xsi:type="string">id</item> + </item> + <item name="Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface" xsi:type="array"> + <item name="entityTableName" xsi:type="string">magento_bulk</item> + <item name="identifierField" xsi:type="string">uuid</item> + </item> + <item name="Magento\AsynchronousOperations\Api\Data\OperationListInterface" xsi:type="array"> + <item name="entityTableName" xsi:type="string">magento_operation</item> + <item name="identifierField" xsi:type="string">id</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\EntityManager\Mapper"> + <arguments> + <argument name="config" xsi:type="array"> + <item name="Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface" xsi:type="array"> + <item name="uuid" xsi:type="string">bulk_id</item> + </item> + </argument> + </arguments> + </type> + <virtualType name="bulkSummaryMapper" type="Magento\Framework\EntityManager\CompositeMapper"> + <arguments> + <argument name="mappers" xsi:type="array"> + <item name="identifierMapper" xsi:type="object">Magento\AsynchronousOperations\Model\Entity\BulkSummaryMapper</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Framework\EntityManager\MapperPool"> + <arguments> + <argument name="mappers" xsi:type="array"> + <item name="Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface" xsi:type="string">bulkSummaryMapper</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> + <arguments> + <argument name="collections" xsi:type="array"> + <item name="bulk_listing_data_source" xsi:type="string">Magento\AsynchronousOperations\Ui\Component\DataProvider\SearchResult</item> + <item name="failed_operation_listing_data_source" xsi:type="string">Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Failed\SearchResult</item> + <item name="retriable_operation_listing_data_source" xsi:type="string">Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Retriable\SearchResult</item> + <item name="failed_operation_modal_listing_data_source" xsi:type="string">Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Failed\SearchResult</item> + <item name="retriable_operation_modal_listing_data_source" xsi:type="string">Magento\AsynchronousOperations\Ui\Component\DataProvider\Operation\Retriable\SearchResult</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\EntityManager\OperationPool"> + <arguments> + <argument name="operations" xsi:type="array"> + <item name="Magento\AsynchronousOperations\Api\Data\OperationListInterface" xsi:type="array"> + <item name="checkIfExists" xsi:type="string">Magento\AsynchronousOperations\Model\ResourceModel\Operation\CheckIfExists</item> + <item name="create" xsi:type="string">Magento\AsynchronousOperations\Model\ResourceModel\Operation\Create</item> + </item> + </argument> + </arguments> + </type> + <virtualType + name="Magento\AsynchronousOperations\Ui\Component\DataProvider" + type="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider"/> + <virtualType name="Magento\AsynchronousOperations\Model\VirtualType\PublisherPool" type="Magento\Framework\MessageQueue\PublisherPool"> + <arguments> + <argument name="publishers" xsi:type="array"> + <item name="async" xsi:type="array"> + <item name="amqp" xsi:type="object">Magento\AsynchronousOperations\Model\MassPublisher</item> + <item name="db" xsi:type="object">Magento\AsynchronousOperations\Model\MassPublisher</item> + </item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\AsynchronousOperations\Model\VirtualType\BulkManagement" type="Magento\AsynchronousOperations\Model\BulkManagement"> + <arguments> + <argument name="publisher" xsi:type="object">Magento\AsynchronousOperations\Model\VirtualType\PublisherPool</argument> + </arguments> + </virtualType> + <type name="Magento\AsynchronousOperations\Model\MassSchedule"> + <arguments> + <argument name="bulkManagement" xsi:type="object">Magento\AsynchronousOperations\Model\VirtualType\BulkManagement</argument> + </arguments> + </type> + <preference for="Magento\AsynchronousOperations\Api\Data\AsyncResponseInterface" type="Magento\AsynchronousOperations\Model\AsyncResponse" /> + <preference for="Magento\AsynchronousOperations\Api\Data\ItemStatusInterface" type="Magento\AsynchronousOperations\Model\ItemStatus" /> +</config> diff --git a/app/code/Magento/AsynchronousOperations/etc/extension_attributes.xml b/app/code/Magento/AsynchronousOperations/etc/extension_attributes.xml new file mode 100644 index 0000000000000..6eeda62373f06 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/extension_attributes.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> + <extension_attributes for="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <attribute code="start_time" type="string"> + <resources> + <resource ref="Magento_Logging::system_magento_logging_bulk_operations"/> + </resources> + <join reference_table="magento_bulk" join_on_field="bulk_uuid" reference_field="uuid"> + <field column="start_time">start_time</field> + </join> + </attribute> + </extension_attributes> +</config> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_AsynchronousOperations" > + <sequence> + <module name="Magento_User"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/AsynchronousOperations/etc/webapi.xml b/app/code/Magento/AsynchronousOperations/etc/webapi.xml new file mode 100644 index 0000000000000..4c10a5756c8d6 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/etc/webapi.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> + + <route url="/V1/bulk/:bulkUuid/detailed-status" method="GET"> + <service class="Magento\AsynchronousOperations\Api\BulkStatusInterface" method="getBulkDetailedStatus"/> + <resources> + <resource ref="Magento_Logging::system_magento_logging_bulk_operations" /> + </resources> + </route> + + <route url="/V1/bulk/:bulkUuid/status" method="GET"> + <service class="Magento\AsynchronousOperations\Api\BulkStatusInterface" method="getBulkShortStatus"/> + <resources> + <resource ref="Magento_Logging::system_magento_logging_bulk_operations" /> + </resources> + </route> + + <route url="/V1/bulk/:bulkUuid/operation-status/:status" method="GET"> + <service class="Magento\AsynchronousOperations\Api\BulkStatusInterface" method="getOperationsCountByBulkIdAndStatus"/> + <resources> + <resource ref="Magento_Logging::system_magento_logging_bulk_operations" /> + </resources> + </route> + + <route url="/V1/bulk" method="GET"> + <service class="Magento\AsynchronousOperations\Api\OperationRepositoryInterface" method="getList"/> + <resources> + <resource ref="Magento_Logging::system_magento_logging_bulk_operations" /> + </resources> + </route> + +</routes> 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AsynchronousOperations', __DIR__); diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_bulk_details.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_bulk_details.xml new file mode 100644 index 0000000000000..a250eed51c781 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/layout/bulk_bulk_details.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="formkey"/> + <update handle="styles"/> + <body> + <referenceContainer name="content"> + <uiComponent name="bulk_details_form"/> + </referenceContainer> + </body> +</page> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="formkey"/> + <update handle="styles"/> + <body> + <referenceContainer name="content"> + <uiComponent name="bulk_details_form_modal"/> + </referenceContainer> + </body> +</page> 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 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="formkey"/> + <update handle="styles"/> + <body> + <referenceContainer name="content"> + <uiComponent name="bulk_listing"/> + </referenceContainer> + </body> +</page> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">bulk_details_form.bulk_details_form_data_source</item> + </item> + <item name="template" xsi:type="string">templates/form/collapsible</item> + </argument> + <settings> + <buttons> + <button name="save" class="Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\RetryButton"/> + <button name="back" class="Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\BackButton"/> + </buttons> + <namespace>bulk_details_form</namespace> + <dataScope>data</dataScope> + <deps> + <dep>bulk_details_form.bulk_details_form_data_source</dep> + </deps> + </settings> + <dataSource name="bulk_details_form_data_source"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item> + </item> + </argument> + <settings> + <submitUrl path="bulk/bulk/retry"/> + </settings> + <dataProvider class="Magento\AsynchronousOperations\Ui\Component\Operation\DataProvider" name="bulk_details_form_data_source"> + <settings> + <requestFieldName>uuid</requestFieldName> + <primaryFieldName>uuid</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <fieldset name="general" sortOrder="10"> + <settings> + <label/> + </settings> + <field name="description" formElement="input"> + <settings> + <elementTmpl>ui/form/element/text</elementTmpl> + <dataType>text</dataType> + <label translate="true">Description of Operation</label> + </settings> + </field> + <field name="summary" formElement="input"> + <settings> + <elementTmpl>ui/form/element/text</elementTmpl> + <dataType>text</dataType> + <label translate="true">Summary</label> + </settings> + </field> + <field name="start_time" formElement="date"> + <settings> + <elementTmpl>ui/form/element/textDate</elementTmpl> + <dataType>text</dataType> + <label translate="true">Start Time</label> + </settings> + <formElements> + <date> + <settings> + <options> + <option name="showsTime" xsi:type="boolean">true</option> + <option name="dateFormat" xsi:type="string">MMM d, YYYY</option> + <option name="timeFormat" xsi:type="string">h:mm:ss A</option> + </options> + </settings> + </date> + </formElements> + </field> + </fieldset> + <fieldset name="retriable_operations" sortOrder="20"> + <settings> + <label translate="true">Items to Retry</label> + </settings> + <container name="retriable_operations_description" template="ui/form/components/complex"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="label" xsi:type="string"/> + <item name="text" xsi:type="string" translate="true">To retry, select the items and click “Retry”.</item> + </item> + </argument> + </container> + <field name="retriable_operation_validation" component="Magento_AsynchronousOperations/js/form/error" template="Magento_AsynchronousOperations/form/field" formElement="input"> + <settings> + <additionalClasses> + <class name="message message-warning">true</class> + </additionalClasses> + <validation> + <rule name="required-entry" xsi:type="array"> + <item name="validate" xsi:type="boolean">true</item> + <item name="message" xsi:type="string">An item needs to be selected. Select and try again.</item> + </rule> + </validation> + <elementTmpl/> + <dataType>text</dataType> + <dataScope>operations_to_retry</dataScope> + </settings> + </field> + <insertListing name="retriable_operation"> + <settings> + <externalProvider>${ $.ns }.retriable_operation_listing_data_source</externalProvider> + <loading>false</loading> + <selectionsProvider>${ $.ns }.${ $.ns }.retriable_operation_listing_columns.ids</selectionsProvider> + <autoRender>true</autoRender> + <dataScope>operations_to_retry</dataScope> + <ns>retriable_operation_listing</ns> + <exports> + <link name="uuid">${ $.externalProvider }:params.uuid</link> + </exports> + <imports> + <link name="uuid">${ $.provider }:data.uuid</link> + </imports> + </settings> + </insertListing> + </fieldset> + <fieldset name="failed_operations" sortOrder="30"> + <settings> + <label translate="true">Items That Can’t Be Updated.</label> + </settings> + <container name="failed_operations_description" template="ui/form/components/complex"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="label" xsi:type="string"/> + </item> + </argument> + </container> + <insertListing name="failed_operation"> + <settings> + <externalProvider>failed_operation_listing.failed_operation_listing_data_source</externalProvider> + <loading>false</loading> + <autoRender>true</autoRender> + <ns>failed_operation_listing</ns> + <exports> + <link name="uuid">${ $.externalProvider }:params.uuid</link> + </exports> + <imports> + <link name="uuid">${ $.provider }:data.uuid</link> + </imports> + </settings> + </insertListing> + </fieldset> +</form> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_details_form_modal.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_details_form_modal.xml new file mode 100644 index 0000000000000..a7d4ad03a0adf --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_details_form_modal.xml @@ -0,0 +1,146 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">bulk_details_form_modal.bulk_details_form_modal_data_source</item> + </item> + <item name="template" xsi:type="string">templates/form/collapsible</item> + </argument> + <settings> + <buttons> + <button name="save" class="Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\RetryButton"/> + <button name="done" class="Magento\AsynchronousOperations\Block\Adminhtml\Bulk\Details\DoneButton"/> + </buttons> + <namespace>bulk_details_form_modal</namespace> + <ajaxSave>true</ajaxSave> + <ajaxSaveType>simple</ajaxSaveType> + <dataScope>data</dataScope> + <deps> + <dep>bulk_details_form_modal.bulk_details_form_modal_data_source</dep> + </deps> + </settings> + <dataSource name="bulk_details_form_modal_data_source"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item> + </item> + </argument> + <settings> + <submitUrl path="bulk/bulk/retry"/> + </settings> + <dataProvider class="Magento\AsynchronousOperations\Ui\Component\Operation\DataProvider" name="bulk_details_form_modal_data_source"> + <settings> + <requestFieldName>uuid</requestFieldName> + <primaryFieldName>uuid</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <fieldset name="general" sortOrder="10"> + <settings> + <label/> + </settings> + <field name="description" formElement="input"> + <settings> + <elementTmpl>ui/form/element/text</elementTmpl> + <dataType>text</dataType> + <label translate="true">Description of Operation</label> + </settings> + </field> + <field name="summary" formElement="input"> + <settings> + <elementTmpl>ui/form/element/text</elementTmpl> + <dataType>text</dataType> + <label translate="true">Summary</label> + </settings> + </field> + <field name="start_time" formElement="date"> + <settings> + <elementTmpl>ui/form/element/textDate</elementTmpl> + <dataType>text</dataType> + <label translate="true">Start Time</label> + </settings> + <formElements> + <date> + <settings> + <options> + <option name="timeFormat" xsi:type="string">h:mm:ss A</option> + <option name="dateFormat" xsi:type="string">MMM d, YYYY</option> + <option name="showsTime" xsi:type="boolean">true</option> + </options> + </settings> + </date> + </formElements> + </field> + </fieldset> + <fieldset name="retriable_operations" sortOrder="20"> + <settings> + <label translate="true">Items to Retry</label> + </settings> + <container name="retriable_operations_description" template="ui/form/components/complex"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="label" xsi:type="string"/> + <item name="text" xsi:type="string" translate="true">To retry, select the items and click “Retry”.</item> + </item> + </argument> + </container> + <field name="retriable_operation_validation" component="Magento_AsynchronousOperations/js/form/error" template="Magento_AsynchronousOperations/form/field" formElement="input"> + <settings> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + <elementTmpl/> + <dataType>text</dataType> + <dataScope>operations_to_retry</dataScope> + </settings> + </field> + <insertListing name="retriable_operation"> + <settings> + <externalProvider>${ $.ns }.retriable_operation_modal_listing_data_source</externalProvider> + <loading>false</loading> + <selectionsProvider>${ $.ns }.${ $.ns }.retriable_operation_modal_listing_columns.ids</selectionsProvider> + <autoRender>true</autoRender> + <dataScope>operations_to_retry</dataScope> + <ns>retriable_operation_modal_listing</ns> + <exports> + <link name="uuid">${ $.externalProvider }:params.uuid</link> + </exports> + <imports> + <link name="uuid">${ $.provider }:data.uuid</link> + </imports> + </settings> + </insertListing> + </fieldset> + <fieldset name="failed_operations" sortOrder="30"> + <settings> + <label translate="true">Items That Can’t Be Updated.</label> + </settings> + <container name="failed_operations_description" template="ui/form/components/complex"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="label" xsi:type="string"/> + </item> + </argument> + </container> + <insertListing name="failed_operation"> + <settings> + <externalProvider>failed_operation_modal_listing.failed_operation_modal_listing_data_source</externalProvider> + <loading>false</loading> + <autoRender>true</autoRender> + <ns>failed_operation_modal_listing</ns> + <exports> + <link name="uuid">${ $.externalProvider }:params.uuid</link> + </exports> + <imports> + <link name="uuid">${ $.provider }:data.uuid</link> + </imports> + </settings> + </insertListing> + </fieldset> +</form> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml new file mode 100644 index 0000000000000..512b08d6a8de2 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">bulk_listing.bulk_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>bulk_columns</spinner> + <deps> + <dep>bulk_listing.bulk_listing_data_source</dep> + </deps> + </settings> + <dataSource name="bulk_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Logging::system_magento_logging_bulk_operations</aclResource> + <dataProvider class="Magento\AsynchronousOperations\Ui\Component\DataProvider" name="bulk_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <bookmark name="bookmarks"/> + <columnsControls name="columns_controls"/> + <exportButton name="export_button"/> + <filters name="listing_filters"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="params" xsi:type="array"> + <item name="filters_modifier" xsi:type="array"/> + </item> + <item name="observers" xsi:type="array"/> + </item> + </argument> + <settings> + <statefull> + <property name="applied" xsi:type="boolean">false</property> + </statefull> + </settings> + </filters> + <paging name="listing_paging"/> + </listingToolbar> + <columns name="bulk_columns"> + <selectionsColumn name="ids" sortOrder="10"> + <settings> + <indexField>uuid</indexField> + <visible>false</visible> + </settings> + </selectionsColumn> + <column name="uuid" sortOrder="20"> + <settings> + <filter>text</filter> + <label translate="true">ID</label> + </settings> + </column> + <column name="start_time" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date" sortOrder="40"> + <settings> + <filter>dateRange</filter> + <dataType>date</dataType> + <label translate="true">Start Time</label> + </settings> + </column> + <column name="description" sortOrder="50"> + <settings> + <filter>select</filter> + <options class="Magento\AsynchronousOperations\Model\BulkDescription\Options"/> + <dataType>select</dataType> + <label translate="true">Description of Operation</label> + </settings> + </column> + <column name="status" component="Magento_Ui/js/grid/columns/select" sortOrder="60"> + <settings> + <filter>select</filter> + <options class="Magento\AsynchronousOperations\Model\BulkStatus\Options"/> + <dataType>select</dataType> + <label translate="true">Status</label> + </settings> + </column> + <actionsColumn name="actions" class="\Magento\AsynchronousOperations\Ui\Component\Listing\Column\Actions"> + <settings> + <indexField>id</indexField> + <label>Action</label> + </settings> + </actionsColumn> + </columns> +</listing> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/failed_operation_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/failed_operation_listing.xml new file mode 100644 index 0000000000000..2ac762e398521 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/failed_operation_listing.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">failed_operation_listing.failed_operation_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>failed_operation_listing_columns</spinner> + <deps> + <dep>failed_operation_listing.failed_operation_listing_data_source</dep> + </deps> + </settings> + <dataSource name="failed_operation_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Logging::system_magento_logging_bulk_operations</aclResource> + <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="failed_operation_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <paging name="listing_paging"/> + <exportButton name="export_button"> + <settings> + <additionalParams> + <param xsi:type="string" name="uuid">${ $.provider}:params.uuid</param> + </additionalParams> + </settings> + </exportButton> + </listingToolbar> + <columns name="failed_operation_listing_columns"> + <settings> + <dndConfig> + <param name="enabled" xsi:type="boolean">false</param> + </dndConfig> + </settings> + <selectionsColumn name="ids"> + <settings> + <indexField>id</indexField> + <visible>false</visible> + </settings> + </selectionsColumn> + <column name="id" component="Magento_Ui/js/grid/columns/link" sortOrder="10"> + <settings> + <label translate="true">ID</label> + <sortable>false</sortable> + </settings> + </column> + <column name="meta_information" sortOrder="20"> + <settings> + <label translate="true">Meta Information</label> + <sortable>false</sortable> + </settings> + </column> + <column name="result_message" sortOrder="30"> + <settings> + <label translate="true">Error</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/failed_operation_modal_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/failed_operation_modal_listing.xml new file mode 100644 index 0000000000000..62a4935da8ba7 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/failed_operation_modal_listing.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">failed_operation_modal_listing.failed_operation_modal_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>failed_operation_modal_listing_columns</spinner> + <deps> + <dep>failed_operation_modal_listing.failed_operation_modal_listing_data_source</dep> + </deps> + </settings> + <dataSource name="failed_operation_modal_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Logging::system_magento_logging_bulk_operations</aclResource> + <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="failed_operation_modal_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <paging name="listing_paging"/> + <exportButton name="export_button"> + <settings> + <additionalParams> + <param xsi:type="string" name="uuid">${ $.provider}:params.uuid</param> + </additionalParams> + </settings> + </exportButton> + </listingToolbar> + <columns name="failed_operation_modal_listing_columns"> + <settings> + <dndConfig> + <param name="enabled" xsi:type="boolean">false</param> + </dndConfig> + </settings> + <selectionsColumn name="ids"> + <settings> + <indexField>id</indexField> + <visible>false</visible> + </settings> + </selectionsColumn> + <column name="id" component="Magento_Ui/js/grid/columns/link" sortOrder="10"> + <settings> + <label translate="true">ID</label> + <sortable>false</sortable> + </settings> + </column> + <column name="meta_information" sortOrder="20"> + <settings> + <label translate="true">Meta Information</label> + <sortable>false</sortable> + </settings> + </column> + <column name="result_message" sortOrder="30"> + <settings> + <label translate="true">Error</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/notification_area.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/notification_area.xml new file mode 100644 index 0000000000000..b43a7e6f4c358 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/notification_area.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <columns name="columns" component="Magento_AsynchronousOperations/js/grid/listing" template="Magento_AsynchronousOperations/grid/listing"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="link" xsi:type="url" path="bulk/index"/> + <item name="linkText" xsi:type="string" translate="true">Bulk Actions Log</item> + <item name="dismissAllText" xsi:type="string" translate="true">Dismiss All Completed Tasks</item> + <item name="dismissUrl" xsi:type="url" path="bulk/notification/dismiss"/> + </item> + </argument> + <actionsColumn name="actions" class="Magento\AsynchronousOperations\Ui\Component\Listing\Column\NotificationActions" sortOrder="20"> + <settings> + <indexField>identity</indexField> + </settings> + </actionsColumn> + <actionsColumn name="dismiss" class="Magento\AsynchronousOperations\Ui\Component\Listing\Column\NotificationDismissActions" sortOrder="10"> + <settings> + <indexField>identity</indexField> + <bodyTmpl>Magento_AsynchronousOperations/grid/cells/actions</bodyTmpl> + </settings> + </actionsColumn> + </columns> + <container name="modalContainer"> + <modal name="modal"> + <insertForm name="insertBulk" component="Magento_AsynchronousOperations/js/insert-form"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="titlePrefix" xsi:type="string" translate="true">Action Details - #</item> + <item name="modalTitleProvider" xsi:type="string">${ $.externalProvider }:data.uuid</item> + </item> + </argument> + <settings> + <formSubmitType>ajax</formSubmitType> + <columnsProvider>ns = notification_area, index = columns</columnsProvider> + <renderUrl path="mui/index/render_handle"> + <param name="handle">bulk_bulk_details_modal</param> + <param name="buttons">1</param> + </renderUrl> + <loading>false</loading> + <toolbarContainer>${ $.parentName }</toolbarContainer> + <externalProvider>${ $.ns }.bulk_details_form_modal_data_source</externalProvider> + <ns>bulk_details_form_modal</ns> + </settings> + </insertForm> + </modal> + </container> +</listing> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/retriable_operation_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/retriable_operation_listing.xml new file mode 100644 index 0000000000000..3618e10ee77d8 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/retriable_operation_listing.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">retriable_operation_listing.retriable_operation_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>retriable_operation_listing_columns</spinner> + <deps> + <dep>retriable_operation_listing.retriable_operation_listing_data_source</dep> + </deps> + </settings> + <dataSource name="retriable_operation_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Logging::system_magento_logging_bulk_operations</aclResource> + <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="retriable_operation_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <paging name="listing_paging"/> + </listingToolbar> + <columns name="retriable_operation_listing_columns"> + <settings> + <dndConfig> + <param name="enabled" xsi:type="boolean">false</param> + </dndConfig> + </settings> + <selectionsColumn name="ids" sortOrder="10"> + <settings> + <indexField>error_code</indexField> + </settings> + </selectionsColumn> + <column name="result_message" sortOrder="20"> + <settings> + <label translate="true">Error</label> + </settings> + </column> + <column name="records_qty" sortOrder="30"> + <settings> + <label translate="true">Number of Records Affected</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/retriable_operation_modal_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/retriable_operation_modal_listing.xml new file mode 100644 index 0000000000000..97e3e897c2533 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/retriable_operation_modal_listing.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">retriable_operation_modal_listing.retriable_operation_modal_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>retriable_operation_modal_listing_columns</spinner> + <deps> + <dep>retriable_operation_modal_listing.retriable_operation_modal_listing_data_source</dep> + </deps> + </settings> + <dataSource name="retriable_operation_modal_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Logging::system_magento_logging_bulk_operations</aclResource> + <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="retriable_operation_modal_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <paging name="listing_paging"/> + </listingToolbar> + <columns name="retriable_operation_modal_listing_columns"> + <settings> + <dndConfig> + <param name="enabled" xsi:type="boolean">false</param> + </dndConfig> + </settings> + <selectionsColumn name="ids" sortOrder="10"> + <settings> + <indexField>error_code</indexField> + </settings> + </selectionsColumn> + <column name="result_message" sortOrder="20"> + <settings> + <label translate="true">Error</label> + </settings> + </column> + <column name="records_qty" sortOrder="30"> + <settings> + <label translate="true">Number of Records Affected</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/form/error.js b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/form/error.js new file mode 100644 index 0000000000000..bc41946657df1 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/form/error.js @@ -0,0 +1,17 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/abstract' +], function (Abstract) { + 'use strict'; + + return Abstract.extend({ + /** @inheritdoc */ + onUpdate: function () { + this.bubble('update', this.hasChanged()); + } + }); +}); diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/grid/listing.js b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/grid/listing.js new file mode 100644 index 0000000000000..104ac0ae1ec10 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/grid/listing.js @@ -0,0 +1,88 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_AdminNotification/js/grid/listing', + 'Magento_Ui/js/modal/alert', + 'mage/translate', + 'underscore', + 'jquery' +], function (Listing, uiAlert, $t, _, $) { + 'use strict'; + + return Listing.extend({ + defaults: { + isAllowed: true, + ajaxSettings: { + method: 'POST', + data: {}, + url: '${ $.dismissUrl }' + } + }, + + /** @inheritdoc */ + initialize: function () { + _.bindAll(this, 'reload', 'onError'); + + return this._super(); + }, + + /** + * Dismiss all items. + */ + dismissAll: function () { + var toDismiss = []; + + _.each(this.rows, function (row) { + if (row.dismiss) { + toDismiss.push(row.uuid); + } + }); + toDismiss.length && this.dismiss(toDismiss); + }, + + /** + * Dismiss action. + * + * @param {Array} items + */ + dismiss: function (items) { + var config = _.extend({}, this.ajaxSettings); + + config.data.uuid = items; + this.showLoader(); + + $.ajax(config) + .done(this.reload) + .fail(this.onError); + }, + + /** + * Success callback for dismiss request. + */ + reload: function () { + this.source.reload({ + refresh: true + }); + }, + + /** + * Error callback for dismiss request. + * + * @param {Object} xhr + */ + onError: function (xhr) { + this.hideLoader(); + + if (xhr.statusText === 'abort') { + return; + } + + uiAlert({ + content: $t('Something went wrong.') + }); + } + }); +}); diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/insert-form.js b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/insert-form.js new file mode 100644 index 0000000000000..5c39534ea5e9a --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/js/insert-form.js @@ -0,0 +1,65 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/components/insert-form', + 'uiRegistry' +], function (Insert, registry) { + 'use strict'; + + return Insert.extend({ + defaults: { + modalProvider: '${ $.parentName }', + titlePrefix: '', + imports: { + changeModalTitle: '${ $.modalProvider }:state' + }, + listens: { + responseData: 'afterRetry' + }, + modules: { + modal: '${ $.modalProvider }', + notificationListing: '${ $.columnsProvider }' + } + }, + + /** @inheritdoc */ + initConfig: function () { + var modalTitleProvider; + + this._super(); + modalTitleProvider = this.modalTitleProvider.split(':'); + this.modalTitleTarget = modalTitleProvider[0]; + this.modalTitlePath = modalTitleProvider[1]; + }, + + /** + * Change modal title. + * + * @param {Boolean} change + */ + changeModalTitle: function (change) { + if (change) { + registry.get(this.modalTitleTarget, function (target) { + this.modal().setTitle(this.titlePrefix + target.get(this.modalTitlePath)); + }.bind(this)); + } else { + this.modal().setTitle(''); + } + }, + + /** + * Action after retry operation. + * + * @param {Object} data + */ + afterRetry: function (data) { + if (!data.error) { + this.modal().closeModal(); + this.notificationListing().reload(); + } + } + }); +}); diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/form/field.html b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/form/field.html new file mode 100644 index 0000000000000..6cff5e3ca9732 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/form/field.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div css="$data.additionalClasses" + if="error" + text="error"/> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/grid/cells/actions.html b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/grid/cells/actions.html new file mode 100644 index 0000000000000..17144a22cd32c --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/grid/cells/actions.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="action-menu-item action-close-wrapper"> + <button class="action-close" + repeat="foreach: $col.getVisibleActions($row()._rowIndex), item: '$action'" + click="$col.getActionHandler($action())" + attr="{ + title: $action().label + }" + /> +</div> diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/grid/listing.html b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/grid/listing.html new file mode 100644 index 0000000000000..e6559a6ede37c --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/web/template/grid/listing.html @@ -0,0 +1,43 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div id="system_messages" class="message-system" collapsible visible="totalRecords"> + <div class="message-system-inner" outerClick="fixLoaderHeight.bind($data, true)"> + <div class="message-system-short"> + <button class="message-system-action-dropdown" toggleCollapsible> + <span> + <translate args="'System Messages'"/>: + <text args="totalRecords"/> + </span> + </button> + <div class="message-system-short-wrapper" if="rows[0]" repeat="foreach: [rows[0]], item: '$row'" visible="!$collapsible.opened()"> + <fastForEach args="data: getVisible(), as: '$col'" > + <render args="$col.getBody()"/> + </fastForEach> + </div> + </div> + <div class="message-system-collapsible"> + <ul class="message-system-list"> + <li repeat="foreach: rows, item: '$row'"> + <fastForEach args="data: getVisible(), as: '$col'" > + <render args="$col.getBody()"/> + </fastForEach> + </li> + </ul> + <div class="message-system-summary" if="isAllowed"> + <a class="action__message-log" + href="#" + click="dismissAll" + text="dismissAllText"/> + <a class="action__message-log" + attr="{ + href: link + }" + text="linkText"/> + </div> + </div> + </div> +</div> 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 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Authorization\Setup; - -use Magento\Framework\Setup\InstallDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; -use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Authorization\Model\Acl\Role\Group as RoleGroup; -use Magento\Authorization\Model\UserContextInterface; - -/** - * @codeCoverageIgnore - */ -class InstallData implements InstallDataInterface -{ - /** - * Authorization factory - * - * @var AuthorizationFactory - */ - private $authFactory; - - /** - * Init - * - * @param AuthorizationFactory $authFactory - */ - public function __construct(AuthorizationFactory $authFactory) - { - $this->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 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Authorization\Setup; - -use Magento\Framework\Setup\InstallSchemaInterface; -use Magento\Framework\Setup\ModuleContextInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * @codeCoverageIgnore - */ -class InstallSchema implements InstallSchemaInterface -{ - /** - * {@inheritdoc} - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) - { - $installer = $setup; - - $installer->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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Authorization\Setup\Patch\Data; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Magento\Authorization\Model\Acl\Role\Group as RoleGroup; +use Magento\Authorization\Model\UserContextInterface; + +/** + * Class InitializeAuthRoles + * @package Magento\Authorization\Setup\Patch + */ +class InitializeAuthRoles implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Authorization\Setup\AuthorizationFactory + */ + private $authFactory; + + /** + * InitializeAuthRoles constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Authorization\Setup\AuthorizationFactory $authorizationFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Authorization\Setup\AuthorizationFactory $authorizationFactory + ) { + $this->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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Braintree/LICENSE.txt b/app/code/Magento/Authorization/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Braintree/LICENSE.txt rename to app/code/Magento/Authorization/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backup/LICENSE_AFL.txt b/app/code/Magento/Authorization/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Backup/LICENSE_AFL.txt rename to app/code/Magento/Authorization/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Authorization/Test/Mftf/README.md b/app/code/Magento/Authorization/Test/Mftf/README.md new file mode 100644 index 0000000000000..1d44ab2e73052 --- /dev/null +++ b/app/code/Magento/Authorization/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Authorization Functional Tests + +The Functional Test Module for **Magento Authorization** module. 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..cd51c0f9bc4b8 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() { @@ -78,6 +78,9 @@ public function testGetAllowedResourcesByUser() ); } + /** + * @return AclRetriever + */ protected function createAclRetriever() { $this->roleMock = $this->createPartialMock(\Magento\Authorization\Model\Role::class, ['getId', '__wakeup']); 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..a38828eb6efca --- /dev/null +++ b/app/code/Magento/Authorization/etc/db_schema.xml @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="authorization_role" resource="default" engine="innodb" comment="Admin Role Table"> + <column xsi:type="int" name="role_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Role ID"/> + <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" + default="0" comment="Parent Role ID"/> + <column xsi:type="smallint" name="tree_level" padding="5" unsigned="true" nullable="false" identity="false" + default="0" comment="Role Tree Level"/> + <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" + default="0" comment="Role Sort Order"/> + <column xsi:type="varchar" name="role_type" nullable="false" length="1" default="0" comment="Role Type"/> + <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" + comment="User ID"/> + <column xsi:type="varchar" name="user_type" nullable="true" length="16" comment="User Type"/> + <column xsi:type="varchar" name="role_name" nullable="true" length="50" comment="Role Name"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="role_id"/> + </constraint> + <index referenceId="AUTHORIZATION_ROLE_PARENT_ID_SORT_ORDER" indexType="btree"> + <column name="parent_id"/> + <column name="sort_order"/> + </index> + <index referenceId="AUTHORIZATION_ROLE_TREE_LEVEL" indexType="btree"> + <column name="tree_level"/> + </index> + </table> + <table name="authorization_rule" resource="default" engine="innodb" comment="Admin Rule Table"> + <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Rule ID"/> + <column xsi:type="int" name="role_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" + comment="Role ID"/> + <column xsi:type="varchar" name="resource_id" nullable="true" length="255" comment="Resource ID"/> + <column xsi:type="varchar" name="privileges" nullable="true" length="20" comment="Privileges"/> + <column xsi:type="varchar" name="permission" nullable="true" length="10" comment="Permission"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="rule_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="AUTHORIZATION_RULE_ROLE_ID_AUTHORIZATION_ROLE_ROLE_ID" + table="authorization_rule" column="role_id" referenceTable="authorization_role" + referenceColumn="role_id" onDelete="CASCADE"/> + <index referenceId="AUTHORIZATION_RULE_RESOURCE_ID_ROLE_ID" indexType="btree"> + <column name="resource_id"/> + <column name="role_id"/> + </index> + <index referenceId="AUTHORIZATION_RULE_ROLE_ID_RESOURCE_ID" indexType="btree"> + <column name="role_id"/> + <column name="resource_id"/> + </index> + </table> +</schema> 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..8c416d2a8b42c --- /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 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Authorization" setup_version="2.0.0"> + <module name="Magento_Authorization" > <sequence> <module name="Magento_Backend"/> </sequence> diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php new file mode 100644 index 0000000000000..fb9c74d2f0ab1 --- /dev/null +++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Authorizenet\Block\Adminhtml\Order\View\Info; + +use Magento\Framework\Phrase; +use Magento\Payment\Block\ConfigurableInfo; + +/** + * Payment information block for Authorize.net payment method + */ +class PaymentDetails extends ConfigurableInfo +{ + /** + * Returns localized label for payment info block + * + * @param string $field + * @return string | Phrase + */ + protected function getLabel($field) + { + return __($field); + } +} diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php index 3ad9f470909bf..70565ea8ac65f 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php @@ -10,12 +10,15 @@ use Magento\Authorizenet\Model\Directpost; use Magento\Authorizenet\Model\DirectpostFactory; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; use Psr\Log\LoggerInterface; -class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment +class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment implements CsrfAwareActionInterface { /** * @var LoggerInterface @@ -48,6 +51,23 @@ public function __construct( $this->logger = $logger ?: $this->_objectManager->get(LoggerInterface::class); } + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } + /** * Response action. * Action for Authorize.net SIM Relay Request. diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php index bdd0c4a424e99..3c1cb90e0c0a5 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Authorizenet\Controller\Directpost\Payment; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Authorizenet\Helper\DataFactory; use Magento\Checkout\Model\Type\Onepage; @@ -24,7 +26,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Place extends Payment +class Place extends Payment implements HttpPostActionInterface { /** * @var \Magento\Quote\Api\CartManagementInterface @@ -122,7 +124,7 @@ public function execute() /** * Place order for checkout flow * - * @return string + * @return void */ protected function placeCheckoutOrder() { @@ -147,7 +149,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/Controller/Directpost/Payment/Response.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php index d88e77d6c4e28..d562df9fb24a9 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php @@ -6,8 +6,29 @@ */ namespace Magento\Authorizenet\Controller\Directpost\Payment; -class Response extends \Magento\Authorizenet\Controller\Directpost\Payment +use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\RequestInterface; + +class Response extends \Magento\Authorizenet\Controller\Directpost\Payment implements CsrfAwareActionInterface { + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } + /** * Response action. * Action for Authorize.net SIM Relay Request. diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index 0f10fd633cb5b..aeaa0cd9a3ad1 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -5,10 +5,9 @@ */ namespace Magento\Authorizenet\Model; -use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\App\ObjectManager; use Magento\Payment\Model\Method\ConfigInterface; use Magento\Payment\Model\Method\TransparentInterface; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; /** * Authorize.net DirectPost payment method model. @@ -28,7 +27,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra /** * @var string */ - protected $_infoBlockType = \Magento\Payment\Block\Info::class; + protected $_infoBlockType = \Magento\Authorizenet\Block\Adminhtml\Order\View\Info\PaymentDetails::class; /** * Payment Method feature @@ -102,7 +101,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra protected $response; /** - * @var OrderSender + * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender */ protected $orderSender; @@ -123,6 +122,16 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra */ private $psrLogger; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + + /** + * @var \Magento\Sales\Model\Order + */ + private $order; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -134,18 +143,19 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra * @param \Magento\Framework\Module\ModuleListInterface $moduleList * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Authorizenet\Helper\Data $dataHelper - * @param Directpost\Request\Factory $requestFactory - * @param Directpost\Response\Factory $responseFactory + * @param \Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory + * @param \Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory * @param \Magento\Authorizenet\Model\TransactionService $transactionService * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository - * @param OrderSender $orderSender + * @param \Magento\Sales\Model\Order\Email\Sender\OrderSender $orderSender * @param \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -161,8 +171,8 @@ public function __construct( \Magento\Authorizenet\Helper\Data $dataHelper, \Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory, \Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory, - TransactionService $transactionService, - ZendClientFactory $httpClientFactory, + \Magento\Authorizenet\Model\TransactionService $transactionService, + \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory, \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Quote\Api\CartRepositoryInterface $quoteRepository, @@ -170,7 +180,8 @@ public function __construct( \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { $this->orderFactory = $orderFactory; $this->storeManager = $storeManager; @@ -179,6 +190,8 @@ public function __construct( $this->orderSender = $orderSender; $this->transactionRepository = $transactionRepository; $this->_code = static::METHOD_CODE; + $this->paymentFailures = $paymentFailures ? : ObjectManager::getInstance() + ->get(\Magento\Sales\Api\PaymentFailuresInterface::class); parent::__construct( $context, @@ -358,8 +371,7 @@ public function void(\Magento\Payment\Model\InfoInterface $payment) } /** - * Refund the amount - * Need to decode last 4 digits for request. + * Refund the amount need to decode last 4 digits for request. * * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment * @param float $amount @@ -561,13 +573,10 @@ public function process(array $responseData) $this->validateResponse(); $response = $this->getResponse(); - //operate with order - $orderIncrementId = $response->getXInvoiceNum(); $responseText = $this->dataHelper->wrapGatewayError($response->getXResponseReasonText()); $isError = false; - if ($orderIncrementId) { - /* @var $order \Magento\Sales\Model\Order */ - $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + if ($this->getOrderIncrementId()) { + $order = $this->getOrderFromResponse(); //check payment method $payment = $order->getPayment(); if (!$payment || $payment->getMethod() != $this->getCode()) { @@ -616,6 +625,14 @@ protected function fillPaymentByResponse(\Magento\Framework\DataObject $payment) $payment->setIsTransactionPending(true) ->setIsFraudDetected(true); } + + $additionalInformationKeys = explode(',', $this->getValue('paymentInfoKeys')); + foreach ($additionalInformationKeys as $paymentInfoKey) { + $paymentInfoValue = $response->getDataByKey($paymentInfoKey); + if ($paymentInfoValue !== null) { + $payment->setAdditionalInformation($paymentInfoKey, $paymentInfoValue); + } + } } /** @@ -632,9 +649,10 @@ public function checkResponseCode() return true; case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: - throw new \Magento\Framework\Exception\LocalizedException( - $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()) - ); + $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); + $order = $this->getOrderFromResponse(); + $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage); + throw new \Magento\Framework\Exception\LocalizedException($errorMessage); default: throw new \Magento\Framework\Exception\LocalizedException( __('There was a payment authorization error.') @@ -671,6 +689,7 @@ protected function matchAmount($amount) /** * Operate with order using information from Authorize.net. + * * Authorize order or authorize and capture it. * * @param \Magento\Sales\Model\Order $order @@ -688,6 +707,7 @@ protected function processOrder(\Magento\Sales\Model\Order $order) //decline the order (in case of wrong response code) but don't return money to customer. $message = $e->getMessage(); $this->declineOrder($order, $message, false); + throw $e; } @@ -758,7 +778,7 @@ protected function processPaymentFraudStatus(\Magento\Sales\Model\Order\Payment } /** - * Add status comment + * Add status comment to history * * @param \Magento\Sales\Model\Order\Payment $payment * @return $this @@ -803,12 +823,17 @@ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = '' { try { $response = $this->getResponse(); - if ($voidPayment && $response->getXTransId() && strtoupper($response->getXType()) - == self::REQUEST_TYPE_AUTH_ONLY + if ($voidPayment + && $response->getXTransId() + && strtoupper($response->getXType()) == self::REQUEST_TYPE_AUTH_ONLY ) { - $order->getPayment()->setTransactionId(null)->setParentTransactionId($response->getXTransId())->void(); + $order->getPayment() + ->setTransactionId(null) + ->setParentTransactionId($response->getXTransId()) + ->void($response); } $order->registerCancellation($message)->save(); + $this->_eventManager->dispatch('order_cancel_after', ['order' => $order ]); } catch (\Exception $e) { //quiet decline $this->getPsrLogger()->critical($e); @@ -843,7 +868,7 @@ public function getConfigInterface() * Getter for specified value according to set payment method code * * @param mixed $key - * @param null $storeId + * @param mixed $storeId * @return mixed */ public function getValue($key, $storeId = null) @@ -903,10 +928,12 @@ public function fetchTransactionInfo(\Magento\Payment\Model\InfoInterface $payme $payment->setIsTransactionDenied(true); } $this->addStatusCommentOnUpdate($payment, $response, $transactionId); - return []; + return $response->getData(); } /** + * Add status comment on update + * * @param \Magento\Sales\Model\Order\Payment $payment * @param \Magento\Framework\DataObject $response * @param string $transactionId @@ -981,21 +1008,52 @@ protected function getTransactionResponse($transactionId) } /** - * @return \Psr\Log\LoggerInterface + * Get psr logger. * + * @return \Psr\Log\LoggerInterface * @deprecated 100.1.0 */ private function getPsrLogger() { if (null === $this->psrLogger) { - $this->psrLogger = \Magento\Framework\App\ObjectManager::getInstance() + $this->psrLogger = ObjectManager::getInstance() ->get(\Psr\Log\LoggerInterface::class); } return $this->psrLogger; } /** - * Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal, + * Fetch order by increment id from response. + * + * @return \Magento\Sales\Model\Order + */ + private function getOrderFromResponse(): \Magento\Sales\Model\Order + { + if (!$this->order) { + $this->order = $this->orderFactory->create(); + + if ($incrementId = $this->getOrderIncrementId()) { + $this->order = $this->order->loadByIncrementId($incrementId); + } + } + + return $this->order; + } + + /** + * Fetch order increment id from response. + * + * @return string + */ + private function getOrderIncrementId(): string + { + return $this->getResponse()->getXInvoiceNum(); + } + + /** + * Checks if filter action is Report Only. + * + * Transactions that trigger this filter are processed as normal, * but are also reported in the Merchant Interface as triggering this filter. * * @param string $fdsFilterAction diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php index d9a403e5c991e..fc78d836b6080 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php @@ -112,50 +112,50 @@ public function setDataFromOrder( sprintf('%.2F', $order->getBaseShippingAmount()) ); - //need to use strval() because NULL values IE6-8 decodes as "null" in JSON in JavaScript, + //need to use (string) because NULL values IE6-8 decodes as "null" in JSON in JavaScript, //but we need "" for null values. $billing = $order->getBillingAddress(); if (!empty($billing)) { - $this->setXFirstName(strval($billing->getFirstname())) - ->setXLastName(strval($billing->getLastname())) - ->setXCompany(strval($billing->getCompany())) - ->setXAddress(strval($billing->getStreetLine(1))) - ->setXCity(strval($billing->getCity())) - ->setXState(strval($billing->getRegion())) - ->setXZip(strval($billing->getPostcode())) - ->setXCountry(strval($billing->getCountryId())) - ->setXPhone(strval($billing->getTelephone())) - ->setXFax(strval($billing->getFax())) - ->setXCustId(strval($billing->getCustomerId())) - ->setXCustomerIp(strval($order->getRemoteIp())) - ->setXCustomerTaxId(strval($billing->getTaxId())) - ->setXEmail(strval($order->getCustomerEmail())) - ->setXEmailCustomer(strval($paymentMethod->getConfigData('email_customer'))) - ->setXMerchantEmail(strval($paymentMethod->getConfigData('merchant_email'))); + $this->setXFirstName((string)$billing->getFirstname()) + ->setXLastName((string)$billing->getLastname()) + ->setXCompany((string)$billing->getCompany()) + ->setXAddress((string)$billing->getStreetLine(1)) + ->setXCity((string)$billing->getCity()) + ->setXState((string)$billing->getRegion()) + ->setXZip((string)$billing->getPostcode()) + ->setXCountry((string)$billing->getCountryId()) + ->setXPhone((string)$billing->getTelephone()) + ->setXFax((string)$billing->getFax()) + ->setXCustId((string)$billing->getCustomerId()) + ->setXCustomerIp((string)$order->getRemoteIp()) + ->setXCustomerTaxId((string)$billing->getTaxId()) + ->setXEmail((string)$order->getCustomerEmail()) + ->setXEmailCustomer((string)$paymentMethod->getConfigData('email_customer')) + ->setXMerchantEmail((string)$paymentMethod->getConfigData('merchant_email')); } $shipping = $order->getShippingAddress(); if (!empty($shipping)) { $this->setXShipToFirstName( - strval($shipping->getFirstname()) + (string)$shipping->getFirstname() )->setXShipToLastName( - strval($shipping->getLastname()) + (string)$shipping->getLastname() )->setXShipToCompany( - strval($shipping->getCompany()) + (string)$shipping->getCompany() )->setXShipToAddress( - strval($shipping->getStreetLine(1)) + (string)$shipping->getStreetLine(1) )->setXShipToCity( - strval($shipping->getCity()) + (string)$shipping->getCity() )->setXShipToState( - strval($shipping->getRegion()) + (string)$shipping->getRegion() )->setXShipToZip( - strval($shipping->getPostcode()) + (string)$shipping->getPostcode() )->setXShipToCountry( - strval($shipping->getCountryId()) + (string)$shipping->getCountryId() ); } - $this->setXPoNum(strval($payment->getPoNumber())); + $this->setXPoNum((string)$payment->getPoNumber()); return $this; } 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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/LICENSE.txt b/app/code/Magento/Authorizenet/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/LICENSE.txt rename to app/code/Magento/Authorizenet/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Braintree/LICENSE_AFL.txt b/app/code/Magento/Authorizenet/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Braintree/LICENSE_AFL.txt rename to app/code/Magento/Authorizenet/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Authorizenet/Test/Mftf/README.md b/app/code/Magento/Authorizenet/Test/Mftf/README.md new file mode 100644 index 0000000000000..9391126a85c94 --- /dev/null +++ b/app/code/Magento/Authorizenet/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Authorizenet Functional Tests + +The Functional Test Module for **Magento Authorizenet** module. 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/Test/Unit/Model/Directpost/ResponseTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php index 6e5d55e52675e..15c7eecb09a69 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php @@ -37,6 +37,9 @@ public function testGenerateHash($merchantMd5, $merchantApiLogin, $amount, $amou ); } + /** + * @return array + */ public function generateHashDataProvider() { return [ @@ -57,6 +60,13 @@ public function generateHashDataProvider() ]; } + /** + * @param $merchantMd5 + * @param $merchantApiLogin + * @param $amount + * @param $transactionId + * @return string + */ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) { return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php index dbb6ac8333c14..95c67f67852da 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Authorizenet\Test\Unit\Model; +use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Framework\Simplexml\Element; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Authorizenet\Model\Directpost; @@ -74,6 +75,14 @@ class DirectpostTest extends \PHPUnit\Framework\TestCase */ protected $requestFactory; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) @@ -104,6 +113,12 @@ protected function setUp() ->setMethods(['getTransactionDetails']) ->getMock(); + $this->paymentFailures = $this->getMockBuilder( + PaymentFailuresInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->requestFactory = $this->getRequestFactoryMock(); $httpClientFactoryMock = $this->getHttpClientFactoryMock(); @@ -117,7 +132,8 @@ protected function setUp() 'responseFactory' => $this->responseFactoryMock, 'transactionRepository' => $this->transactionRepositoryMock, 'transactionService' => $this->transactionServiceMock, - 'httpClientFactory' => $httpClientFactoryMock + 'httpClientFactory' => $httpClientFactoryMock, + 'paymentFailures' => $this->paymentFailures, ] ); } @@ -313,12 +329,16 @@ public function checkResponseCodeSuccessDataProvider() } /** - * @param bool $responseCode + * Checks response failures behaviour. + * + * @param int $responseCode + * @param int $failuresHandlerCalls + * @return void * * @expectedException \Magento\Framework\Exception\LocalizedException * @dataProvider checkResponseCodeFailureDataProvider */ - public function testCheckResponseCodeFailure($responseCode) + public function testCheckResponseCodeFailure(int $responseCode, int $failuresHandlerCalls): void { $reasonText = 'reason text'; @@ -333,18 +353,35 @@ public function testCheckResponseCodeFailure($responseCode) ->with($reasonText) ->willReturn(__('Gateway error: %1', $reasonText)); + $orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $orderMock->expects($this->exactly($failuresHandlerCalls)) + ->method('getQuoteId') + ->willReturn(1); + + $this->paymentFailures->expects($this->exactly($failuresHandlerCalls)) + ->method('handle') + ->with(1); + + $reflection = new \ReflectionClass($this->directpost); + $order = $reflection->getProperty('order'); + $order->setAccessible(true); + $order->setValue($this->directpost, $orderMock); + $this->directpost->checkResponseCode(); } /** * @return array */ - public function checkResponseCodeFailureDataProvider() + public function checkResponseCodeFailureDataProvider(): array { return [ - ['responseCode' => Directpost::RESPONSE_CODE_DECLINED], - ['responseCode' => Directpost::RESPONSE_CODE_ERROR], - ['responseCode' => 999999] + ['responseCode' => Directpost::RESPONSE_CODE_DECLINED, 1], + ['responseCode' => Directpost::RESPONSE_CODE_ERROR, 1], + ['responseCode' => 999999, 0], ]; } diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json index 89d3ba8045a40..4e646004d9a6d 100644 --- a/app/code/Magento/Authorizenet/composer.json +++ b/app/code/Magento/Authorizenet/composer.json @@ -5,23 +5,23 @@ "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" + "OSL-3.0", + "AFL-3.0" ], "autoload": { "files": [ diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml index eacf77cda1e77..3a192646b6f7e 100644 --- a/app/code/Magento/Authorizenet/etc/config.xml +++ b/app/code/Magento/Authorizenet/etc/config.xml @@ -32,6 +32,7 @@ <cgi_url>https://secure.authorize.net/gateway/transact.dll</cgi_url> <cgi_url_td_test_mode>https://apitest.authorize.net/xml/v1/request.api</cgi_url_td_test_mode> <cgi_url_td>https://api2.authorize.net/xml/v1/request.api</cgi_url_td> + <paymentInfoKeys>x_card_type,x_account_number,x_avs_code,x_auth_code,x_response_reason_text,x_cvv2_resp_code</paymentInfoKeys> </authorizenet_directpost> </payment> </default> diff --git a/app/code/Magento/Authorizenet/etc/di.xml b/app/code/Magento/Authorizenet/etc/di.xml index 4beb2456be110..69d24019f2fb7 100644 --- a/app/code/Magento/Authorizenet/etc/di.xml +++ b/app/code/Magento/Authorizenet/etc/di.xml @@ -35,4 +35,9 @@ </argument> </arguments> </type> + <type name="Magento\Authorizenet\Block\Adminhtml\Order\View\Info\PaymentDetails"> + <arguments> + <argument name="config" xsi:type="object">Magento\Authorizenet\Model\Directpost</argument> + </arguments> + </type> </config> 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 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Authorizenet" setup_version="2.0.0"> + <module name="Magento_Authorizenet" > <sequence> <module name="Magento_Sales"/> <module name="Magento_Quote"/> diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv index bb59afffff2c6..6228d5102b13c 100644 --- a/app/code/Magento/Authorizenet/i18n/en_US.csv +++ b/app/code/Magento/Authorizenet/i18n/en_US.csv @@ -67,3 +67,9 @@ Debug,Debug "Minimum Order Total","Minimum Order Total" "Maximum Order Total","Maximum Order Total" "Sort Order","Sort Order" +"x_card_type","Credit Card Type" +"x_account_number", "Credit Card Number" +"x_avs_code","AVS Response Code" +"x_auth_code","Processor Authentication Code" +"x_response_reason_text","Processor Response Text" +"x_cvv2_resp_code","CVV2 Response Code" diff --git a/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml b/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml index 60fec263352fe..ac91fa30bfbe0 100644 --- a/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml +++ b/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml @@ -44,8 +44,8 @@ $fraudDetails = $payment->getAdditionalInformation('fraud_details'); <?php endif; ?> <?php if(!empty($fraudDetails['fraud_filters'])): ?> - <b><?= $block->escapeHtml(__('Fraud Filters')) ?>: - </b></br> + <strong><?= $block->escapeHtml(__('Fraud Filters')) ?>: + </strong></br> <?php foreach($fraudDetails['fraud_filters'] as $filter): ?> <?= $block->escapeHtml($filter['name']) ?>: <?= $block->escapeHtml($filter['action']) ?> diff --git a/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js b/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js index 8edc38dce6f60..8c4c90bf111de 100644 --- a/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js +++ b/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - transparent: 'Magento_Payment/transparent' + transparent: 'Magento_Payment/js/transparent', + 'Magento_Payment/transparent': 'Magento_Payment/js/transparent' } } }; diff --git a/app/code/Magento/Backend/App/AbstractAction.php b/app/code/Magento/Backend/App/AbstractAction.php index 99ee86b2b6407..fb2daa283f111 100644 --- a/app/code/Magento/Backend/App/AbstractAction.php +++ b/app/code/Magento/Backend/App/AbstractAction.php @@ -205,10 +205,6 @@ private function _moveBlockToContainer(\Magento\Framework\View\Element\AbstractB */ public function dispatch(\Magento\Framework\App\RequestInterface $request) { - if (!$this->_processUrlKeys()) { - return parent::dispatch($request); - } - if ($request->isDispatched() && $request->getActionName() !== 'denied' && !$this->_isAllowed()) { $this->_response->setStatusHeader(403, '1.1', 'Forbidden'); if (!$this->_auth->isLoggedIn()) { @@ -217,6 +213,7 @@ public function dispatch(\Magento\Framework\App\RequestInterface $request) $this->_view->loadLayout(['default', 'adminhtml_denied'], true, true, false); $this->_view->renderLayout(); $this->_request->setDispatched(true); + return $this->_response; } @@ -226,6 +223,11 @@ public function dispatch(\Magento\Framework\App\RequestInterface $request) $this->_processLocaleSettings(); + // Need to preload isFirstPageAfterLogin (see https://github.com/magento/magento2/issues/15510) + if ($this->_auth->isLoggedIn()) { + $this->_auth->getAuthStorage()->isFirstPageAfterLogin(); + } + return parent::dispatch($request); } @@ -246,6 +248,9 @@ protected function _isUrlChecked() * Check url keys. If non valid - redirect * * @return bool + * + * @see \Magento\Backend\App\Request\BackendValidator for default + * request validation. */ public function _processUrlKeys() { diff --git a/app/code/Magento/Backend/App/Action/Plugin/Authentication.php b/app/code/Magento/Backend/App/Action/Plugin/Authentication.php index 68506a521c1cf..4b25e9921e404 100644 --- a/app/code/Magento/Backend/App/Action/Plugin/Authentication.php +++ b/app/code/Magento/Backend/App/Action/Plugin/Authentication.php @@ -160,7 +160,7 @@ protected function _processNotLoggedInUser(\Magento\Framework\App\RequestInterfa } else { $this->_actionFlag->set('', \Magento\Framework\App\ActionInterface::FLAG_NO_DISPATCH, true); $this->_response->setRedirect($this->_url->getCurrentUrl()); - $this->messageManager->addError(__('Invalid Form Key. Please refresh the page.')); + $this->messageManager->addErrorMessage(__('Invalid Form Key. Please refresh the page.')); $isRedirectNeeded = true; } } @@ -205,7 +205,7 @@ protected function _performLogin(\Magento\Framework\App\RequestInterface $reques $this->_auth->login($username, $password); } catch (AuthenticationException $e) { if (!$request->getParam('messageSent')) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $request->setParam('messageSent', true); $outputValue = false; } diff --git a/app/code/Magento/Backend/App/DefaultPath.php b/app/code/Magento/Backend/App/DefaultPath.php index df8b718389741..b790a2edc3fab 100644 --- a/app/code/Magento/Backend/App/DefaultPath.php +++ b/app/code/Magento/Backend/App/DefaultPath.php @@ -42,6 +42,6 @@ public function __construct(\Magento\Backend\App\ConfigInterface $config) */ public function getPart($code) { - return isset($this->_parts[$code]) ? $this->_parts[$code] : null; + return $this->_parts[$code] ?? null; } } diff --git a/app/code/Magento/Backend/App/Request/BackendValidator.php b/app/code/Magento/Backend/App/Request/BackendValidator.php new file mode 100644 index 0000000000000..4d04d2fed8eb2 --- /dev/null +++ b/app/code/Magento/Backend/App/Request/BackendValidator.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\App\Request; + +use Magento\Backend\App\AbstractAction; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\Request\ValidatorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Backend\Model\Auth; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Controller\Result\Raw as RawResult; +use Magento\Framework\Controller\Result\RedirectFactory; +use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; +use Magento\Backend\Model\UrlInterface as BackendUrl; +use Magento\Framework\Phrase; + +/** + * Do backend validations. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BackendValidator implements ValidatorInterface +{ + /** + * @var Auth + */ + private $auth; + + /** + * @var FormKeyValidator + */ + private $formKeyValidator; + + /** + * @var BackendUrl + */ + private $backendUrl; + + /** + * @var RedirectFactory + */ + private $redirectFactory; + + /** + * @var RawFactory + */ + private $rawResultFactory; + + /** + * @param Auth $auth + * @param FormKeyValidator $formKeyValidator + * @param BackendUrl $backendUrl + * @param RedirectFactory $redirectFactory + * @param RawFactory $rawResultFactory + */ + public function __construct( + Auth $auth, + FormKeyValidator $formKeyValidator, + BackendUrl $backendUrl, + RedirectFactory $redirectFactory, + RawFactory $rawResultFactory + ) { + $this->auth = $auth; + $this->formKeyValidator = $formKeyValidator; + $this->backendUrl = $backendUrl; + $this->redirectFactory = $redirectFactory; + $this->rawResultFactory = $rawResultFactory; + } + + /** + * Validate request + * + * @param RequestInterface $request + * @param ActionInterface $action + * + * @return bool + */ + private function validateRequest( + RequestInterface $request, + ActionInterface $action + ): bool { + /** @var bool|null $valid */ + $valid = null; + + if ($action instanceof CsrfAwareActionInterface) { + $valid = $action->validateForCsrf($request); + } + + if ($valid === null) { + $validFormKey = true; + $validSecretKey = true; + if ($request instanceof HttpRequest && $request->isPost()) { + $validFormKey = $this->formKeyValidator->validate($request); + } elseif ($this->auth->isLoggedIn() + && $this->backendUrl->useSecretKey() + ) { + $secretKeyValue = (string)$request->getParam( + BackendUrl::SECRET_KEY_PARAM_NAME, + null + ); + $secretKey = $this->backendUrl->getSecretKey(); + $validSecretKey = ($secretKeyValue === $secretKey); + } + $valid = $validFormKey && $validSecretKey; + } + + return $valid; + } + + /** + * Create exception + * + * @param RequestInterface $request + * @param ActionInterface $action + * + * @return InvalidRequestException + */ + private function createException( + RequestInterface $request, + ActionInterface $action + ): InvalidRequestException { + /** @var InvalidRequestException|null $exception */ + $exception = null; + + if ($action instanceof CsrfAwareActionInterface) { + $exception = $action->createCsrfValidationException($request); + } + + if ($exception === null) { + if ($request instanceof HttpRequest && $request->isAjax()) { + //Sending empty response for AJAX request since we don't know + //the expected response format and it's pointless to redirect. + /** @var RawResult $response */ + $response = $this->rawResultFactory->create(); + $response->setHttpResponseCode(401); + $response->setContents(''); + $exception = new InvalidRequestException($response); + } else { + //For regular requests. + $response = $this->redirectFactory->create() + ->setUrl($this->backendUrl->getStartupPageUrl()); + $exception = new InvalidRequestException( + $response, + [ + new Phrase( + 'Invalid security or form key. Please refresh the page.' + ) + ] + ); + } + } + + return $exception; + } + + /** + * @inheritDoc + */ + public function validate( + RequestInterface $request, + ActionInterface $action + ): void { + if ($action instanceof AbstractAction) { + //Abstract Action has built-in validation. + if (!$action->_processUrlKeys()) { + throw new InvalidRequestException($action->getResponse()); + } + } else { + //Fallback validation. + if (!$this->validateRequest($request, $action)) { + throw $this->createException($request, $action); + } + } + } +} 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 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Backend\Block\Cache; + +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Class Permissions + */ +class Permissions implements ArgumentInterface +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Permissions constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct(AuthorizationInterface $authorization) + { + $this->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/Bar.php b/app/code/Magento/Backend/Block/Dashboard/Bar.php index 29557f12c1093..7ccb2d51ccd1b 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Bar.php +++ b/app/code/Magento/Backend/Block/Dashboard/Bar.php @@ -38,14 +38,6 @@ public function getTotals() */ public function addTotal($label, $value, $isQuantity = false) { - /*if (!$isQuantity) { - $value = $this->format($value); - $decimals = substr($value, -2); - $value = substr($value, 0, -2); - } else { - $value = ($value != '')?$value:0; - $decimals = ''; - }*/ if (!$isQuantity) { $value = $this->format($value); } diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index 301dffbdc4987..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 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/DataProviders/ImageUploadConfig.php b/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php new file mode 100644 index 0000000000000..9c17b0e5538f4 --- /dev/null +++ b/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Block\DataProviders; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Backend\Model\Image\UploadResizeConfigInterface; + +/** + * Provides additional data for image uploader + */ +class ImageUploadConfig implements ArgumentInterface +{ + /** + * @var UploadResizeConfigInterface + */ + private $imageUploadConfig; + + /** + * @param UploadResizeConfigInterface $imageUploadConfig + */ + public function __construct(UploadResizeConfigInterface $imageUploadConfig) + { + $this->imageUploadConfig = $imageUploadConfig; + } + + /** + * Get image resize configuration + * + * @return int + */ + public function getIsResizeEnabled(): int + { + return (int)$this->imageUploadConfig->isResizeEnabled(); + } +} diff --git a/app/code/Magento/Backend/Block/GlobalSearch.php b/app/code/Magento/Backend/Block/GlobalSearch.php index f4a46283808f4..3cea12fea205c 100644 --- a/app/code/Magento/Backend/Block/GlobalSearch.php +++ b/app/code/Magento/Backend/Block/GlobalSearch.php @@ -31,6 +31,7 @@ public function getWidgetInitOptions() 'filterProperty' => 'name', 'preventClickPropagation' => false, 'minLength' => 2, + 'submitInputOnEnter' => false, ] ]; } diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php index 5bad74d8a8be5..84fa487281ac8 100644 --- a/app/code/Magento/Backend/Block/Media/Uploader.php +++ b/app/code/Magento/Backend/Block/Media/Uploader.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Block\Media; use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Image\Adapter\UploadConfigInterface; +use Magento\Backend\Model\Image\UploadResizeConfigInterface; /** * Adminhtml media library uploader @@ -35,24 +39,46 @@ class Uploader extends \Magento\Backend\Block\Widget */ private $jsonEncoder; + /** + * @var UploadResizeConfigInterface + */ + private $imageUploadConfig; + + /** + * @var UploadConfigInterface + * @deprecated + * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface + */ + private $imageConfig; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\File\Size $fileSize * @param array $data * @param Json $jsonEncoder + * @param UploadConfigInterface $imageConfig + * @param UploadResizeConfigInterface $imageUploadConfig */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\File\Size $fileSize, array $data = [], - Json $jsonEncoder = null + Json $jsonEncoder = null, + UploadConfigInterface $imageConfig = null, + UploadResizeConfigInterface $imageUploadConfig = null ) { $this->_fileSizeService = $fileSize; $this->jsonEncoder = $jsonEncoder ?: ObjectManager::getInstance()->get(Json::class); + $this->imageConfig = $imageConfig + ?: ObjectManager::getInstance()->get(UploadConfigInterface::class); + $this->imageUploadConfig = $imageUploadConfig + ?: ObjectManager::getInstance()->get(UploadResizeConfigInterface::class); parent::__construct($context, $data); } /** + * Initialize block. + * * @return void */ protected function _construct() @@ -90,6 +116,26 @@ public function getFileSizeService() return $this->_fileSizeService; } + /** + * Get Image Upload Maximum Width Config. + * + * @return int + */ + public function getImageUploadMaxWidth() + { + return $this->imageUploadConfig->getMaxWidth(); + } + + /** + * Get Image Upload Maximum Height Config. + * + * @return int + */ + public function getImageUploadMaxHeight() + { + return $this->imageUploadConfig->getMaxHeight(); + } + /** * Prepares layout and set element renderer * diff --git a/app/code/Magento/Backend/Block/Menu.php b/app/code/Magento/Backend/Block/Menu.php index d6bdeb4ea8968..1e2561e2efe05 100644 --- a/app/code/Magento/Backend/Block/Menu.php +++ b/app/code/Magento/Backend/Block/Menu.php @@ -9,12 +9,12 @@ /** * Backend menu block * - * @api - * @method \Magento\Backend\Block\Menu setAdditionalCacheKeyInfo(array $cacheKeyInfo) + * @method $this setAdditionalCacheKeyInfo(array $cacheKeyInfo) * @method array getAdditionalCacheKeyInfo() - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Menu extends \Magento\Backend\Block\Template { @@ -75,7 +75,12 @@ class Menu extends \Magento\Backend\Block\Template private $anchorRenderer; /** - * @param Template\Context $context + * @var \Magento\Framework\App\Route\ConfigInterface + */ + private $routeConfig; + + /** + * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Model\UrlInterface $url * @param \Magento\Backend\Model\Menu\Filter\IteratorFactory $iteratorFactory * @param \Magento\Backend\Model\Auth\Session $authSession @@ -84,6 +89,9 @@ class Menu extends \Magento\Backend\Block\Template * @param array $data * @param MenuItemChecker|null $menuItemChecker * @param AnchorRenderer|null $anchorRenderer + * @param \Magento\Framework\App\Route\ConfigInterface|null $routeConfig + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -94,7 +102,8 @@ public function __construct( \Magento\Framework\Locale\ResolverInterface $localeResolver, array $data = [], MenuItemChecker $menuItemChecker = null, - AnchorRenderer $anchorRenderer = null + AnchorRenderer $anchorRenderer = null, + \Magento\Framework\App\Route\ConfigInterface $routeConfig = null ) { $this->_url = $url; $this->_iteratorFactory = $iteratorFactory; @@ -103,6 +112,9 @@ public function __construct( $this->_localeResolver = $localeResolver; $this->menuItemChecker = $menuItemChecker; $this->anchorRenderer = $anchorRenderer; + $this->routeConfig = $routeConfig ?: + \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\App\Route\ConfigInterface::class); parent::__construct($context, $data); } @@ -130,6 +142,7 @@ protected function _getAnchorLabel($menuItem) /** * Render menu item mouse events + * * @param \Magento\Backend\Model\Menu\Item $menuItem * @return string */ @@ -203,8 +216,9 @@ protected function _afterToHtml($html) */ protected function _callbackSecretKey($match) { + $routeId = $this->routeConfig->getRouteByFrontName($match[1]); return \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/' . $this->_url->getSecretKey( - $match[1], + $routeId ?: $match[1], $match[2], $match[3] ); @@ -342,7 +356,7 @@ protected function _columnBrake($items, $limit) * @param \Magento\Backend\Model\Menu\Item $menuItem * @param int $level * @param int $limit - * @param $id int + * @param int|null $id * @return string HTML code */ protected function _addSubMenu($menuItem, $level, $limit, $id = null) @@ -352,7 +366,7 @@ protected function _addSubMenu($menuItem, $level, $limit, $id = null) return $output; } $output .= '<div class="submenu"' . ($level == 0 && isset($id) ? ' aria-labelledby="' . $id . '"' : '') . '>'; - $colStops = null; + $colStops = []; if ($level == 0 && $limit) { $colStops = $this->_columnBrake($menuItem->getChildren(), $limit); $output .= '<strong class="submenu-title">' . $this->_getAnchorLabel($menuItem) . '</strong>'; @@ -387,7 +401,11 @@ public function renderNavigation($menu, $level = 0, $limit = 0, $colBrakes = []) $itemName = substr($menuId, strrpos($menuId, '::') + 2); $itemClass = str_replace('_', '-', strtolower($itemName)); - if (count($colBrakes) && $colBrakes[$itemPosition]['colbrake'] && $itemPosition != 1) { + if (is_array($colBrakes) + && count($colBrakes) + && $colBrakes[$itemPosition]['colbrake'] + && $itemPosition != 1 + ) { $output .= '</ul></li><li class="column"><ul role="menu">'; } @@ -401,7 +419,7 @@ public function renderNavigation($menu, $level = 0, $limit = 0, $colBrakes = []) $itemPosition++; } - if (count($colBrakes) && $limit) { + if (is_array($colBrakes) && count($colBrakes) && $limit) { $output = '<li class="column"><ul role="menu">' . $output . '</ul></li>'; } diff --git a/app/code/Magento/Backend/Block/Page/Copyright.php b/app/code/Magento/Backend/Block/Page/Copyright.php index 062497d6a8304..a1b61352930b5 100644 --- a/app/code/Magento/Backend/Block/Page/Copyright.php +++ b/app/code/Magento/Backend/Block/Page/Copyright.php @@ -18,5 +18,5 @@ class Copyright extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'page/copyright.phtml'; + protected $_template = 'Magento_Backend::page/copyright.phtml'; } diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php index 368869b79e15c..e0c173a4cbfec 100644 --- a/app/code/Magento/Backend/Block/Page/Footer.php +++ b/app/code/Magento/Backend/Block/Page/Footer.php @@ -17,7 +17,7 @@ class Footer extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'page/footer.phtml'; + protected $_template = 'Magento_Backend::page/footer.phtml'; /** * @var \Magento\Framework\App\ProductMetadataInterface @@ -40,7 +40,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -57,4 +57,12 @@ public function getMagentoVersion() { return $this->productMetadata->getVersion(); } + + /** + * @inheritdoc + */ + protected function getCacheLifetime() + { + return 3600 * 24 * 10; + } } diff --git a/app/code/Magento/Backend/Block/Page/Header.php b/app/code/Magento/Backend/Block/Page/Header.php index b7ed05ce58e95..c2c5f7472b370 100644 --- a/app/code/Magento/Backend/Block/Page/Header.php +++ b/app/code/Magento/Backend/Block/Page/Header.php @@ -18,7 +18,7 @@ class Header extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'page/header.phtml'; + protected $_template = 'Magento_Backend::page/header.phtml'; /** * Backend data diff --git a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php index 2f9b73f0ae037..6fe8416784c2e 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php +++ b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php @@ -25,7 +25,7 @@ class Fieldset extends \Magento\Backend\Block\Template implements RendererInterf /** * @var string */ - protected $_template = 'store/switcher/form/renderer/fieldset.phtml'; + protected $_template = 'Magento_Backend::store/switcher/form/renderer/fieldset.phtml'; /** * Retrieve an element diff --git a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php index ddd1f1a9178cd..71d4db6849bd2 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php @@ -23,7 +23,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme /** * @var string */ - protected $_template = 'store/switcher/form/renderer/fieldset/element.phtml'; + protected $_template = 'Magento_Backend::store/switcher/form/renderer/fieldset/element.phtml'; /** * Retrieve an element diff --git a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php index e479e8f560dae..47a156c16ce3e 100644 --- a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php +++ b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Backend\Block\System\Store\Delete; +use Magento\Backup\Helper\Data as BackupHelper; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml cms block edit form * @@ -12,6 +15,25 @@ */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { + /** + * @var BackupHelper + */ + private $backup; + + /** + * @inheritDoc + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Data\FormFactory $formFactory, + array $data = [], + ?BackupHelper $backup = null + ) { + parent::__construct($context, $registry, $formFactory, $data); + $this->backup = $backup ?? ObjectManager::getInstance()->get(BackupHelper::class); + } + /** * Init form * @@ -25,7 +47,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritDoc */ protected function _prepareForm() { @@ -45,6 +67,12 @@ protected function _prepareForm() $fieldset->addField('item_id', 'hidden', ['name' => 'item_id', 'value' => $dataObject->getId()]); + $backupOptions = ['0' => __('No')]; + $backupSelected = '0'; + if ($this->backup->isEnabled()) { + $backupOptions['1'] = __('Yes'); + $backupSelected = '1'; + } $fieldset->addField( 'create_backup', 'select', @@ -52,8 +80,8 @@ protected function _prepareForm() 'label' => __('Create DB Backup'), 'title' => __('Create DB Backup'), 'name' => 'create_backup', - 'options' => ['1' => __('Yes'), '0' => __('No')], - 'value' => '1' + 'options' => $backupOptions, + 'value' => $backupSelected ] ); diff --git a/app/code/Magento/Backend/Block/System/Store/Delete/Group.php b/app/code/Magento/Backend/Block/System/Store/Delete/Group.php index ae80b56066a6d..e95f3bbf9f8c1 100644 --- a/app/code/Magento/Backend/Block/System/Store/Delete/Group.php +++ b/app/code/Magento/Backend/Block/System/Store/Delete/Group.php @@ -19,7 +19,7 @@ protected function _prepareLayout() { $itemId = $this->getRequest()->getParam('group_id'); - $this->setTemplate('system/store/delete_group.phtml'); + $this->setTemplate('Magento_Backend::system/store/delete_group.phtml'); $this->setAction($this->getUrl('adminhtml/*/deleteGroupPost', ['group_id' => $itemId])); $this->addChild( 'confirm_deletion_button', diff --git a/app/code/Magento/Backend/Block/System/Store/Delete/Website.php b/app/code/Magento/Backend/Block/System/Store/Delete/Website.php index da28a471130cc..82cbb780137b8 100644 --- a/app/code/Magento/Backend/Block/System/Store/Delete/Website.php +++ b/app/code/Magento/Backend/Block/System/Store/Delete/Website.php @@ -19,7 +19,7 @@ protected function _prepareLayout() { $itemId = $this->getRequest()->getParam('website_id'); - $this->setTemplate('system/store/delete_website.phtml'); + $this->setTemplate('Magento_Backend::system/store/delete_website.phtml'); $this->setAction($this->getUrl('adminhtml/*/deleteWebsitePost', ['website_id' => $itemId])); $this->addChild( 'confirm_deletion_button', diff --git a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php index f19799d2e4939..034887c67d1ee 100644 --- a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php +++ b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php @@ -37,7 +37,7 @@ protected function _prepareForm() ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']] ); - $this->_prepareStoreFieldSet($form); + $this->_prepareStoreFieldset($form); $form->addField( 'store_type', diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php index 59657f38465d7..3d7154eb20f92 100644 --- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php +++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php @@ -27,6 +27,7 @@ public function render(\Magento\Framework\DataObject $row) $this->getUrl('adminhtml/*/editGroup', ['group_id' => $row->getGroupId()]) . '">' . $this->escapeHtml($row->getData($this->getColumn()->getIndex())) . - '</a>'; + '</a><br />' + . '(' . __('Code') . ': ' . $row->getGroupCode() . ')'; } } diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php index 23b2de683a958..9cfc8bfc52691 100644 --- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php +++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php @@ -27,6 +27,7 @@ public function render(\Magento\Framework\DataObject $row) $this->getUrl('adminhtml/*/editStore', ['store_id' => $row->getStoreId()]) . '">' . $this->escapeHtml($row->getData($this->getColumn()->getIndex())) . - '</a>'; + '</a><br />' . + '(' . __('Code') . ': ' . $row->getStoreCode() . ')'; } } diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php index 913e2c903d20c..487eb4f8acfda 100644 --- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php +++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php @@ -24,6 +24,7 @@ public function render(\Magento\Framework\DataObject $row) $this->getUrl('adminhtml/*/editWebsite', ['website_id' => $row->getWebsiteId()]) . '">' . $this->escapeHtml($row->getData($this->getColumn()->getIndex())) . - '</a>'; + '</a><br />' . + '(' . __('Code') . ': ' . $row->getCode() . ')'; } } diff --git a/app/code/Magento/Backend/Block/Template.php b/app/code/Magento/Backend/Block/Template.php index d0f39b54c1492..3ae4451a2592f 100644 --- a/app/code/Magento/Backend/Block/Template.php +++ b/app/code/Magento/Backend/Block/Template.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backend\Block; /** @@ -17,10 +20,12 @@ * Example: * <block name="my.block" class="Magento\Backend\Block\Template" template="My_Module::template.phtml" > * <arguments> - * <argument name="viewModel" xsi:type="object">My\Module\ViewModel\Custom</argument> + * <argument name="view_model" xsi:type="object">My\Module\ViewModel\Custom</argument> * </arguments> * </block> * + * Your class object can then be accessed by doing $block->getViewModel() + * * @api * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 diff --git a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php index 94af9a1d7578f..5a792ddb39132 100644 --- a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php +++ b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php @@ -7,6 +7,8 @@ namespace Magento\Backend\Block\Widget\Button; /** + * Button list widget + * * @api * @since 100.0.2 */ @@ -127,12 +129,6 @@ public function getItems() */ public function sortButtons(Item $itemA, Item $itemB) { - $sortOrderA = intval($itemA->getSortOrder()); - $sortOrderB = intval($itemB->getSortOrder()); - - if ($sortOrderA == $sortOrderB) { - return 0; - } - return ($sortOrderA < $sortOrderB) ? -1 : 1; + return (int)$itemA->getSortOrder() <=> (int)$itemB->getSortOrder(); } } diff --git a/app/code/Magento/Backend/Block/Widget/Form.php b/app/code/Magento/Backend/Block/Widget/Form.php index 30221618edbed..38d5d90a22d15 100644 --- a/app/code/Magento/Backend/Block/Widget/Form.php +++ b/app/code/Magento/Backend/Block/Widget/Form.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Block\Widget; +use Magento\Framework\App\ObjectManager; + /** * Backend form widget * @@ -27,13 +30,23 @@ class Form extends \Magento\Backend\Block\Widget */ protected $_template = 'Magento_Backend::widget/form.phtml'; + /** @var Form\Element\ElementCreator */ + private $creator; + /** + * Constructs form + * * @param \Magento\Backend\Block\Template\Context $context * @param array $data + * @param Form\Element\ElementCreator|null $creator */ - public function __construct(\Magento\Backend\Block\Template\Context $context, array $data = []) - { + public function __construct( + \Magento\Backend\Block\Template\Context $context, + array $data = [], + Form\Element\ElementCreator $creator = null + ) { parent::__construct($context, $data); + $this->creator = $creator ?: ObjectManager::getInstance()->get(Form\Element\ElementCreator::class); } /** @@ -46,7 +59,6 @@ protected function _construct() parent::_construct(); $this->setDestElementId('edit_form'); - $this->setShowGlobalIcon(false); } /** @@ -148,6 +160,7 @@ protected function _beforeToHtml() /** * Initialize form fields values + * * Method will be called after prepareForm and can be used for field values initialization * * @return $this @@ -173,32 +186,11 @@ protected function _setFieldset($attributes, $fieldset, $exclude = []) if (!$this->_isAttributeVisible($attribute)) { continue; } - if (($inputType = $attribute->getFrontend()->getInputType()) && !in_array( - $attribute->getAttributeCode(), - $exclude - ) && ('media_image' != $inputType || $attribute->getAttributeCode() == 'image') + if (($inputType = $attribute->getFrontend()->getInputType()) + && !in_array($attribute->getAttributeCode(), $exclude) + && ('media_image' !== $inputType || $attribute->getAttributeCode() == 'image') ) { - $fieldType = $inputType; - $rendererClass = $attribute->getFrontend()->getInputRendererClass(); - if (!empty($rendererClass)) { - $fieldType = $inputType . '_' . $attribute->getAttributeCode(); - $fieldset->addType($fieldType, $rendererClass); - } - - $element = $fieldset->addField( - $attribute->getAttributeCode(), - $fieldType, - [ - 'name' => $attribute->getAttributeCode(), - 'label' => $attribute->getFrontend()->getLocalizedLabel(), - 'class' => $attribute->getFrontend()->getClass(), - 'required' => $attribute->getIsRequired(), - 'note' => $attribute->getNote() - ] - )->setEntityAttribute( - $attribute - ); - + $element = $this->creator->create($fieldset, $attribute); $element->setAfterElementHtml($this->_getAdditionalElementHtml($element)); $this->_applyTypeSpecificConfig($inputType, $element, $attribute); diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php index 8b7babc1bb9b6..97116de6db79b 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Container.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php @@ -93,7 +93,7 @@ protected function _construct() 'class' => 'delete', 'onclick' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')' + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})' ] ); } diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php index 723deab1e9f7e..d599d5fbad5e0 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ +namespace Magento\Backend\Block\Widget\Form\Element; + /** * Form element dependencies mapper * Assumes that one element may depend on other element values. * Will toggle as "enabled" only if all elements it depends from toggle as true. - */ -namespace Magento\Backend\Block\Widget\Form\Element; - -/** + * * @api * @since 100.0.2 */ @@ -117,6 +116,7 @@ public function addConfigOptions(array $options) /** * HTML output getter + * * @return string */ protected function _toHtml() @@ -124,18 +124,23 @@ protected function _toHtml() if (!$this->_depends) { return ''; } - return '<script> - require(["mage/adminhtml/form"], function(){ - new FormElementDependenceController(' . - $this->_getDependsJson() . - ($this->_configOptions ? ', ' . - $this->_jsonEncoder->encode( - $this->_configOptions - ) : '') . '); });</script>'; + + $params = $this->_getDependsJson(); + + if ($this->_configOptions) { + $params .= ', ' . $this->_jsonEncoder->encode($this->_configOptions); + } + + return "<script> +require(['mage/adminhtml/form'], function(){ + new FormElementDependenceController({$params}); +}); +</script>"; } /** - * Field dependences JSON map generator + * Field dependencies JSON map generator + * * @return string */ protected function _getDependsJson() diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php new file mode 100644 index 0000000000000..b9cdd259796d0 --- /dev/null +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Block\Widget\Form\Element; + +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\Data\Form\Element\Fieldset; + +/** + * Class ElementCreator + * + * @deprecated 100.3.0 in favour of UI component implementation + * @package Magento\Backend\Block\Widget\Form\Element + */ +class ElementCreator +{ + /** + * @var array + */ + private $modifiers; + + /** + * ElementCreator constructor. + * + * @param array $modifiers + */ + public function __construct(array $modifiers = []) + { + $this->modifiers = $modifiers; + } + + /** + * Creates element + * + * @param Fieldset $fieldset + * @param Attribute $attribute + * + * @return AbstractElement + */ + public function create(Fieldset $fieldset, Attribute $attribute): AbstractElement + { + $config = $this->getElementConfig($attribute); + + if (!empty($config['rendererClass'])) { + $fieldType = $config['inputType'] . '_' . $attribute->getAttributeCode(); + $fieldset->addType($fieldType, $config['rendererClass']); + } + + return $fieldset + ->addField($config['attribute_code'], $config['inputType'], $config) + ->setEntityAttribute($attribute); + } + + /** + * Returns element config + * + * @param Attribute $attribute + * @return array + */ + private function getElementConfig(Attribute $attribute): array + { + $defaultConfig = $this->createDefaultConfig($attribute); + $config = $this->modifyConfig($defaultConfig); + + $config['label'] = __($config['label']); + + return $config; + } + + /** + * Returns default config + * + * @param Attribute $attribute + * @return array + */ + private function createDefaultConfig(Attribute $attribute): array + { + return [ + 'inputType' => $attribute->getFrontend()->getInputType(), + 'rendererClass' => $attribute->getFrontend()->getInputRendererClass(), + 'attribute_code' => $attribute->getAttributeCode(), + 'name' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontend()->getLabel(), + 'class' => $attribute->getFrontend()->getClass(), + 'required' => $attribute->getIsRequired(), + 'note' => $attribute->getNote(), + ]; + } + + /** + * Modify config + * + * @param array $config + * @return array + */ + private function modifyConfig(array $config): array + { + if ($this->isModified($config['attribute_code'])) { + return $this->applyModifier($config); + } + return $config; + } + + /** + * Returns bool if attribute need to modify + * + * @param string $attribute_code + * @return bool + */ + private function isModified($attribute_code): bool + { + return isset($this->modifiers[$attribute_code]); + } + + /** + * Apply modifier to config + * + * @param array $config + * @return array + */ + private function applyModifier(array $config): array + { + $modifiedConfig = $this->modifiers[$config['attribute_code']]; + foreach (array_keys($config) as $key) { + if (isset($modifiedConfig[$key])) { + $config[$key] = $modifiedConfig[$key]; + } + } + return $config; + } +} diff --git a/app/code/Magento/Backend/Block/Widget/Grid.php b/app/code/Magento/Backend/Block/Widget/Grid.php index 72ab5a265d808..66298d23389fb 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid.php +++ b/app/code/Magento/Backend/Block/Widget/Grid.php @@ -12,7 +12,7 @@ * @api * @deprecated 100.2.0 in favour of UI component implementation * @method string getRowClickCallback() getRowClickCallback() - * @method \Magento\Backend\Block\Widget\Grid setRowClickCallback() setRowClickCallback(string $value) + * @method \Magento\Backend\Block\Widget\Grid setRowClickCallback(string $value) * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ @@ -150,7 +150,10 @@ public function __construct( } /** + * Internal constructor, that is called from real constructor + * * @return void + * * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _construct() @@ -709,6 +712,7 @@ public function getGridUrl() /** * Grid url getter + * * Version of getGridUrl() but with parameters * * @param array $params url parameters diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php index 40a5d92c56b6f..632603d389d21 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php @@ -127,7 +127,7 @@ public function getHtml() /** * @param string|null $index - * @return string + * @return array|string|int|float|null */ public function getEscapedValue($index = null) { @@ -138,6 +138,11 @@ public function getEscapedValue($index = null) $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT) ); } + + if (is_string($value)) { + return $this->escapeHtml($value); + } + return $value; } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php index 96b3471db845e..1d8d658267020 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php @@ -26,7 +26,6 @@ public function getValue($index = null) { if ($index) { if ($data = $this->getData('value', 'orig_' . $index)) { - // date('Y-m-d', strtotime($data)); return $data; } return null; @@ -140,8 +139,8 @@ public function getHtml() /** * Return escaped value for calendar * - * @param string $index - * @return string + * @param string|null $index + * @return array|string|int|float|null */ public function getEscapedValue($index = null) { @@ -150,6 +149,11 @@ public function getEscapedValue($index = null) if ($value instanceof \DateTimeInterface) { return $this->_localeDate->formatDateTime($value); } + + if (is_string($value)) { + return $this->escapeHtml($value); + } + return $value; } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php index 2cbe264c5f396..479a2b6b20293 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php @@ -31,8 +31,7 @@ public function getCondition() { if ($this->getValue()) { return $this->getColumn()->getValue(); - } else { - return [['neq' => $this->getColumn()->getValue()], ['is' => new \Zend_Db_Expr('NULL')]]; } + return [['neq' => $this->getColumn()->getValue()], ['is' => new \Zend_Db_Expr('NULL')]]; } } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php index d49ad2941146b..a0907726ccc46 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php @@ -9,6 +9,9 @@ */ namespace Magento\Backend\Block\Widget\Grid\Column\Filter; +/** + * Theme grid filter + */ class Theme extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilter { /** @@ -54,7 +57,8 @@ public function getHtml() } /** - * Retrieve options setted in column. + * Retrieve options set in column. + * * Or load if options was not set. * * @return array diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php index ff0399e4f507f..03566bce3fc34 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php @@ -68,10 +68,7 @@ public function __construct( $this->_storeManager = $storeManager; $this->_currencyLocator = $currencyLocator; $this->_localeCurrency = $localeCurrency; - $defaultBaseCurrencyCode = $this->_scopeConfig->getValue( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, - 'default' - ); + $defaultBaseCurrencyCode = $currencyLocator->getDefaultCurrency($this->_request); $this->_defaultBaseCurrency = $currencyFactory->create()->load($defaultBaseCurrencyCode); } @@ -85,7 +82,7 @@ public function render(\Magento\Framework\DataObject $row) { if ($data = (string)$this->_getValue($row)) { $currency_code = $this->_getCurrencyCode($row); - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $sign = (bool)(int)$this->getColumn()->getShowNumberSign() && $data > 0 ? '+' : ''; $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currency_code)->toCurrency($data); @@ -121,10 +118,10 @@ protected function _getCurrencyCode($row) protected function _getRate($row) { if ($rate = $this->getColumn()->getRate()) { - return floatval($rate); + return (float)$rate; } if ($rate = $row->getData($this->getColumn()->getRateField())) { - return floatval($rate); + return (float)$rate; } return $this->_defaultBaseCurrency->getRate($this->_getCurrencyCode($row)); } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php index 320713f8b57c4..a611e91f32f00 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php @@ -65,7 +65,7 @@ public function render(\Magento\Framework\DataObject $row) */ protected function _getCheckboxHtml($value, $checked) { - $id = 'id_' . rand(0, 999); + $id = 'id_' . random_int(0, 999); $html = '<label class="data-grid-checkbox-cell-inner" for="'. $id .'">'; $html .= '<input type="checkbox" name="' . $this->getColumn()->getName() . '" '; $html .= 'id="' . $id . '" data-role="select-row"'; diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php index e4300c63485f5..9da23af83f036 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php @@ -60,7 +60,7 @@ public function render(\Magento\Framework\DataObject $row) return $data; } - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data); return $data; @@ -94,10 +94,10 @@ protected function _getCurrencyCode($row) protected function _getRate($row) { if ($rate = $this->getColumn()->getRate()) { - return floatval($rate); + return (float)$rate; } if ($rate = $row->getData($this->getColumn()->getRateField())) { - return floatval($rate); + return (float)$rate; } return 1; } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/ColumnSet.php b/app/code/Magento/Backend/Block/Widget/Grid/ColumnSet.php index 1a6f937f1171d..9443ffefbc0df 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/ColumnSet.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/ColumnSet.php @@ -258,7 +258,8 @@ public function getRowUrl($item) */ public function getMultipleRows($item) { - return $item->getChildren(); + $children = $item->getChildren(); + return $children ?: []; } /** diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php index d9b00d2ba2503..662cbedaed8db 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Block\Widget\Grid; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Json\EncoderInterface; + /** * Grid widget massaction default block * @@ -14,4 +21,72 @@ */ class Massaction extends \Magento\Backend\Block\Widget\Grid\Massaction\AbstractMassaction { + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Map bind item id to a particular acl type + * itemId => acl + * + * @var array + */ + private $restrictions = [ + 'enable' => 'Magento_Backend::toggling_cache_type', + 'disable' => 'Magento_Backend::toggling_cache_type', + 'refresh' => 'Magento_Backend::refresh_cache_type', + ]; + + /** + * Massaction constructor. + * + * @param Context $context + * @param EncoderInterface $jsonEncoder + * @param array $data + * @param AuthorizationInterface $authorization + */ + public function __construct( + Context $context, + EncoderInterface $jsonEncoder, + array $data = [], + AuthorizationInterface $authorization = null + ) { + $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); + + parent::__construct($context, $jsonEncoder, $data); + } + + /** + * {@inheritdoc} + * + * @param string $itemId + * @param array|DataObject $item + * + * @return $this + */ + public function addItem($itemId, $item) + { + if (!$this->isRestricted($itemId)) { + parent::addItem($itemId, $item); + } + + return $this; + } + + /** + * Check if access to action restricted + * + * @param string $itemId + * + * @return bool + */ + private function isRestricted(string $itemId): bool + { + if (!key_exists($itemId, $this->restrictions)) { + return false; + } + + return !$this->authorization->isAllowed($this->restrictions[$itemId]); + } } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 8252ed1a1e2f8..9890a10a4ceb0 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Block\Widget\Grid\Massaction; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; +use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\DataObject; /** @@ -51,13 +53,13 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { parent::_construct(); - $this->setErrorText($this->escapeHtml(__('Please select items.'))); + $this->setErrorText($this->escapeHtml(__('An item needs to be selected. Select and try again.'))); if (null !== $this->getOptions()) { foreach ($this->getOptions() as $optionId => $option) { @@ -216,30 +218,30 @@ public function getGridJsObjectName() * Retrieve JSON string of selected checkboxes * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSelectedJson() { if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) { $selected = explode(',', $selected); return join(',', $selected); - } else { - return ''; } + return ''; } /** * Retrieve array of selected checkboxes * * @return string[] + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSelected() { if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) { $selected = explode(',', $selected); return $selected; - } else { - return []; } + return []; } /** @@ -253,6 +255,8 @@ public function getApplyButtonHtml() } /** + * Get mass action javascript code. + * * @return string */ public function getJavaScript() @@ -269,6 +273,8 @@ public function getJavaScript() } /** + * Get grid ids in JSON format. + * * @return string */ public function getGridIdsJson() @@ -278,13 +284,18 @@ public function getGridIdsJson() } /** @var \Magento\Framework\Data\Collection $allIdsCollection */ $allIdsCollection = clone $this->getParentBlock()->getCollection(); - + if ($this->getMassactionIdField()) { $massActionIdField = $this->getMassactionIdField(); } else { $massActionIdField = $this->getParentBlock()->getMassactionIdField(); } - + + if ($allIdsCollection instanceof AbstractDb) { + $allIdsCollection->getSelect()->limit(); + $allIdsCollection->clear(); + } + $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); if (!empty($gridIds)) { return join(",", $gridIds); @@ -293,6 +304,8 @@ public function getGridIdsJson() } /** + * Get Html id. + * * @return string */ public function getHtmlId() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php index 42f5e61bf5fa8..8e0fce2b16cc9 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Block\Widget\Grid\Massaction; /** @@ -69,7 +70,7 @@ public function __construct( public function _construct() { parent::_construct(); - $this->setErrorText($this->escapeHtml(__('Please select items.'))); + $this->setErrorText($this->escapeHtml(__('An item needs to be selected. Select and try again.'))); } /** @@ -218,9 +219,8 @@ public function getSelectedJson() if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) { $selected = explode(',', $selected); return join(',', $selected); - } else { - return ''; } + return ''; } /** @@ -233,9 +233,8 @@ public function getSelected() if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) { $selected = explode(',', $selected); return $selected; - } else { - return []; } + return []; } /** @@ -275,13 +274,13 @@ public function getGridIdsJson() /** @var \Magento\Framework\Data\Collection $allIdsCollection */ $allIdsCollection = clone $this->getParentBlock()->getCollection(); - + if ($this->getMassactionIdField()) { $massActionIdField = $this->getMassactionIdField(); } else { $massActionIdField = $this->getParentBlock()->getMassactionIdField(); } - + $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); if (!empty($gridIds)) { diff --git a/app/code/Magento/Backend/Block/Widget/Tabs.php b/app/code/Magento/Backend/Block/Widget/Tabs.php index 7f146e19e8c6e..c7c1f93e8ca73 100644 --- a/app/code/Magento/Backend/Block/Widget/Tabs.php +++ b/app/code/Magento/Backend/Block/Widget/Tabs.php @@ -8,6 +8,8 @@ use Magento\Backend\Block\Widget\Tab\TabInterface; /** + * Tabs widget + * * @api * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 @@ -117,6 +119,7 @@ public function addTab($tabId, $tab) if (empty($tabId)) { throw new \Exception(__('Please correct the tab configuration and try again. Tab Id should be not empty')); } + if (is_array($tab)) { $this->_tabs[$tabId] = new \Magento\Framework\DataObject($tab); } elseif ($tab instanceof \Magento\Framework\DataObject) { @@ -126,6 +129,7 @@ public function addTab($tabId, $tab) } } elseif (is_string($tab)) { $this->_addTabByName($tab, $tabId); + if (!$this->_tabs[$tabId] instanceof TabInterface) { unset($this->_tabs[$tabId]); return $this; @@ -133,6 +137,7 @@ public function addTab($tabId, $tab) } else { throw new \Exception(__('Please correct the tab configuration and try again.')); } + if ($this->_tabs[$tabId]->getUrl() === null) { $this->_tabs[$tabId]->setUrl('#'); } @@ -143,10 +148,7 @@ public function addTab($tabId, $tab) $this->_tabs[$tabId]->setId($tabId); $this->_tabs[$tabId]->setTabId($tabId); - - if ($this->_activeTab === null) { - $this->_activeTab = $tabId; - } + if (true === $this->_tabs[$tabId]->getActive()) { $this->setActiveTab($tabId); } @@ -178,6 +180,8 @@ protected function _addTabByName($tab, $tabId) } /** + * Get active tab id + * * @return string */ public function getActiveTabId() @@ -187,6 +191,7 @@ public function getActiveTabId() /** * Set Active Tab + * * Tab has to be not hidden and can show * * @param string $tabId @@ -231,38 +236,117 @@ protected function _setActiveTab($tabId) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _beforeToHtml() { + $this->_tabs = $this->reorderTabs(); + if ($activeTab = $this->getRequest()->getParam('active_tab')) { $this->setActiveTab($activeTab); } elseif ($activeTabId = $this->_authSession->getActiveTabId()) { $this->_setActiveTab($activeTabId); } - $_new = []; + if ($this->_activeTab === null && !empty($this->_tabs)) { + /** @var TabInterface $tab */ + $this->_activeTab = (reset($this->_tabs))->getId(); + } + + $this->assign('tabs', $this->_tabs); + return parent::_beforeToHtml(); + } + + /** + * Reorder the tabs. + * + * @return array + */ + private function reorderTabs() + { + $orderByIdentity = []; + $orderByPosition = []; + $position = 100; + + /** + * Set the initial positions for each tab. + * + * @var string $key + * @var TabInterface $tab + */ foreach ($this->_tabs as $key => $tab) { - foreach ($this->_tabs as $k => $t) { - if ($t->getAfter() == $key) { - $_new[$key] = $tab; - $_new[$k] = $t; - } else { - if (!$tab->getAfter() || !in_array($tab->getAfter(), array_keys($this->_tabs))) { - $_new[$key] = $tab; - } - } - } + $tab->setPosition($position); + + $orderByIdentity[$key] = $tab; + $orderByPosition[$position] = $tab; + + $position += 100; } - $this->_tabs = $_new; - unset($_new); + return $this->applyTabsCorrectOrder($orderByPosition, $orderByIdentity); + } - $this->assign('tabs', $this->_tabs); - return parent::_beforeToHtml(); + /** + * Apply tabs order + * + * @param array $orderByPosition + * @param array $orderByIdentity + * + * @return array + */ + private function applyTabsCorrectOrder(array $orderByPosition, array $orderByIdentity) + { + $positionFactor = 1; + + /** + * Rearrange the positions by using the after tag for each tab. + * + * @var int $position + * @var TabInterface $tab + */ + foreach ($orderByPosition as $position => $tab) { + if (!$tab->getAfter() || !in_array($tab->getAfter(), array_keys($orderByIdentity))) { + $positionFactor = 1; + continue; + } + + $grandPosition = $orderByIdentity[$tab->getAfter()]->getPosition(); + $newPosition = $grandPosition + $positionFactor; + + unset($orderByPosition[$position]); + $orderByPosition[$newPosition] = $tab; + $tab->setPosition($newPosition); + + $positionFactor++; + } + + return $this->finalTabsSortOrder($orderByPosition); } /** + * Apply the last sort order to tabs. + * + * @param array $orderByPosition + * + * @return array + */ + private function finalTabsSortOrder(array $orderByPosition) + { + ksort($orderByPosition); + + $ordered = []; + + /** @var TabInterface $tab */ + foreach ($orderByPosition as $tab) { + $ordered[$tab->getId()] = $tab; + } + + return $ordered; + } + + /** + * Get js object name + * * @return string */ public function getJsObjectName() @@ -271,6 +355,8 @@ public function getJsObjectName() } /** + * Get tabs ids + * * @return string[] */ public function getTabsIds() @@ -283,6 +369,8 @@ public function getTabsIds() } /** + * Get tab id + * * @param \Magento\Framework\DataObject|TabInterface $tab * @param bool $withPrefix * @return string @@ -296,6 +384,8 @@ public function getTabId($tab, $withPrefix = true) } /** + * CVan show tab + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return bool */ @@ -308,6 +398,8 @@ public function canShowTab($tab) } /** + * Get tab is hidden + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) @@ -321,6 +413,8 @@ public function getTabIsHidden($tab) } /** + * Get tab url + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -339,6 +433,8 @@ public function getTabUrl($tab) } /** + * Get tab title + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -351,6 +447,8 @@ public function getTabTitle($tab) } /** + * Get tab class + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -366,6 +464,8 @@ public function getTabClass($tab) } /** + * Get tab label + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -378,6 +478,8 @@ public function getTabLabel($tab) } /** + * Get tab content + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -393,7 +495,8 @@ public function getTabContent($tab) } /** - * Mark tabs as dependant of each other + * Mark tabs as dependent of each other + * * Arbitrary number of tabs can be specified, but at least two * * @param string $tabOneId diff --git a/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php b/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php index 70b01046f6afe..11da740c46606 100644 --- a/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php +++ b/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php @@ -11,13 +11,15 @@ use Symfony\Component\Console\Input\InputOption; /** + * Abstract cache command + * * @api * @since 100.0.2 */ abstract class AbstractCacheCommand extends Command { /** - * Input option bootsrap + * Input option bootstrap */ const INPUT_KEY_BOOTSTRAP = 'bootstrap'; @@ -40,7 +42,7 @@ public function __construct(Manager $cacheManager) } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php index ad4546097768a..23731e29f0df4 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Auth; -class DeniedJson extends \Magento\Backend\Controller\Adminhtml\Auth +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class DeniedJson extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php index b34e4d9c84939..1de77c810f316 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php @@ -6,11 +6,14 @@ */ namespace Magento\Backend\Controller\Adminhtml\Auth; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; + /** * @api * @since 100.0.2 */ -class Login extends \Magento\Backend\Controller\Adminhtml\Auth +class Login extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGet, HttpPost { /** * @var \Magento\Framework\View\Result\PageFactory @@ -50,9 +53,8 @@ public function execute() // redirect according to rewrite rule if ($requestUrl != $backendUrl) { return $this->getRedirect($backendUrl); - } else { - return $this->resultPageFactory->create(); } + return $this->resultPageFactory->create(); } /** diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php index 41e32c929287a..d7ad080395e29 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php @@ -6,7 +6,10 @@ */ namespace Magento\Backend\Controller\Adminhtml\Auth; -class Logout extends \Magento\Backend\Controller\Adminhtml\Auth +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; + +class Logout extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGet, HttpPost { /** * Administrator logout action @@ -16,7 +19,7 @@ class Logout extends \Magento\Backend\Controller\Adminhtml\Auth public function execute() { $this->_auth->logout(); - $this->messageManager->addSuccess(__('You have logged out.')); + $this->messageManager->addSuccessMessage(__('You have logged out.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache.php index daaa2a9aaeced..4fcd5993fb504 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Controller\Adminhtml; use Magento\Backend\App\Action; @@ -73,7 +74,7 @@ protected function _validateTypes(array $types) $allTypes = array_keys($this->_cacheTypeList->getTypes()); $invalidTypes = array_diff($types, $allTypes); if (count($invalidTypes) > 0) { - throw new LocalizedException(__('Specified cache type(s) don\'t exist: %1', join(', ', $invalidTypes))); + throw new LocalizedException(__('These cache type(s) don\'t exist: %1', join(', ', $invalidTypes))); } } } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php index 1895bd08c464f..79bc19256d270 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php @@ -6,11 +6,19 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; -class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache +class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::flush_catalog_images'; + /** * Clean JS/css files cache * @@ -21,11 +29,11 @@ public function execute() try { $this->_objectManager->create(\Magento\Catalog\Model\Product\Image::class)->clearCache(); $this->_eventManager->dispatch('clean_catalog_images_cache_after'); - $this->messageManager->addSuccess(__('The image cache was cleaned.')); + $this->messageManager->addSuccessMessage(__('The image cache was cleaned.')); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('An error occurred while clearing the image cache.')); + $this->messageManager->addExceptionMessage($e, __('An error occurred while clearing the image cache.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php index 521cb9806a135..36aca1afcc480 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php @@ -6,11 +6,19 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; -class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache +class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::flush_js_css'; + /** * Clean JS/css files cache * @@ -21,11 +29,12 @@ public function execute() try { $this->_objectManager->get(\Magento\Framework\View\Asset\MergeService::class)->cleanMergedJsCss(); $this->_eventManager->dispatch('clean_media_cache_after'); - $this->messageManager->addSuccess(__('The JavaScript/CSS cache has been cleaned.')); + $this->messageManager->addSuccessMessage(__('The JavaScript/CSS cache has been cleaned.')); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('An error occurred while clearing the JavaScript/CSS cache.')); + $this->messageManager + ->addExceptionMessage($e, __('An error occurred while clearing the JavaScript/CSS cache.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php index adfbf7cfe63c8..a3a26c5cf6242 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php @@ -6,10 +6,18 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache +class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::flush_static_files'; + /** * Clean static files cache * @@ -19,7 +27,7 @@ public function execute() { $this->_objectManager->get(\Magento\Framework\App\State\CleanupFiles::class)->clearMaterializedViewFiles(); $this->_eventManager->dispatch('clean_static_files_cache_after'); - $this->messageManager->addSuccess(__('The static files cache has been cleaned.')); + $this->messageManager->addSuccessMessage(__('The static files cache has been cleaned.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php index 2f9dc9ad6a7a5..daf424d14c55b 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php @@ -6,8 +6,17 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; -class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::flush_cache_storage'; + /** * Flush cache storage * @@ -20,7 +29,7 @@ public function execute() foreach ($this->_cacheFrontendPool as $cacheFrontend) { $cacheFrontend->getBackend()->clean(); } - $this->messageManager->addSuccess(__("You flushed the cache storage.")); + $this->messageManager->addSuccessMessage(__("You flushed the cache storage.")); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('adminhtml/*'); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php index 0f55a59353d65..f3474bf43872b 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php @@ -6,8 +6,17 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; -class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::flush_magento_cache'; + /** * Flush all magento cache * @@ -20,7 +29,7 @@ public function execute() $cacheFrontend->clean(); } $this->_eventManager->dispatch('adminhtml_cache_flush_system'); - $this->messageManager->addSuccess(__("The Magento cache storage has been flushed.")); + $this->messageManager->addSuccessMessage(__("The Magento cache storage has been flushed.")); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('adminhtml/*'); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php index 05bd309ca620e..f1e908bb842ee 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; -class Index extends \Magento\Backend\Controller\Adminhtml\Cache +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Display cache management grid diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php index 204105852b9f1..03b88ca1d3f47 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php @@ -16,6 +16,13 @@ */ class MassDisable extends \Magento\Backend\Controller\Adminhtml\Cache { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type'; + /** * @var State */ @@ -60,12 +67,12 @@ private function disableCache() } if ($updatedTypes > 0) { $this->_cacheState->persist(); - $this->messageManager->addSuccess(__("%1 cache type(s) disabled.", $updatedTypes)); + $this->messageManager->addSuccessMessage(__("%1 cache type(s) disabled.", $updatedTypes)); } } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('An error occurred while disabling cache.')); + $this->messageManager->addExceptionMessage($e, __('An error occurred while disabling cache.')); } } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php index 32acf47887c44..1b98a00d4bf35 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php @@ -16,6 +16,13 @@ */ class MassEnable extends \Magento\Backend\Controller\Adminhtml\Cache { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type'; + /** * @var State */ @@ -59,12 +66,12 @@ private function enableCache() } if ($updatedTypes > 0) { $this->_cacheState->persist(); - $this->messageManager->addSuccess(__("%1 cache type(s) enabled.", $updatedTypes)); + $this->messageManager->addSuccessMessage(__("%1 cache type(s) enabled.", $updatedTypes)); } } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('An error occurred while enabling cache.')); + $this->messageManager->addExceptionMessage($e, __('An error occurred while enabling cache.')); } } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php index e18aa1555e11b..bde211debcf72 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php @@ -11,6 +11,13 @@ class MassRefresh extends \Magento\Backend\Controller\Adminhtml\Cache { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::refresh_cache_type'; + /** * Mass action for cache refresh * @@ -30,12 +37,12 @@ public function execute() $updatedTypes++; } if ($updatedTypes > 0) { - $this->messageManager->addSuccess(__("%1 cache type(s) refreshed.", $updatedTypes)); + $this->messageManager->addSuccessMessage(__("%1 cache type(s) refreshed.", $updatedTypes)); } } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('An error occurred while refreshing cache.')); + $this->messageManager->addExceptionMessage($e, __('An error occurred while refreshing cache.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php index d8c52f6c50bba..decca6837fa00 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php @@ -6,7 +6,11 @@ */ namespace Magento\Backend\Controller\Adminhtml\Dashboard; -class Index extends \Magento\Backend\Controller\Adminhtml\Dashboard +use Magento\Backend\Controller\Adminhtml\Dashboard as DashboardAction; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Index extends DashboardAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php index f831fa67f4bb0..c10d1a77997b7 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php @@ -34,9 +34,9 @@ public function execute() foreach ($collectionsNames as $collectionName) { $this->_objectManager->create($collectionName)->aggregate(); } - $this->messageManager->addSuccess(__('We updated lifetime statistic.')); + $this->messageManager->addSuccessMessage(__('We updated lifetime statistic.')); } catch (\Exception $e) { - $this->messageManager->addError(__('We can\'t refresh lifetime statistics.')); + $this->messageManager->addErrorMessage(__('We can\'t refresh lifetime statistics.')); $this->logger->critical($e); } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php b/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php index 9ca4021d08356..37f3064aeaa38 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php @@ -6,11 +6,15 @@ */ namespace Magento\Backend\Controller\Adminhtml\Index; +use Magento\Backend\Controller\Adminhtml\Index as IndexAction; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + /** * @api * @since 100.0.2 */ -class GlobalSearch extends \Magento\Backend\Controller\Adminhtml\Index +class GlobalSearch extends IndexAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php index a5c71fb2dbc5c..afb1b4573271d 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php @@ -6,7 +6,10 @@ */ namespace Magento\Backend\Controller\Adminhtml\Index; -class Index extends \Magento\Backend\Controller\Adminhtml\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; + +class Index extends \Magento\Backend\Controller\Adminhtml\Index implements HttpGet, HttpPost { /** * Admin area entry point diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php index e8251b5be6030..e84987d8e1d70 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php @@ -6,6 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Noroute; +/** + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ class Index extends \Magento\Backend\App\Action { /** @@ -34,7 +37,7 @@ public function execute() { /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); - $resultPage->setStatusHeader(404, '1.1', 'Forbidden'); + $resultPage->setStatusHeader(404, '1.1', 'Not Found'); $resultPage->setHeader('Status', '404 File not found'); $resultPage->addHandle('adminhtml_noroute'); return $resultPage; diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php index 54771bfdc1a7d..648f1be86f56c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Account; -class Index extends \Magento\Backend\Controller\Adminhtml\System\Account +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backend\Controller\Adminhtml\System\Account implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php index c9bce1cbf3888..d95b0541c2c76 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php @@ -32,9 +32,8 @@ private function getSecurityCookie() { if (!($this->securityCookie instanceof SecurityCookie)) { return \Magento\Framework\App\ObjectManager::getInstance()->get(SecurityCookie::class); - } else { - return $this->securityCookie; } + return $this->securityCookie; } /** @@ -54,9 +53,9 @@ public function execute() $user = $this->_objectManager->create(\Magento\User\Model\User::class)->load($userId); $user->setId($userId) - ->setUsername($this->getRequest()->getParam('username', false)) - ->setFirstname($this->getRequest()->getParam('firstname', false)) - ->setLastname($this->getRequest()->getParam('lastname', false)) + ->setUserName($this->getRequest()->getParam('username', false)) + ->setFirstName($this->getRequest()->getParam('firstname', false)) + ->setLastName($this->getRequest()->getParam('lastname', false)) ->setEmail(strtolower($this->getRequest()->getParam('email', false))); if ($this->_objectManager->get(\Magento\Framework\Validator\Locale::class)->isValid($interfaceLocale)) { @@ -77,12 +76,12 @@ public function execute() $errors = $user->validate(); if ($errors !== true && !empty($errors)) { foreach ($errors as $error) { - $this->messageManager->addError($error); + $this->messageManager->addErrorMessage($error); } } else { $user->save(); $user->sendNotificationEmailsIfRequired(); - $this->messageManager->addSuccess(__('You saved the account.')); + $this->messageManager->addSuccessMessage(__('You saved the account.')); } } catch (UserLockedException $e) { $this->_auth->logout(); @@ -92,12 +91,12 @@ public function execute() } catch (ValidatorException $e) { $this->messageManager->addMessages($e->getMessages()); if ($e->getMessage()) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('An error occurred while saving account.')); + $this->messageManager->addErrorMessage(__('An error occurred while saving account.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php index 76402169f269e..21f28188cf874 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php @@ -19,11 +19,11 @@ public function execute() try { $design->delete(); - $this->messageManager->addSuccess(__('You deleted the design change.')); + $this->messageManager->addSuccessMessage(__('You deleted the design change.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __("You can't delete the design change.")); + $this->messageManager->addExceptionMessage($e, __("You can't delete the design change.")); } } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php index 30b26f2294193..c6a05b5a71d0c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Design; -class Index extends \Magento\Backend\Controller\Adminhtml\System\Design +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php index 1f478604ced7d..0228b48f7f11e 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php @@ -50,9 +50,9 @@ public function execute() try { $design->save(); $this->_eventManager->dispatch('theme_save_after'); - $this->messageManager->addSuccess(__('You saved the design change.')); + $this->messageManager->addSuccessMessage(__('You saved the design change.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setDesignData($data); return $resultRedirect->setPath('adminhtml/*/', ['id' => $design->getId()]); } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php index 4fbae6abb423a..a9be14b77b29c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php @@ -14,6 +14,7 @@ * Store controller * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.AllPurposeAction) */ abstract class Store extends Action { @@ -86,6 +87,8 @@ protected function createPage() * Backup database * * @return bool + * + * @deprecated Backup module is to be removed. */ protected function _backupDatabase() { @@ -103,12 +106,12 @@ protected function _backupDatabase() ->setType('db') ->setPath($filesystem->getDirectoryRead(DirectoryList::VAR_DIR)->getAbsolutePath('backups')); $backupDb->createBackup($backup); - $this->messageManager->addSuccess(__('The database was backed up.')); + $this->messageManager->addSuccessMessage(__('The database was backed up.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); return false; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('We can\'t create a backup right now. Please try again later.') ); @@ -125,7 +128,7 @@ protected function _backupDatabase() */ protected function _addDeletionNotice($typeTitle) { - $this->messageManager->addNotice( + $this->messageManager->addNoticeMessage( __( 'Deleting a %1 will not delete the information associated with the %1 (e.g. categories, products, etc.)' . ', but the %1 will not be able to be restored. It is suggested that you create a database backup ' diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php index 925ae4c69ee8e..4e323be709ae1 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php @@ -15,13 +15,13 @@ public function execute() { $itemId = $this->getRequest()->getParam('item_id', null); if (!($model = $this->_objectManager->create(\Magento\Store\Model\Group::class)->load($itemId))) { - $this->messageManager->addError(__('Something went wrong. Please try again.')); + $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultRedirectFactory->create(); return $redirectResult->setPath('adminhtml/*/'); } if (!$model->isCanDelete()) { - $this->messageManager->addError(__('This store cannot be deleted.')); + $this->messageManager->addErrorMessage(__('This store cannot be deleted.')); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultRedirectFactory->create(); return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $itemId]); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php index b6fbd88c7669c..49c327060dae5 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php @@ -1,16 +1,20 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Backend\Controller\Adminhtml\System\Store; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class DeleteGroupPost extends \Magento\Backend\Controller\Adminhtml\System\Store +/** + * Delete store. + */ +class DeleteGroupPost extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpPostActionInterface { /** + * @inheritDoc * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -21,11 +25,11 @@ public function execute() $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (!($model = $this->_objectManager->create(\Magento\Store\Model\Group::class)->load($itemId))) { - $this->messageManager->addError(__('Something went wrong. Please try again.')); + $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); return $redirectResult->setPath('adminhtml/*/'); } if (!$model->isCanDelete()) { - $this->messageManager->addError(__('This store cannot be deleted.')); + $this->messageManager->addErrorMessage(__('This store cannot be deleted.')); return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $model->getId()]); } @@ -35,12 +39,12 @@ public function execute() try { $model->delete(); - $this->messageManager->addSuccess(__('You deleted the store.')); + $this->messageManager->addSuccessMessage(__('You deleted the store.')); return $redirectResult->setPath('adminhtml/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Unable to delete the store. Please try again later.')); + $this->messageManager->addExceptionMessage($e, __('Unable to delete the store. Please try again later.')); } return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $itemId]); } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php index b31de6cacc5ff..c340b1ec53aa5 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php @@ -15,13 +15,13 @@ public function execute() { $itemId = $this->getRequest()->getParam('item_id', null); if (!($model = $this->_objectManager->create(\Magento\Store\Model\Store::class)->load($itemId))) { - $this->messageManager->addError(__('Something went wrong. Please try again.')); + $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultRedirectFactory->create(); return $redirectResult->setPath('adminhtml/*/'); } if (!$model->isCanDelete()) { - $this->messageManager->addError(__('This store view cannot be deleted.')); + $this->messageManager->addErrorMessage(__('This store view cannot be deleted.')); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultRedirectFactory->create(); return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $itemId]); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php index ac470238e588f..7999012b43594 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php @@ -1,14 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Backend\Controller\Adminhtml\System\Store; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class DeleteStorePost extends \Magento\Backend\Controller\Adminhtml\System\Store +/** + * Delete store view. + */ +class DeleteStorePost extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpPostActionInterface { /** * Delete store view post action @@ -22,11 +25,11 @@ public function execute() /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (!($model = $this->_objectManager->create(\Magento\Store\Model\Store::class)->load($itemId))) { - $this->messageManager->addError(__('Something went wrong. Please try again.')); + $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); return $redirectResult->setPath('adminhtml/*/'); } if (!$model->isCanDelete()) { - $this->messageManager->addError(__('This store view cannot be deleted.')); + $this->messageManager->addErrorMessage(__('This store view cannot be deleted.')); return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $model->getId()]); } @@ -37,14 +40,13 @@ public function execute() try { $model->delete(); - $this->_eventManager->dispatch('store_delete', ['store' => $model]); - - $this->messageManager->addSuccess(__('You deleted the store view.')); + $this->messageManager->addSuccessMessage(__('You deleted the store view.')); return $redirectResult->setPath('adminhtml/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Unable to delete the store view. Please try again later.')); + $this->messageManager + ->addExceptionMessage($e, __('Unable to delete the store view. Please try again later.')); } return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $itemId]); } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php index 1f2ec4b2ba4b1..9e8664b8ecd11 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Store; -class DeleteWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class DeleteWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface { /** * @return \Magento\Framework\Controller\ResultInterface @@ -15,13 +17,13 @@ public function execute() { $itemId = $this->getRequest()->getParam('item_id', null); if (!($model = $this->_objectManager->create(\Magento\Store\Model\Website::class)->load($itemId))) { - $this->messageManager->addError(__('Something went wrong. Please try again.')); + $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultRedirectFactory->create(); return $redirectResult->setPath('adminhtml/*/'); } if (!$model->isCanDelete()) { - $this->messageManager->addError(__('This website cannot be deleted.')); + $this->messageManager->addErrorMessage(__('This website cannot be deleted.')); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $redirectResult = $this->resultRedirectFactory->create(); return $redirectResult->setPath('adminhtml/*/editWebsite', ['website_id' => $itemId]); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php index c2d24b8c41a8c..3fee1a25c9fe4 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php @@ -6,11 +6,16 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Store; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Store +/** + * Delete website. + */ +class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpPostActionInterface { /** + * @inheritDoc * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -23,11 +28,11 @@ public function execute() $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (!$model) { - $this->messageManager->addError(__('Something went wrong. Please try again.')); + $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.')); return $redirectResult->setPath('adminhtml/*/'); } if (!$model->isCanDelete()) { - $this->messageManager->addError(__('This website cannot be deleted.')); + $this->messageManager->addErrorMessage(__('This website cannot be deleted.')); return $redirectResult->setPath('adminhtml/*/editWebsite', ['website_id' => $model->getId()]); } @@ -37,12 +42,12 @@ public function execute() try { $model->delete(); - $this->messageManager->addSuccess(__('You deleted the website.')); + $this->messageManager->addSuccessMessage(__('You deleted the website.')); return $redirectResult->setPath('adminhtml/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Unable to delete the website. Please try again later.')); + $this->messageManager->addExceptionMessage($e, __('Unable to delete the website. Please try again later.')); } return $redirectResult->setPath('*/*/editWebsite', ['website_id' => $itemId]); } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php index cbc068a480865..e5cd43b521fd1 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Store; -class EditStore extends \Magento\Backend\Controller\Adminhtml\System\Store +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class EditStore extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface { /** * @return \Magento\Framework\Controller\ResultInterface @@ -57,7 +59,7 @@ public function execute() if ($model->getId() || $this->_coreRegistry->registry('store_action') == 'add') { $this->_coreRegistry->register('store_data', $model); if ($this->_coreRegistry->registry('store_action') == 'edit' && $codeBase && !$model->isReadOnly()) { - $this->messageManager->addNotice($codeBase); + $this->messageManager->addNoticeMessage($codeBase); } $resultPage = $this->createPage(); if ($this->_coreRegistry->registry('store_action') == 'add') { @@ -71,7 +73,7 @@ public function execute() )); return $resultPage; } else { - $this->messageManager->addError($notExists); + $this->messageManager->addErrorMessage($notExists); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('adminhtml/*/'); diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php index bfdf7cdbeb8fb..74ed7951e6214 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditWebsite.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Store; -class EditWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class EditWebsite extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Forward diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php index b104704f41bdb..54da065c4af91 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Index.php @@ -6,12 +6,13 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Store; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; /** * Class Index returns Stores page */ -class Index extends \Magento\Backend\Controller\Adminhtml\System\Store +class Index extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpGetActionInterface { /** * Returns Stores page diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php index 1d6862a6ff845..b67f1f23f16ba 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php @@ -6,12 +6,14 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Store; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + /** * Class Save * * Save controller for system entities such as: Store, StoreGroup, Website */ -class Save extends \Magento\Backend\Controller\Adminhtml\System\Store +class Save extends \Magento\Backend\Controller\Adminhtml\System\Store implements HttpPostActionInterface { /** * Process Website model save @@ -32,7 +34,7 @@ private function processWebsiteSave($postData) } $websiteModel->save(); - $this->messageManager->addSuccess(__('You saved the website.')); + $this->messageManager->addSuccessMessage(__('You saved the website.')); return $postData; } @@ -46,7 +48,6 @@ private function processWebsiteSave($postData) */ private function processStoreSave($postData) { - $eventName = 'store_edit'; /** @var \Magento\Store\Model\Store $storeModel */ $storeModel = $this->_objectManager->create(\Magento\Store\Model\Store::class); $postData['store']['name'] = $this->filterManager->removeTags($postData['store']['name']); @@ -56,7 +57,6 @@ private function processStoreSave($postData) $storeModel->setData($postData['store']); if ($postData['store']['store_id'] == '') { $storeModel->setId(null); - $eventName = 'store_add'; } $groupModel = $this->_objectManager->create( \Magento\Store\Model\Group::class @@ -70,9 +70,7 @@ private function processStoreSave($postData) ); } $storeModel->save(); - $this->_objectManager->get(\Magento\Store\Model\StoreManager::class)->reinitStores(); - $this->_eventManager->dispatch($eventName, ['store' => $storeModel]); - $this->messageManager->addSuccess(__('You saved the store view.')); + $this->messageManager->addSuccessMessage(__('You saved the store view.')); return $postData; } @@ -102,8 +100,7 @@ private function processGroupSave($postData) ); } $groupModel->save(); - $this->_eventManager->dispatch('store_group_save', ['group' => $groupModel]); - $this->messageManager->addSuccess(__('You saved the store.')); + $this->messageManager->addSuccessMessage(__('You saved the store.')); return $postData; } @@ -139,10 +136,10 @@ public function execute() $redirectResult->setPath('adminhtml/*/'); return $redirectResult; } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_getSession()->setPostData($postData); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while saving. Please review the error log.') ); diff --git a/app/code/Magento/Backend/Helper/Dashboard/Order.php b/app/code/Magento/Backend/Helper/Dashboard/Order.php index 9fc2c2cdb4e6f..c19c28b6e33eb 100644 --- a/app/code/Magento/Backend/Helper/Dashboard/Order.php +++ b/app/code/Magento/Backend/Helper/Dashboard/Order.php @@ -13,7 +13,7 @@ * @api * @since 100.0.2 */ -class Order extends \Magento\Backend\Helper\Dashboard\AbstractDashboard +class Order extends AbstractDashboard { /** * @var \Magento\Reports\Model\ResourceModel\Order\Collection @@ -29,32 +29,25 @@ class Order extends \Magento\Backend\Helper\Dashboard\AbstractDashboard /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Reports\Model\ResourceModel\Order\Collection $orderCollection + * @param \Magento\Store\Model\StoreManagerInterface $storeManager */ public function __construct( \Magento\Framework\App\Helper\Context $context, - \Magento\Reports\Model\ResourceModel\Order\Collection $orderCollection + \Magento\Reports\Model\ResourceModel\Order\Collection $orderCollection, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { $this->_orderCollection = $orderCollection; - parent::__construct($context); - } + $this->_storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(\Magento\Store\Model\StoreManagerInterface::class); - /** - * The getter function to get the new StoreManager dependency - * - * @return \Magento\Store\Model\StoreManagerInterface - * - * @deprecated 100.1.0 - */ - private function getStoreManager() - { - if ($this->_storeManager === null) { - $this->_storeManager = ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManagerInterface::class); - } - return $this->_storeManager; + parent::__construct($context); } /** * @return void + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _initCollection() { @@ -65,15 +58,15 @@ protected function _initCollection() if ($this->getParam('store')) { $this->_collection->addFieldToFilter('store_id', $this->getParam('store')); } elseif ($this->getParam('website')) { - $storeIds = $this->getStoreManager()->getWebsite($this->getParam('website'))->getStoreIds(); + $storeIds = $this->_storeManager->getWebsite($this->getParam('website'))->getStoreIds(); $this->_collection->addFieldToFilter('store_id', ['in' => implode(',', $storeIds)]); } elseif ($this->getParam('group')) { - $storeIds = $this->getStoreManager()->getGroup($this->getParam('group'))->getStoreIds(); + $storeIds = $this->_storeManager->getGroup($this->getParam('group'))->getStoreIds(); $this->_collection->addFieldToFilter('store_id', ['in' => implode(',', $storeIds)]); } elseif (!$this->_collection->isLive()) { $this->_collection->addFieldToFilter( 'store_id', - ['eq' => $this->getStoreManager()->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId()] + ['eq' => $this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId()] ); } $this->_collection->load(); diff --git a/app/code/Magento/Backend/Model/AdminPathConfig.php b/app/code/Magento/Backend/Model/AdminPathConfig.php index e7338adca4a2a..0e77835a5134c 100644 --- a/app/code/Magento/Backend/Model/AdminPathConfig.php +++ b/app/code/Magento/Backend/Model/AdminPathConfig.php @@ -48,10 +48,7 @@ public function __construct( } /** - * {@inheritdoc} - * - * @param \Magento\Framework\App\RequestInterface $request - * @return string + * @inheritdoc */ public function getCurrentSecureUrl(\Magento\Framework\App\RequestInterface $request) { @@ -59,28 +56,29 @@ public function getCurrentSecureUrl(\Magento\Framework\App\RequestInterface $req } /** - * {@inheritdoc} - * - * @param string $path - * @return bool + * @inheritdoc */ public function shouldBeSecure($path) { - return parse_url( - (string)$this->coreConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, 'default'), - PHP_URL_SCHEME - ) === 'https' - || $this->backendConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML) - && parse_url( - (string)$this->coreConfig->getValue(Store::XML_PATH_SECURE_BASE_URL, 'default'), - PHP_URL_SCHEME - ) === 'https'; + $baseUrl = (string)$this->coreConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, 'default'); + if (parse_url($baseUrl, PHP_URL_SCHEME) === 'https') { + return true; + } + + if ($this->backendConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML)) { + if ($this->backendConfig->isSetFlag('admin/url/use_custom')) { + $adminBaseUrl = (string)$this->coreConfig->getValue('admin/url/custom', 'default'); + } else { + $adminBaseUrl = (string)$this->coreConfig->getValue(Store::XML_PATH_SECURE_BASE_URL, 'default'); + } + return parse_url($adminBaseUrl, PHP_URL_SCHEME) === 'https'; + } + + return false; } /** - * {@inheritdoc} - * - * @return string + * @inheritdoc */ public function getDefaultPath() { diff --git a/app/code/Magento/Backend/Model/Auth.php b/app/code/Magento/Backend/Model/Auth.php index 08921d1615f87..02bf64fef07ed 100644 --- a/app/code/Magento/Backend/Model/Auth.php +++ b/app/code/Magento/Backend/Model/Auth.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model; use Magento\Framework\Exception\AuthenticationException; @@ -148,7 +149,12 @@ public function getCredentialStorage() public function login($username, $password) { if (empty($username) || empty($password)) { - self::throwException(__('You did not sign in correctly or your account is temporarily disabled.')); + self::throwException( + __( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) + ); } try { @@ -165,7 +171,12 @@ public function login($username, $password) } if (!$this->getAuthStorage()->getUser()) { - self::throwException(__('You did not sign in correctly or your account is temporarily disabled.')); + self::throwException( + __( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) + ); } } catch (PluginAuthenticationException $e) { $this->_eventManager->dispatch( @@ -179,7 +190,10 @@ public function login($username, $password) ['user_name' => $username, 'exception' => $e] ); self::throwException( - __($e->getMessage()? : 'You did not sign in correctly or your account is temporarily disabled.') + __( + $e->getMessage()? : 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) ); } } @@ -215,7 +229,7 @@ public function isLoggedIn() public static function throwException(Phrase $msg = null) { if ($msg === null) { - $msg = __('Authentication error occurred.'); + $msg = __('An authentication error occurred. Verify and try again.'); } throw new AuthenticationException($msg); } diff --git a/app/code/Magento/Backend/Model/Auth/StorageInterface.php b/app/code/Magento/Backend/Model/Auth/StorageInterface.php index 52b2b089c71e1..e643165a93317 100644 --- a/app/code/Magento/Backend/Model/Auth/StorageInterface.php +++ b/app/code/Magento/Backend/Model/Auth/StorageInterface.php @@ -23,7 +23,7 @@ interface StorageInterface public function processLogin(); /** - * Perform login specific actions + * Perform logout specific actions * * @return $this * @abstract diff --git a/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php b/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php index 09f33abd0d44d..f6d08883d7a6f 100644 --- a/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php +++ b/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model\Config\SessionLifetime; use Magento\Framework\App\Config\Value; @@ -15,25 +16,31 @@ */ class BackendModel extends Value { - /** Maximum dmin session lifetime; 1 year*/ + /** Maximum admin session lifetime; 1 year*/ const MAX_LIFETIME = 31536000; /** Minimum admin session lifetime */ const MIN_LIFETIME = 60; /** + * Processing object before save data + * * @since 100.1.0 + * @throws LocalizedException */ public function beforeSave() { - $value = (int) $this->getValue(); + $value = (int)$this->getValue(); if ($value > self::MAX_LIFETIME) { throw new LocalizedException( - __('Admin session lifetime must be less than or equal to 31536000 seconds (one year)') + __( + 'The Admin session lifetime is invalid. ' + . 'Set the lifetime to 31536000 seconds (one year) or shorter and try again.' + ) ); } elseif ($value < self::MIN_LIFETIME) { throw new LocalizedException( - __('Admin session lifetime must be greater than or equal to 60 seconds') + __('The Admin session lifetime is invalid. Set the lifetime to 60 seconds or longer and try again.') ); } return parent::beforeSave(); diff --git a/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php b/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php new file mode 100644 index 0000000000000..8155aa5e2fe2d --- /dev/null +++ b/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Model\Image; + +/** + * Image uploader config provider. + */ +class UploadResizeConfig implements UploadResizeConfigInterface +{ + /** + * Config path for the maximal image width value + */ + const XML_PATH_MAX_WIDTH_IMAGE = 'system/upload_configuration/max_width'; + + /** + * Config path for the maximal image height value + */ + const XML_PATH_MAX_HEIGHT_IMAGE = 'system/upload_configuration/max_height'; + + /** + * Config path for the maximal image height value + */ + const XML_PATH_ENABLE_RESIZE = 'system/upload_configuration/enable_resize'; + + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $config; + + /** + * @param \Magento\Framework\App\Config\ScopeConfigInterface $config + */ + public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $config) + { + $this->config = $config; + } + + /** + * Get maximal width value for resized image + * + * @return int + */ + public function getMaxWidth(): int + { + return (int)$this->config->getValue(self::XML_PATH_MAX_WIDTH_IMAGE); + } + + /** + * Get maximal height value for resized image + * + * @return int + */ + public function getMaxHeight(): int + { + return (int)$this->config->getValue(self::XML_PATH_MAX_HEIGHT_IMAGE); + } + + /** + * Get config value for frontend resize + * + * @return bool + */ + public function isResizeEnabled(): bool + { + return (bool)$this->config->getValue(self::XML_PATH_ENABLE_RESIZE); + } +} diff --git a/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php b/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php new file mode 100644 index 0000000000000..50582dfafbcd1 --- /dev/null +++ b/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Model\Image; + +/** + * Interface UploadResizeConfigInterface + * + * Used to retrieve configuration for frontend image uploader + */ +interface UploadResizeConfigInterface +{ + /** + * Get maximal width value for resized image + * + * @return int + */ + public function getMaxWidth(): int; + + /** + * Get maximal height value for resized image + * + * @return int + */ + public function getMaxHeight(): int; + + /** + * Get config value for frontend resize + * + * @return bool + */ + public function isResizeEnabled(): bool; +} diff --git a/app/code/Magento/Backend/Model/Menu.php b/app/code/Magento/Backend/Model/Menu.php index 22110cf52de2b..0246346ec4d97 100644 --- a/app/code/Magento/Backend/Model/Menu.php +++ b/app/code/Magento/Backend/Model/Menu.php @@ -83,7 +83,7 @@ public function add(Item $item, $parentId = null, $index = null) } $parentItem->getChildren()->add($item, null, $index); } else { - $index = intval($index); + $index = (int) $index; if (!isset($this[$index])) { $this->offsetSet($index, $item); $this->_logger->info( diff --git a/app/code/Magento/Backend/Model/Menu/Builder.php b/app/code/Magento/Backend/Model/Menu/Builder.php index ae572deab53d9..1c6e900bc5cfc 100644 --- a/app/code/Magento/Backend/Model/Menu/Builder.php +++ b/app/code/Magento/Backend/Model/Menu/Builder.php @@ -102,6 +102,6 @@ public function getResult(\Magento\Backend\Model\Menu $menu) */ protected function _getParam($params, $paramName, $defaultValue = null) { - return isset($params[$paramName]) ? $params[$paramName] : $defaultValue; + return $params[$paramName] ?? $defaultValue; } } diff --git a/app/code/Magento/Backend/Model/Menu/Item.php b/app/code/Magento/Backend/Model/Menu/Item.php index 42febe94d0abf..67c6216cbbc06 100644 --- a/app/code/Magento/Backend/Model/Menu/Item.php +++ b/app/code/Magento/Backend/Model/Menu/Item.php @@ -503,12 +503,11 @@ public function populateFromArray(array $data) $this->_tooltip = $this->_getArgument($data, 'toolTip'); $this->_title = $this->_getArgument($data, 'title'); $this->target = $this->_getArgument($data, 'target'); + $this->_submenu = null; if (isset($data['sub_menu'])) { $menu = $this->_menuFactory->create(); $menu->populateFromArray($data['sub_menu']); $this->_submenu = $menu; - } else { - $this->_submenu = null; } } } diff --git a/app/code/Magento/Backend/Model/Menu/Item/Validator.php b/app/code/Magento/Backend/Model/Menu/Item/Validator.php index d79752b296e5f..7b72b355f551d 100644 --- a/app/code/Magento/Backend/Model/Menu/Item/Validator.php +++ b/app/code/Magento/Backend/Model/Menu/Item/Validator.php @@ -74,34 +74,86 @@ public function __construct() * @throws \BadMethodCallException */ public function validate($data) + { + if ($this->checkMenuItemIsRemoved($data)) { + return; + } + + $this->assertContainsRequiredParameters($data); + $this->assertIdentifierIsNotUsed($data['id']); + + foreach ($data as $param => $value) { + $this->validateMenuItemParameter($param, $value); + } + $this->_ids[] = $data['id']; + } + + /** + * Check that menu item is not deleted + * + * @param array $data + * @return bool + */ + private function checkMenuItemIsRemoved($data) + { + return isset($data['id'], $data['removed']) && $data['removed'] === true; + } + + /** + * Check that menu item contains all required data + * @param array $data + * + * @throws \BadMethodCallException + */ + private function assertContainsRequiredParameters($data) { foreach ($this->_required as $param) { if (!isset($data[$param])) { throw new \BadMethodCallException('Missing required param ' . $param); } } + } - if (array_search($data['id'], $this->_ids) !== false) { - throw new \InvalidArgumentException('Item with id ' . $data['id'] . ' already exists'); + /** + * Check that menu item id is not used + * + * @param string $id + * @throws \InvalidArgumentException + */ + private function assertIdentifierIsNotUsed($id) + { + if (array_search($id, $this->_ids) !== false) { + throw new \InvalidArgumentException('Item with id ' . $id . ' already exists'); } + } - foreach ($data as $param => $value) { - if ($data[$param] !== null - && isset( - $this->_validators[$param] - ) && !$this->_validators[$param]->isValid( - $value - ) - ) { - throw new \InvalidArgumentException( - "Param " . $param . " doesn't pass validation: " . implode( - '; ', - $this->_validators[$param]->getMessages() - ) - ); - } + /** + * Validate menu item parameter value + * + * @param string $param + * @param mixed $value + * @throws \InvalidArgumentException + */ + private function validateMenuItemParameter($param, $value) + { + if ($value === null) { + return; } - $this->_ids[] = $data['id']; + if (!isset($this->_validators[$param])) { + return; + } + + $validator = $this->_validators[$param]; + if ($validator->isValid($value)) { + return; + } + + throw new \InvalidArgumentException( + "Param " . $param . " doesn't pass validation: " . implode( + '; ', + $validator->getMessages() + ) + ); } /** diff --git a/app/code/Magento/Backend/Model/Search/Customer.php b/app/code/Magento/Backend/Model/Search/Customer.php index 35a7359ce9980..e76a1b77ab2d6 100644 --- a/app/code/Magento/Backend/Model/Search/Customer.php +++ b/app/code/Magento/Backend/Model/Search/Customer.php @@ -89,7 +89,7 @@ public function load() $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); $this->searchCriteriaBuilder->setPageSize($this->getLimit()); - $searchFields = ['firstname', 'lastname', 'company']; + $searchFields = ['firstname', 'lastname', 'billing_company']; $filters = []; foreach ($searchFields as $field) { $filters[] = $this->filterBuilder diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php index 48b443fe7ffd3..f199fd0fe7bf1 100644 --- a/app/code/Magento/Backend/Model/Url.php +++ b/app/code/Magento/Backend/Model/Url.php @@ -202,7 +202,7 @@ public function getUrl($routePath = null, $routeParams = null) } $cacheSecretKey = false; - if (is_array($routeParams) && isset($routeParams['_cache_secret_key'])) { + if (isset($routeParams['_cache_secret_key'])) { unset($routeParams['_cache_secret_key']); $cacheSecretKey = true; } @@ -210,25 +210,28 @@ public function getUrl($routePath = null, $routeParams = null) if (!$this->useSecretKey()) { return $result; } + + $this->getRouteParamsResolver()->unsetData('route_params'); $this->_setRoutePath($routePath); + $extraParams = $this->getRouteParamsResolver()->getRouteParams(); $routeName = $this->_getRouteName('*'); $controllerName = $this->_getControllerName(self::DEFAULT_CONTROLLER_NAME); $actionName = $this->_getActionName(self::DEFAULT_ACTION_NAME); - if ($cacheSecretKey) { - $secret = [self::SECRET_KEY_PARAM_NAME => "\${$routeName}/{$controllerName}/{$actionName}\$"]; - } else { - $secret = [ - self::SECRET_KEY_PARAM_NAME => $this->getSecretKey($routeName, $controllerName, $actionName), - ]; - } - if (is_array($routeParams)) { - $routeParams = array_merge($secret, $routeParams); - } else { - $routeParams = $secret; + + if (!isset($routeParams[self::SECRET_KEY_PARAM_NAME])) { + if (!is_array($routeParams)) { + $routeParams = []; + } + $secretKey = $cacheSecretKey + ? "\${$routeName}/{$controllerName}/{$actionName}\$" + : $this->getSecretKey($routeName, $controllerName, $actionName); + $routeParams[self::SECRET_KEY_PARAM_NAME] = $secretKey; } - if (is_array($this->_getRouteParams())) { - $routeParams = array_merge($this->_getRouteParams(), $routeParams); + + if (!empty($extraParams)) { + $routeParams = array_merge($extraParams, $routeParams); } + return parent::getUrl("{$routeName}/{$controllerName}/{$actionName}", $routeParams); } diff --git a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php index e2b77fb471acd..fbed7149aa565 100644 --- a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php +++ b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php @@ -30,8 +30,9 @@ public function parseExpression($expression) $expression = trim($expression); foreach ($this->_operations as $operation) { $splittedExpr = preg_split('/\\' . $operation . '/', $expression, -1, PREG_SPLIT_DELIM_CAPTURE); - if (count($splittedExpr) > 1) { - for ($i = 0; $i < count($splittedExpr); $i++) { + $count = count($splittedExpr); + if ($count > 1) { + for ($i = 0; $i < $count; $i++) { $stack = array_merge($stack, $this->parseExpression($splittedExpr[$i])); if ($i > 0) { $stack[] = $operation; diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml new file mode 100644 index 0000000000000..9ba4430bafe35 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginActionGroup"> + <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}" stepKey="navigateToAdmin"/> + <fillField userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" selector="{{AdminLoginFormSection.username}}" stepKey="fillUsername"/> + <fillField userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" selector="{{AdminLoginFormSection.password}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml new file mode 100644 index 0000000000000..1070bc409962a --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginAsAdmin"> + <arguments> + <argument name="adminUser" defaultValue="_ENV"/> + </arguments> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{adminUser.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{adminUser.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <closeAdminNotification stepKey="closeAdminNotification"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml new file mode 100644 index 0000000000000..a4d922086df34 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="logout"> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml new file mode 100644 index 0000000000000..6f27b03e4df30 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Action group to delete an item given that items name --> + <!-- Must already be on the admin page containing the grid --> + <actionGroup name="deleteEntitySecondaryGrid"> + <arguments> + <argument name="name" type="string"/> + <argument name="searchInput" type="string"/> + </arguments> + + <!-- search for the name --> + <click stepKey="resetFilters" selector="{{AdminSecondaryGridSection.resetFilters}}"/> + <fillField stepKey="fillIdentifier" selector="{{searchInput}}" userInput="{{name}}"/> + <click stepKey="searchForName" selector="{{AdminSecondaryGridSection.searchButton}}"/> + <click stepKey="clickResult" selector="{{AdminSecondaryGridSection.firstRow}}"/> + <waitForPageLoad stepKey="waitForTaxRateLoad"/> + + <!-- delete the rule --> + <click stepKey="clickDelete" selector="{{AdminStoresMainActionsSection.deleteButton}}"/> + <click stepKey="clickOk" selector="{{AdminConfirmationModalSection.ok}}"/> + <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="deleted"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml new file mode 100644 index 0000000000000..fd353964bae9a --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SortByIdDescendingActionGroup"> + <conditionalClick selector="//div[contains(@data-role, 'grid-wrapper')]/table/thead/tr/th/span[contains(text(), 'ID')]" dependentSelector="//span[contains(text(), 'ID')]/parent::th[not(contains(@class, '_descend'))]/parent::tr/parent::thead/parent::table/parent::div[contains(@data-role, 'grid-wrapper')]" stepKey="clickToAttemptSortByIdDescending" visible="true"/> + <waitForLoadingMaskToDisappear stepKey="waitForFirstIdSortDescendingToFinish" /> + <!-- Conditional Click again in case it goes from default state to ascending on first click --> + <conditionalClick selector="//div[contains(@data-role, 'grid-wrapper')]/table/thead/tr/th/span[contains(text(), 'ID')]" dependentSelector="//span[contains(text(), 'ID')]/parent::th[not(contains(@class, '_descend'))]/parent::tr/parent::thead/parent::table/parent::div[contains(@data-role, 'grid-wrapper')]" stepKey="secondClickToAttemptSortByIdDescending" visible="true"/> + <waitForLoadingMaskToDisappear stepKey="waitForSecondIdSortDescendingToFinish" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml b/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml new file mode 100644 index 0000000000000..016e936977cd0 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="backendDataOne" type="backend"> + <data key="backendConfigName">data</data> + </entity> +</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/BundleImportExport/LICENSE.txt b/app/code/Magento/Backend/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/BundleImportExport/LICENSE.txt rename to app/code/Magento/Backend/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/LICENSE_AFL.txt b/app/code/Magento/Backend/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Bundle/LICENSE_AFL.txt rename to app/code/Magento/Backend/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml new file mode 100644 index 0000000000000..8afc2c5bbb32f --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="ConfigurationStoresPage" url="admin/system_config/edit/section/cms/" area="admin" module="Catalog"> + <section name="WYSIWYGOptionsSection"/> + </page> + <page name="WebConfigurationPage" url="admin/system_config/edit/section/web/" area="admin" module="Backend"> + <section name="WYSIWYGOptionsSection"/> + </page> + <page name="GeneralConfigurationPage" url="admin/system_config/edit/section/general/" area="admin" module="Backend"> + <section name="LocaleOptionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml new file mode 100644 index 0000000000000..8c258accdf06c --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminDashboardPage" url="admin/dashboard/" area="admin" module="Magento_Backend"> + <section name="AdminMenuSection"/> + </page> +</pages> diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml new file mode 100644 index 0000000000000..b68b9914186f6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminLoginPage" url="admin" area="admin" module="Magento_Backend"> + <section name="AdminLoginFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml new file mode 100644 index 0000000000000..713199771e824 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLogoutPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminLogoutPage" url="admin/auth/logout/" area="admin" module="Magento_Backend"/> +</pages> diff --git a/app/code/Magento/Backend/Test/Mftf/README.md b/app/code/Magento/Backend/Test/Mftf/README.md new file mode 100644 index 0000000000000..ed8a3a3bc2c49 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Backend Functional Tests + +The Functional Test Module for **Magento Backend** module. diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml new file mode 100644 index 0000000000000..2ec25da461908 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminConfirmationModalSection"> + <element name="title" type="text" selector="aside.confirm .modal-title"/> + <element name="message" type="text" selector="aside.confirm .modal-content"/> + <element name="cancel" type="button" selector="aside.confirm .modal-footer button.action-dismiss" timeout="30"/> + <element name="ok" type="button" selector="aside.confirm .modal-footer button.action-accept" timeout="60"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml new file mode 100644 index 0000000000000..cc92e530cf3d4 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminGridTableSection"> + <element name="row" type="text" selector="table.data-grid tbody tr[data-role=row]:nth-of-type({{row}})" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml new file mode 100644 index 0000000000000..441ce886f117b --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminHeaderSection"> + <element name="pageTitle" type="text" selector=".page-header h1.page-title"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml new file mode 100644 index 0000000000000..3b10fac7bb9dc --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#username"/> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml new file mode 100644 index 0000000000000..bc576559e7a13 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMainActionsSection"> + <element name="save" type="button" selector="#save" timeout="30"/> + <element name="delete" type="button" selector="#delete" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml new file mode 100644 index 0000000000000..9e4a6d9219526 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMenuSection"> + <element name="catalog" type="button" selector="#menu-magento-catalog-catalog"/> + <element name="catalogProducts" type="button" selector="#nav li[data-ui-id='menu-magento-catalog-catalog-products']"/> + <element name="customers" type="button" selector="#menu-magento-customer-customer"/> + <element name="content" type="button" selector="#menu-magento-backend-content"/> + <element name="widgets" type="button" selector="#nav li[data-ui-id='menu-magento-widget-cms-widget-instance']"/> + <element name="stores" type="button" selector="#menu-magento-backend-stores"/> + <element name="configuration" type="button" selector="#nav li[data-ui-id='menu-magento-config-system-config']"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml new file mode 100644 index 0000000000000..b1350d5dcc1d7 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMessagesSection"> + <element name="success" type="text" selector="#messages div.message-success"/> + <element name="nthSuccess" type="text" selector=".message.message-success.success:nth-of-type({{n}})>div" parameterized="true"/> + <element name="error" type="text" selector="#messages div.message-error"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml new file mode 100644 index 0000000000000..9051eb747a7a6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSecondaryGridSection"> + <element name="resetFilters" type="button" selector="[title='Reset Filter']"/> + <element name="taxIdentifierSearch" type="input" selector=".col-code .admin__control-text"/> + <element name="catalogRuleIdentifierSearch" type="input" selector=".col-name .admin__control-text"/> + <element name="searchButton" type="input" selector=".admin__filter-actions [title='Search']"/> + <element name="firstRow" type="block" selector="tr[data-role='row']"/> + </section> +</sections> + diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml new file mode 100644 index 0000000000000..a2645c9cbf96d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSlideOutDialogSection"> + <element name="closeButton" type="button" selector=".modal-slide._show [data-role='closeBtn']" timeout="30"/> + <element name="cancelButton" type="button" selector="//*[contains(@class, 'modal-slide') and contains(@class, '_show')]//*[contains(@class, 'page-actions')]//button[normalize-space(.)='Cancel']" timeout="30"/> + <element name="doneButton" type="button" selector="//*[contains(@class, 'modal-slide') and contains(@class, '_show')]//*[contains(@class, 'page-actions')]//button[normalize-space(.)='Done']" timeout="30"/> + <element name="saveButton" type="button" selector="//*[contains(@class, 'modal-slide') and contains(@class, '_show')]//*[contains(@class, 'page-actions')]//button[normalize-space(.)='Save']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml new file mode 100644 index 0000000000000..c50bf0664f9cb --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="LocaleOptionsSection"> + <element name="sectionHeader" type="text" selector="#general_locale-head"/> + <element name="timezone" type="select" selector="#general_locale_timezone"/> + </section> +</sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml new file mode 100644 index 0000000000000..7f0194b7dc347 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginTest"> + <annotations> + <features value="Backend"/> + <stories value="Login on the Admin Login page"/> + <title value="Admin should be able to log into the Magento Admin backend"/> + <description value="Admin should be able to log into the Magento Admin backend"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-71572"/> + <group value="example"/> + <group value="login"/> + </annotations> + + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn"/> + <closeAdminNotification stepKey="closeAdminNotification"/> + <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml new file mode 100644 index 0000000000000..c9a3b8089cc1d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMenuNavigationWithSecretKeysTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin should be able to navigate between menu options with secret url keys enabled"/> + <description value="Admin should be able to navigate between menu options with secret url keys enabled"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-95349"/> + <group value="menu"/> + </annotations> + <before> + <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set admin/security/use_form_key 0" stepKey="disableUrlSecretKeys"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <click selector="{{AdminMenuSection.stores}}" stepKey="clickStoresMenuOption1"/> + <waitForLoadingMaskToDisappear stepKey="waitForStoresMenu1" /> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickStoresConfigurationMenuOption1"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoad1"/> + <seeCurrentUrlMatches regex="~\/admin\/system_config\/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> + + <click selector="{{AdminMenuSection.catalog}}" stepKey="clickCatalogMenuOption"/> + <waitForLoadingMaskToDisappear stepKey="waitForCatalogMenu1" /> + <click selector="{{AdminMenuSection.catalogProducts}}" stepKey="clickCatalogProductsMenuOption"/> + <waitForPageLoad stepKey="waitForProductsPageLoad"/> + <seeCurrentUrlMatches regex="~\/catalog\/product\/~" stepKey="seeCurrentUrlMatchesProductsPath"/> + + <click selector="{{AdminMenuSection.stores}}" stepKey="clickStoresMenuOption2"/> + <waitForLoadingMaskToDisappear stepKey="waitForStoresMenu2" /> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickStoresConfigurationMenuOption2"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoad2"/> + <seeCurrentUrlMatches regex="~\/admin\/system_config\/~" stepKey="seeCurrentUrlMatchesConfigPath2"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php index 7e4c426de9452..88b994a6b93b7 100644 --- a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php +++ b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php @@ -146,6 +146,9 @@ public function testProcessNotLoggedInUser($isIFrameParam, $isAjaxParam, $isForw $this->assertEquals($expectedResult, $this->plugin->aroundDispatch($subject, $proceed, $request)); } + /** + * @return array + */ public function processNotLoggedInUserDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php index 2f808eaf2d1b8..d793a80cdeacf 100644 --- a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php +++ b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php @@ -74,6 +74,9 @@ public function testBeforeDispatchWhenMassactionPrepareKeyRequestExists($postDat $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } + /** + * @return array + */ public function beforeDispatchDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php b/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php index 4eff6218961af..2d60bef3f3e8c 100644 --- a/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php +++ b/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php @@ -8,6 +8,9 @@ class ActionStub extends \Magento\Backend\App\Action { + /** + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void + */ public function execute() { // Empty method stub for test diff --git a/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php b/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php index bc7dce6f20bac..642c6283decae 100644 --- a/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php +++ b/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php @@ -118,6 +118,9 @@ public function testIsHostBackend($url, $host, $useCustomAdminUrl, $customAdminU $this->assertEquals($this->model->isHostBackend(), $expectedValue); } + /** + * @return array + */ public function hostsDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php b/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php index 114c57867badf..53640a81e722f 100644 --- a/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php +++ b/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php @@ -70,6 +70,9 @@ public function testIsSetFlag($configPath, $configValue, $expectedResult) $this->assertEquals($expectedResult, $this->model->isSetFlag($configPath)); } + /** + * @return array + */ public function isSetFlagDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php b/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php index f52f4ab337712..eccb08e788a95 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php @@ -141,6 +141,9 @@ public function testRenderAnchorLevelIsNotOne($hasTarget) ); } + /** + * @return array + */ public function targetDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php b/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php index 160cfe609f85f..915b3fe21eaa8 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php @@ -11,7 +11,7 @@ class AdditionalTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Backend\Block\Cache\Additional */ - private $additonalBlock; + private $additionalBlock; /** * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject @@ -39,7 +39,7 @@ protected function setUp() ] ); - $this->additonalBlock = $objectHelper->getObject( + $this->additionalBlock = $objectHelper->getObject( \Magento\Backend\Block\Cache\Additional::class, ['context' => $context] ); @@ -52,7 +52,7 @@ public function testGetCleanImagesUrl() ->method('getUrl') ->with('*/*/cleanImages') ->will($this->returnValue($expectedUrl)); - $this->assertEquals($expectedUrl, $this->additonalBlock->getCleanImagesUrl()); + $this->assertEquals($expectedUrl, $this->additionalBlock->getCleanImagesUrl()); } public function testGetCleanMediaUrl() @@ -62,7 +62,7 @@ public function testGetCleanMediaUrl() ->method('getUrl') ->with('*/*/cleanMedia') ->will($this->returnValue($expectedUrl)); - $this->assertEquals($expectedUrl, $this->additonalBlock->getCleanMediaUrl()); + $this->assertEquals($expectedUrl, $this->additionalBlock->getCleanMediaUrl()); } public function testGetCleanStaticFiles() @@ -72,7 +72,7 @@ public function testGetCleanStaticFiles() ->method('getUrl') ->with('*/*/cleanStaticFiles') ->will($this->returnValue($expectedUrl)); - $this->assertEquals($expectedUrl, $this->additonalBlock->getCleanStaticFilesUrl()); + $this->assertEquals($expectedUrl, $this->additionalBlock->getCleanStaticFilesUrl()); } /** @@ -85,9 +85,12 @@ public function testIsInProductionMode($mode, $expected) $this->appStateMock->expects($this->once()) ->method('getMode') ->willReturn($mode); - $this->assertEquals($expected, $this->additonalBlock->isInProductionMode()); + $this->assertEquals($expected, $this->additionalBlock->isInProductionMode()); } + /** + * @return array + */ public function isInProductionModeDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php new file mode 100644 index 0000000000000..6975b8ac092ad --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Backend\Test\Unit\Block\Cache; + +use Magento\Backend\Block\Cache\Permissions; +use Magento\Framework\Authorization; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Class PermissionsTest + */ +class PermissionsTest extends TestCase +{ + /** + * @var Permissions + */ + private $permissions; + + /** + * @var AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $mockAuthorization; + + /** + * @var ObjectManager + */ + private $objectManager; + + public function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->mockAuthorization = $this->getMockBuilder(Authorization::class) + ->disableOriginalConstructor() + ->setMethods(['isAllowed']) + ->getMock(); + + $this->permissions = new Permissions($this->mockAuthorization); + } + + public function testHasAccessToFlushCatalogImages() + { + $this->mockAuthorization->expects($this->atLeastOnce()) + ->method('isAllowed') + ->with('Magento_Backend::flush_catalog_images') + ->willReturn(true); + + $this->assertTrue($this->permissions->hasAccessToFlushCatalogImages()); + } + + public function testHasAccessToFlushJsCss() + { + $this->mockAuthorization->expects($this->atLeastOnce()) + ->method('isAllowed') + ->with('Magento_Backend::flush_js_css') + ->willReturn(true); + + $this->assertTrue($this->permissions->hasAccessToFlushJsCss()); + } + + public function testHasAccessToFlushStaticFiles() + { + $this->mockAuthorization->expects($this->atLeastOnce()) + ->method('isAllowed') + ->with('Magento_Backend::flush_static_files') + ->willReturn(true); + + $this->assertTrue($this->permissions->hasAccessToFlushStaticFiles()); + } +} diff --git a/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php b/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php index a79050faeb84a..aca719b2e65e9 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php @@ -74,6 +74,9 @@ public function testIsItemActiveLevelNotZero() ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php index bcf5d1adbc12b..e64d1a97af4ae 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php @@ -61,6 +61,9 @@ public function testGetAttributesHtml($data, $expect) $this->assertRegExp($expect, $attributes); } + /** + * @return array + */ public function getAttributesHtmlDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php index 355d11b561317..2848c9b23d2b7 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php @@ -30,6 +30,12 @@ class DateTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $localeDateMock; + /** @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject */ + private $escaperMock; + + /** @var \Magento\Backend\Block\Context|\PHPUnit_Framework_MockObject_MockObject */ + private $contextMock; + protected function setUp() { $this->mathRandomMock = $this->getMockBuilder(\Magento\Framework\Math\Random::class) @@ -58,6 +64,17 @@ protected function setUp() ->setMethods([]) ->getMock(); + $this->escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock = $this->getMockBuilder(\Magento\Backend\Block\Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); + $this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock); + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManagerHelper->getObject( \Magento\Backend\Block\Widget\Grid\Column\Filter\Date::class, @@ -65,7 +82,8 @@ protected function setUp() 'mathRandom' => $this->mathRandomMock, 'localeResolver' => $this->localeResolverMock, 'dateTimeFormatter' => $this->dateTimeFormatterMock, - 'localeDate' => $this->localeDateMock + 'localeDate' => $this->localeDateMock, + 'context' => $this->contextMock, ] ); $this->model->setColumn($this->columnMock); @@ -98,4 +116,16 @@ public function testGetHtmlSuccessfulTimestamp() $this->assertContains('id="' . $uniqueHash . '_from" value="' . $yesterday->getTimestamp(), $output); $this->assertContains('id="' . $uniqueHash . '_to" value="' . $tomorrow->getTimestamp(), $output); } + + public function testGetEscapedValueEscapeString() + { + $value = "\"><img src=x onerror=alert(2) />"; + $array = [ + 'orig_from' => $value, + 'from' => $value, + ]; + $this->model->setValue($array); + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($value); + $this->model->getEscapedValue('from'); + } } diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php index 1e692f890a94e..cdb88b6735f15 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php @@ -30,6 +30,12 @@ class DatetimeTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $localeDateMock; + /** @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject */ + private $escaperMock; + + /** @var \Magento\Backend\Block\Context|\PHPUnit_Framework_MockObject_MockObject */ + private $contextMock; + protected function setUp() { $this->mathRandomMock = $this->getMockBuilder(\Magento\Framework\Math\Random::class) @@ -50,7 +56,7 @@ protected function setUp() $this->columnMock = $this->getMockBuilder(\Magento\Backend\Block\Widget\Grid\Column::class) ->disableOriginalConstructor() - ->setMethods(['getTimezone', 'getHtmlId', 'getId']) + ->setMethods(['getTimezone', 'getHtmlId', 'getId', 'getFilterTime']) ->getMock(); $this->localeDateMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) @@ -58,6 +64,17 @@ protected function setUp() ->setMethods([]) ->getMock(); + $this->escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock = $this->getMockBuilder(\Magento\Backend\Block\Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); + $this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock); + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManagerHelper->getObject( \Magento\Backend\Block\Widget\Grid\Column\Filter\Datetime::class, @@ -65,7 +82,8 @@ protected function setUp() 'mathRandom' => $this->mathRandomMock, 'localeResolver' => $this->localeResolverMock, 'dateTimeFormatter' => $this->dateTimeFormatterMock, - 'localeDate' => $this->localeDateMock + 'localeDate' => $this->localeDateMock, + 'context' => $this->contextMock, ] ); $this->model->setColumn($this->columnMock); @@ -98,4 +116,17 @@ public function testGetHtmlSuccessfulTimestamp() $this->assertContains('id="' . $uniqueHash . '_from" value="' . $yesterday->getTimestamp(), $output); $this->assertContains('id="' . $uniqueHash . '_to" value="' . $tomorrow->getTimestamp(), $output); } + + public function testGetEscapedValueEscapeString() + { + $value = "\"><img src=x onerror=alert(2) />"; + $array = [ + 'orig_from' => $value, + 'from' => $value, + ]; + $this->model->setValue($array); + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($value); + $this->columnMock->expects($this->once())->method('getFilterTime')->willReturn(true); + $this->model->getEscapedValue('from'); + } } diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php index 35e21d7d194aa..81f104dbb636b 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php @@ -54,6 +54,9 @@ public function testRender(array $rowData, $expectedResult) $this->assertEquals($expectedResult, $this->_object->render(new \Magento\Framework\DataObject($rowData))); } + /** + * @return array + */ public function renderDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php index 67ead0ddd8f35..6f838634c6bed 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php @@ -63,6 +63,9 @@ public function testRender(array $rowData, $expectedResult) $this->assertEquals($expectedResult, $this->_object->render(new \Magento\Framework\DataObject($rowData))); } + /** + * @return array + */ public function renderDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php index be171a8ed40bf..df242a4cf6129 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php @@ -117,7 +117,7 @@ public function testSetFilterTypePropagatesFilterTypeToColumns() public function testGetRowUrlIfUrlPathNotSet() { - $this->assertEquals('#', $this->_block->getRowUrl(new \StdClass())); + $this->assertEquals('#', $this->_block->getRowUrl(new \stdClass())); } public function testGetRowUrl() diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php index da13af87b71ea..2e6bed4783e7f 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php @@ -86,6 +86,9 @@ public function testGetSortable($value) $this->assertFalse($this->_block->getSortable()); } + /** + * @return array + */ public function getSortableDataProvider() { return ['zero' => ['0'], 'false' => [false], 'null' => [null]]; @@ -351,7 +354,7 @@ public function testSetGetGrid() $this->_block->setFilter('StdClass'); - $grid = new \StdClass(); + $grid = new \stdClass(); $this->_block->setGrid($grid); $this->assertEquals($grid, $this->_block->getGrid()); } @@ -374,6 +377,9 @@ public function testColumnIsGrouped($groupedData, $expected) $this->assertEquals($expected, $block->isGrouped()); } + /** + * @return array + */ public function columnGroupedDataProvider() { return [[[], false], [['grouped' => 0], false], [['grouped' => 1], true]]; diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php index 4525de1fee542..f81928c4540ba 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php @@ -152,6 +152,9 @@ public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) $this->assertEquals($result, $this->_block->getGridIdsJson()); } + /** + * @return array + */ public function dataProviderGetGridIdsJsonWithUseSelectAll() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php index bb389a996e1ed..e8143b5f6b43a 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php @@ -10,6 +10,7 @@ namespace Magento\Backend\Test\Unit\Block\Widget\Grid; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; +use Magento\Framework\Authorization; class MassactionTest extends \PHPUnit\Framework\TestCase { @@ -43,6 +44,11 @@ class MassactionTest extends \PHPUnit\Framework\TestCase */ protected $_requestMock; + /** + * @var Authorization|\PHPUnit_Framework_MockObject_MockObject + */ + protected $_authorizationMock; + /** * @var VisibilityChecker|\PHPUnit_Framework_MockObject_MockObject */ @@ -86,11 +92,17 @@ protected function setUp() $this->visibilityCheckerMock = $this->getMockBuilder(VisibilityChecker::class) ->getMockForAbstractClass(); + $this->_authorizationMock = $this->getMockBuilder(Authorization::class) + ->disableOriginalConstructor() + ->setMethods(['isAllowed']) + ->getMock(); + $arguments = [ 'layout' => $this->_layoutMock, 'request' => $this->_requestMock, 'urlBuilder' => $this->_urlModelMock, - 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id'] + 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id'], + 'authorization' => $this->_authorizationMock, ]; $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -145,6 +157,10 @@ public function testItemsProcessing($itemId, $item, $expectedItem) ->method('getUrl') ->willReturnMap($urlReturnValueMap); + $this->_authorizationMock->expects($this->any()) + ->method('isAllowed') + ->willReturn(true); + $this->_block->addItem($itemId, $item); $this->assertEquals(1, $this->_block->getCount()); @@ -184,6 +200,28 @@ public function itemsProcessingDataProvider() "id" => 'test_id2', ] ) + ], + [ + 'enabled', + new \Magento\Framework\DataObject(["label" => "Test Item Enabled", "url" => "*/*/test2"]), + new \Magento\Framework\DataObject( + [ + "label" => "Test Item Enabled", + "url" => "http://localhost/index.php/backend/admin/test/test2", + "id" => 'enabled', + ] + ) + ], + [ + 'refresh', + new \Magento\Framework\DataObject(["label" => "Test Item Refresh", "url" => "*/*/test2"]), + new \Magento\Framework\DataObject( + [ + "label" => "Test Item Refresh", + "url" => "http://localhost/index.php/backend/admin/test/test2", + "id" => 'refresh', + ] + ) ] ]; } @@ -205,6 +243,9 @@ public function testSelected($param, $expectedJson, $expected) $this->assertEquals($expected, $this->_block->getSelected()); } + /** + * @return array + */ public function selectedDataProvider() { return [ @@ -237,7 +278,7 @@ public function testGetGridIdsJsonWithoutUseSelectAll() public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) { $this->_block->setUseSelectAll(true); - + if ($this->_block->getMassactionIdField()) { $massActionIdField = $this->_block->getMassactionIdField(); } else { @@ -290,14 +331,20 @@ public function dataProviderGetGridIdsJsonWithUseSelectAll() * @param int $count * @param bool $withVisibilityChecker * @param bool $isVisible + * @param bool $isAllowed + * * @dataProvider addItemDataProvider */ - public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible) + public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible, $isAllowed) { $this->visibilityCheckerMock->expects($this->any()) ->method('isVisible') ->willReturn($isVisible); + $this->_authorizationMock->expects($this->any()) + ->method('isAllowed') + ->willReturn($isAllowed); + if ($withVisibilityChecker) { $item['visible'] = $this->visibilityCheckerMock; } @@ -311,7 +358,7 @@ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isV ->willReturnMap($urlReturnValueMap); $this->_block->addItem($itemId, $item); - $this->assertEquals($count, $this->_block->getCount()); + $this->assertEquals($count, $this->_block->getCount(), $itemId); } /** @@ -325,7 +372,8 @@ public function addItemDataProvider() 'item' => ['label' => 'Test 1', 'url' => '*/*/test1'], 'count' => 1, 'withVisibilityChecker' => false, - '$isVisible' => false, + 'isVisible' => false, + 'isAllowed' => true, ], [ 'itemId' => 'test2', @@ -333,21 +381,56 @@ public function addItemDataProvider() 'count' => 1, 'withVisibilityChecker' => false, 'isVisible' => true, + 'isAllowed' => true, ], [ - 'itemId' => 'test1', - 'item' => ['label' => 'Test 1. Hide', 'url' => '*/*/test1'], + 'itemId' => 'test3', + 'item' => ['label' => 'Test 3. Hide', 'url' => '*/*/test3'], 'count' => 0, 'withVisibilityChecker' => true, 'isVisible' => false, + 'isAllowed' => true, ], [ - 'itemId' => 'test2', - 'item' => ['label' => 'Test 2. Does not hide', 'url' => '*/*/test2'], + 'itemId' => 'test4', + 'item' => ['label' => 'Test 4. Does not hide', 'url' => '*/*/test4'], 'count' => 1, 'withVisibilityChecker' => true, 'isVisible' => true, - ] + 'isAllowed' => true, + ], + [ + 'itemId' => 'enable', + 'item' => ['label' => 'Test 5. Not restricted', 'url' => '*/*/test5'], + 'count' => 1, + 'withVisibilityChecker' => true, + 'isVisible' => true, + 'isAllowed' => true, + ], + [ + 'itemId' => 'enable', + 'item' => ['label' => 'Test 5. restricted', 'url' => '*/*/test5'], + 'count' => 0, + 'withVisibilityChecker' => true, + 'isVisible' => true, + 'isAllowed' => false, + ], + [ + 'itemId' => 'refresh', + 'item' => ['label' => 'Test 6. Not Restricted', 'url' => '*/*/test6'], + 'count' => 1, + 'withVisibilityChecker' => true, + 'isVisible' => true, + 'isAllowed' => true, + ], + [ + 'itemId' => 'refresh', + 'item' => ['label' => 'Test 6. Restricted', 'url' => '*/*/test6'], + 'count' => 0, + 'withVisibilityChecker' => true, + 'isVisible' => true, + 'isAllowed' => false, + ], ]; } } diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php index 1670233324f8e..ad7c6fa99afd2 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php @@ -34,6 +34,9 @@ public function testGetters($method, $field, $value, $expected) $this->assertEquals($expected, $object->{$method}()); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php index b1911da024227..ac0f4a2f467c8 100644 --- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php +++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php @@ -38,7 +38,7 @@ public function testExecute() $messageManagerParams = $helper->getConstructArguments(\Magento\Framework\Message\Manager::class); $messageManagerParams['exceptionMessageFactory'] = $exceptionMessageFactory; $messageManager = $this->getMockBuilder(\Magento\Framework\Message\Manager::class) - ->setMethods(['addSuccess']) + ->setMethods(['addSuccessMessage']) ->setConstructorArgs($messageManagerParams) ->getMock(); @@ -86,7 +86,7 @@ public function testExecute() $mergeService->expects($this->once())->method('cleanMergedJsCss'); $messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('The JavaScript/CSS cache has been cleaned.'); $valueMap = [ diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php index 40d9ca1aa8996..fc457cd9681e6 100644 --- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php +++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php @@ -76,7 +76,7 @@ public function testExecute() ->with('clean_static_files_cache_after'); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('The static files cache has been cleaned.'); $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php index 556db311748bd..a8b248c611e07 100644 --- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php +++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Test\Unit\Controller\Adminhtml\Cache; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -155,8 +156,8 @@ public function testExecuteInvalidTypeCache() ->willReturn(['someCache']); $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('Specified cache type(s) don\'t exist: someCache') + ->method('addErrorMessage') + ->with('These cache type(s) don\'t exist: someCache') ->willReturnSelf(); $this->assertSame($this->redirectMock, $this->controller->execute()); @@ -175,7 +176,7 @@ public function testExecuteWithException() ->willThrowException($exception); $this->messageManagerMock->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, 'An error occurred while disabling cache.') ->willReturnSelf(); @@ -215,7 +216,7 @@ public function testExecuteSuccess() ->method('persist'); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('1 cache type(s) disabled.') ->willReturnSelf(); diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php index ad622ca69757a..6eac44a564f6d 100644 --- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php +++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Test\Unit\Controller\Adminhtml\Cache; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -155,8 +156,8 @@ public function testExecuteInvalidTypeCache() ->willReturn(['someCache']); $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('Specified cache type(s) don\'t exist: someCache') + ->method('addErrorMessage') + ->with('These cache type(s) don\'t exist: someCache') ->willReturnSelf(); $this->assertSame($this->redirectMock, $this->controller->execute()); @@ -175,7 +176,7 @@ public function testExecuteWithException() ->willThrowException($exception); $this->messageManagerMock->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, 'An error occurred while enabling cache.') ->willReturnSelf(); @@ -215,7 +216,7 @@ public function testExecuteSuccess() ->method('persist'); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('1 cache type(s) enabled.') ->willReturnSelf(); diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php index e8dcc00345fc6..a985681919f0b 100644 --- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php +++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php @@ -107,7 +107,7 @@ public function testExecute() $this->resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirect); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('We updated lifetime statistic.')); $this->objectManager->expects($this->any()) diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php index 844a821df1c20..a8490d6ba2e58 100644 --- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php +++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php @@ -71,7 +71,7 @@ protected function setUp() ->getMock(); $this->_messagesMock = $this->getMockBuilder(\Magento\Framework\Message\Manager::class) ->disableOriginalConstructor() - ->setMethods(['addSuccess']) + ->setMethods(['addSuccessMessage']) ->getMockForAbstractClass(); $this->_authSessionMock = $this->getMockBuilder(\Magento\Backend\Model\Auth\Session::class) @@ -221,7 +221,7 @@ public function testSaveAction() $this->_requestMock->setParams($requestParams); - $this->_messagesMock->expects($this->once())->method('addSuccess')->with($this->equalTo($testedMessage)); + $this->_messagesMock->expects($this->once())->method('addSuccessMessage')->with($this->equalTo($testedMessage)); $this->_controller->execute(); } diff --git a/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php b/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php index b7a33ab883b69..50c3a8571b48f 100644 --- a/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php @@ -60,6 +60,9 @@ public function testPrepareFilterStringValues(array $inputString, array $expecte $this->assertEquals($expected, $actual); } + /** + * @return array + */ public function getPrepareFilterStringValuesDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php index 4911dc1e9968e..b373459b7864d 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php @@ -76,17 +76,35 @@ public function testGetCurrentSecureUrl() * @param $unsecureBaseUrl * @param $useSecureInAdmin * @param $secureBaseUrl + * @param $useCustomUrl + * @param $customUrl * @param $expected * @dataProvider shouldBeSecureDataProvider */ - public function testShouldBeSecure($unsecureBaseUrl, $useSecureInAdmin, $secureBaseUrl, $expected) - { - $coreConfigValueMap = [ + public function testShouldBeSecure( + $unsecureBaseUrl, + $useSecureInAdmin, + $secureBaseUrl, + $useCustomUrl, + $customUrl, + $expected + ) { + $coreConfigValueMap = $this->returnValueMap([ [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, 'default', null, $unsecureBaseUrl], [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, 'default', null, $secureBaseUrl], - ]; - $this->coreConfig->expects($this->any())->method('getValue')->will($this->returnValueMap($coreConfigValueMap)); - $this->backendConfig->expects($this->any())->method('isSetFlag')->willReturn($useSecureInAdmin); + ['admin/url/custom', 'default', null, $customUrl], + ]); + $backendConfigFlagsMap = $this->returnValueMap([ + [\Magento\Store\Model\Store::XML_PATH_SECURE_IN_ADMINHTML, $useSecureInAdmin], + ['admin/url/use_custom', $useCustomUrl], + ]); + $this->coreConfig->expects($this->atLeast(1))->method('getValue') + ->will($coreConfigValueMap); + $this->coreConfig->expects($this->atMost(2))->method('getValue') + ->will($coreConfigValueMap); + + $this->backendConfig->expects($this->atMost(2))->method('isSetFlag') + ->will($backendConfigFlagsMap); $this->assertEquals($expected, $this->adminPathConfig->shouldBeSecure('')); } @@ -96,13 +114,13 @@ public function testShouldBeSecure($unsecureBaseUrl, $useSecureInAdmin, $secureB public function shouldBeSecureDataProvider() { return [ - ['http://localhost/', false, 'default', false], - ['http://localhost/', true, 'default', false], - ['https://localhost/', false, 'default', true], - ['https://localhost/', true, 'default', true], - ['http://localhost/', false, 'https://localhost/', false], - ['http://localhost/', true, 'https://localhost/', true], - ['https://localhost/', true, 'https://localhost/', true], + ['http://localhost/', false, 'default', false, '', false], + ['http://localhost/', true, 'default', false, '', false], + ['https://localhost/', false, 'default', false, '', true], + ['https://localhost/', true, 'default', false, '', true], + ['http://localhost/', false, 'https://localhost/', false, '', false], + ['http://localhost/', true, 'https://localhost/', false, '', true], + ['https://localhost/', true, 'https://localhost/', false, '', true], ]; } diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php index 391deac5a1f4e..f1a4bc355b08e 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php @@ -120,6 +120,9 @@ public function testRefreshAcl($isUserPassedViaParams) $this->assertSame($aclMock, $this->session->getAcl()); } + /** + * @return array + */ public function refreshAclDataProvider() { return [ @@ -234,6 +237,9 @@ public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expect $this->assertEquals($expectedResult, $this->session->isAllowed('resource')); } + /** + * @return array + */ public function isAllowedDataProvider() { return [ @@ -254,6 +260,9 @@ public function testFirstPageAfterLogin($isFirstPageAfterLogin) $this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin()); } + /** + * @return array + */ public function firstPageAfterLoginDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Model/AuthTest.php b/app/code/Magento/Backend/Test/Unit/Model/AuthTest.php index 4b79d504dad91..4af060b157ed4 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/AuthTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/AuthTest.php @@ -54,7 +54,6 @@ protected function setUp() /** * @expectedException \Magento\Framework\Exception\AuthenticationException - * @expectedExceptionMessage You did not sign in correctly or your account is temporarily disabled. */ public function testLoginFailed() { @@ -64,7 +63,10 @@ public function testLoginFailed() ->with(\Magento\Backend\Model\Auth\Credential\StorageInterface::class) ->will($this->returnValue($this->_credentialStorage)); $exceptionMock = new \Magento\Framework\Exception\LocalizedException( - __('You did not sign in correctly or your account is temporarily disabled.') + __( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) ); $this->_credentialStorage ->expects($this->once()) @@ -74,5 +76,10 @@ public function testLoginFailed() $this->_credentialStorage->expects($this->never())->method('getId'); $this->_eventManagerMock->expects($this->once())->method('dispatch')->with('backend_auth_user_login_failed'); $this->_model->login('username', 'password'); + + $this->expectExceptionMessage( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ); } } diff --git a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php index 31a13191750a3..cce83c33a2aaa 100755 --- a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Test\Unit\Model\Config\SessionLifetime; use Magento\Backend\Model\Config\SessionLifetime\BackendModel; @@ -20,23 +21,28 @@ public function testBeforeSave($value, $errorMessage = null) \Magento\Backend\Model\Config\SessionLifetime\BackendModel::class ); if ($errorMessage !== null) { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $errorMessage); + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage($errorMessage); } $model->setValue($value); $object = $model->beforeSave(); $this->assertEquals($model, $object); } + /** + * @return array + */ public function adminSessionLifetimeDataProvider() { return [ [ BackendModel::MIN_LIFETIME - 1, - 'Admin session lifetime must be greater than or equal to 60 seconds' + 'The Admin session lifetime is invalid. Set the lifetime to 60 seconds or longer and try again.' ], [ BackendModel::MAX_LIFETIME + 1, - 'Admin session lifetime must be less than or equal to 31536000 seconds (one year)' + 'The Admin session lifetime is invalid. ' + . 'Set the lifetime to 31536000 seconds (one year) or shorter and try again.' ], [ 900 diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php index bc18bd44f4be4..2b5f644e35977 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php @@ -96,7 +96,7 @@ public function testGetMenuWithCachedObjectReturnsUnserializedObject() $this->assertEquals($this->menuMock, $this->model->getMenu()); } - public function testGetMenuWithNotCachedObjectBuidlsObject() + public function testGetMenuWithNotCachedObjectBuildsObject() { $this->cacheInstanceMock->expects( $this->at(0) @@ -140,6 +140,9 @@ public function testGetMenuExceptionLogged($expectedException) $this->model->getMenu(); } + /** + * @return array + */ public function getMenuExceptionLoggedDataProvider() { return [ @@ -165,6 +168,6 @@ public function testGetMenuGenericExceptionIsNotLogged() } catch (\Exception $e) { return; } - $this->fail("Generic \Exception was not throwed"); + $this->fail("Generic \Exception was not thrown"); } } diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php index 3c1f1e43900be..dec85f4b98e3d 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php @@ -79,6 +79,9 @@ public function testValidateWithMissingRequiredParamThrowsException($requiredPar } } + /** + * @return array + */ public function requiredParamsProvider() { return [['id'], ['title'], ['resource']]; @@ -102,6 +105,9 @@ public function testValidateWithNonValidPrimitivesThrowsException($param, $inval } } + /** + * @return array + */ public function invalidParamsProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php b/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php index 23d1ed5da1425..5d026a2b1fc32 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php @@ -35,6 +35,9 @@ public function testAfterGetResult($isPub, $times) ); } + /** + * @return array + */ public function afterGetResultDataProvider() { return [[true, 1], [false, 0],]; diff --git a/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php index 49fcdf4fc8770..00ae8c2f44a69 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php @@ -136,6 +136,9 @@ public function testSetSessionSettingsByConstructor($secureRequest) $this->assertSame($secureRequest, $adminConfig->getCookieSecure()); } + /** + * @return array + */ public function requestSecureDataProvider() { return [[true], [false]]; diff --git a/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php b/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php index 569c5ffc16c9c..98f1965477b2c 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php @@ -58,6 +58,9 @@ public function testIsOperation($operation, $expected) $this->assertEquals($expected, $this->_model->isOperation($operation)); } + /** + * @return array + */ public function isOperationDataProvider() { return [ diff --git a/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php new file mode 100644 index 0000000000000..c7ff1d95617b6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Test\Unit\Service\V1; + +use Magento\Backend\Service\V1\ModuleService; +use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Module List Service Test + * + * Covers \Magento\Sales\Model\ValidatorResultMerger + */ +class ModuleServiceTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ModuleService + */ + private $moduleService; + + /** + * @var ModuleListInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $moduleListMock; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->moduleListMock = $this->createMock(ModuleListInterface::class); + $this->objectManager = new ObjectManager($this); + $this->moduleService = $this->objectManager->getObject( + ModuleService::class, + [ + 'moduleList' => $this->moduleListMock, + ] + ); + } + + /** + * Test getModules method + * + * @return void + */ + public function testGetModules() + { + $moduleNames = ['Magento_Backend', 'Magento_Catalog', 'Magento_Customer']; + $this->moduleListMock->expects($this->once())->method('getNames')->willReturn($moduleNames); + + $expected = $moduleNames; + $actual = $this->moduleService->getModules(); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php b/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php new file mode 100644 index 0000000000000..3f4f3669ab75b --- /dev/null +++ b/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Ui\Component\Control; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Escaper; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; + +/** + * Represents delete button with pre-configured options + * Provide an ability to show confirmation message on click on the "Delete" button + * + * @api + */ +class DeleteButton implements ButtonProviderInterface +{ + /** + * @var RequestInterface + */ + private $request; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @var string + */ + private $confirmationMessage; + + /** + * @var string + */ + private $idFieldName; + + /** + * @var string + */ + private $deleteRoutePath; + + /** + * @var int + */ + private $sortOrder; + + /** + * @param RequestInterface $request + * @param UrlInterface $urlBuilder + * @param Escaper $escaper + * @param string $confirmationMessage + * @param string $idFieldName + * @param string $deleteRoutePath + * @param int $sortOrder + */ + public function __construct( + RequestInterface $request, + UrlInterface $urlBuilder, + Escaper $escaper, + string $confirmationMessage, + string $idFieldName, + string $deleteRoutePath, + int $sortOrder + ) { + $this->request = $request; + $this->urlBuilder = $urlBuilder; + $this->escaper = $escaper; + $this->confirmationMessage = $confirmationMessage; + $this->idFieldName = $idFieldName; + $this->deleteRoutePath = $deleteRoutePath; + $this->sortOrder = $sortOrder; + } + + /** + * {@inheritdoc} + */ + public function getButtonData() + { + $data = []; + $fieldId = $this->escaper->escapeJs($this->escaper->escapeHtml($this->request->getParam($this->idFieldName))); + if (null !== $fieldId) { + $url = $this->urlBuilder->getUrl($this->deleteRoutePath); + $escapedMessage = $this->escaper->escapeJs($this->escaper->escapeHtml($this->confirmationMessage)); + $data = [ + 'label' => __('Delete'), + 'class' => 'delete', + 'on_click' => "deleteConfirm('{$escapedMessage}', '{$url}', {data:{{$this->idFieldName}:{$fieldId}}})", + 'sort_order' => $this->sortOrder, + ]; + } + return $data; + } +} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index fbbecb25efa50..f9408768136bb 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -5,30 +5,29 @@ "sort-packages": true }, "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", - "magento/framework": "100.3.*", - "magento/module-backup": "100.3.*", - "magento/module-catalog": "101.2.*", - "magento/module-config": "100.3.*", - "magento/module-customer": "100.3.*", - "magento/module-developer": "100.3.*", - "magento/module-directory": "100.3.*", - "magento/module-eav": "100.3.*", - "magento/module-quote": "100.3.*", - "magento/module-reports": "100.3.*", - "magento/module-require-js": "100.3.*", - "magento/module-sales": "100.3.*", - "magento/module-security": "100.3.*", - "magento/module-store": "100.3.*", - "magento/module-translation": "100.3.*", - "magento/module-ui": "100.3.*", - "magento/module-user": "100.3.*" + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-backup": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-developer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-quote": "*", + "magento/module-reports": "*", + "magento/module-require-js": "*", + "magento/module-sales": "*", + "magento/module-security": "*", + "magento/module-store": "*", + "magento/module-translation": "*", + "magento/module-ui": "*", + "magento/module-user": "*" }, "suggest": { - "magento/module-theme": "100.3.*" + "magento/module-theme": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Backend/etc/acl.xml b/app/code/Magento/Backend/etc/acl.xml index af4ab5856e94c..cf9471e75bed9 100644 --- a/app/code/Magento/Backend/etc/acl.xml +++ b/app/code/Magento/Backend/etc/acl.xml @@ -38,7 +38,21 @@ <resource id="Magento_Backend::custom" title="Package Extensions" translate="title" sortOrder="20" /> </resource> <resource id="Magento_Backend::tools" title="Tools" translate="title" sortOrder="50"> - <resource id="Magento_Backend::cache" title="Cache Management" translate="title" sortOrder="10" /> + <resource id="Magento_Backend::cache" title="Cache Management" translate="title" sortOrder="10"> + <resource id="Magento_Backend::main_actions" title="Clean Cache Actions" translate="title" sortOrder="10"> + <resource id="Magento_Backend::flush_cache_storage" title="Flush Cache Storage" translate="title" sortOrder="10" /> + <resource id="Magento_Backend::flush_magento_cache" title="Flush Magento Cache" translate="title" sortOrder="20" /> + </resource> + <resource id="Magento_Backend::mass_actions" title="Cache Types Management" translate="title" sortOrder="20"> + <resource id="Magento_Backend::toggling_cache_type" title="Toggle Cache Type" translate="title" sortOrder="10" /> + <resource id="Magento_Backend::refresh_cache_type" title="Refresh Cache Type" translate="title" sortOrder="20" /> + </resource> + <resource id="Magento_Backend::additional_cache_management" title="Additional Cache Management" translate="title" sortOrder="30"> + <resource id="Magento_Backend::flush_catalog_images" title="Catalog Images Cache" translate="title" sortOrder="10" /> + <resource id="Magento_Backend::flush_js_css" title="Flush Js/Css" translate="title" sortOrder="20" /> + <resource id="Magento_Backend::flush_static_files" title="Flush Static Files" translate="title" sortOrder="30" /> + </resource> + </resource> <resource id="Magento_Backend::setup_wizard" title="Web Setup Wizard" translate="title" sortOrder="20" /> </resource> <resource id="Magento_Backend::system_other_settings" title="Other Settings" translate="title" sortOrder="80" /> diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml index 050115087c26e..4abea272c5495 100644 --- a/app/code/Magento/Backend/etc/adminhtml/di.xml +++ b/app/code/Magento/Backend/etc/adminhtml/di.xml @@ -139,10 +139,16 @@ <type name="Magento\Backend\Model\Menu\Builder"> <plugin name="SetupMenuBuilder" type="Magento\Backend\Model\Setup\MenuBuilder" /> </type> - <type name="Magento\Config\Model\Config\Structure\ConcealInProductionConfigList"> + <type name="Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction"> <arguments> <argument name="configs" xsi:type="array"> <item name="dev" xsi:type="const">Magento\Config\Model\Config\Structure\ElementVisibilityInterface::HIDDEN</item> + </argument> + </arguments> + </type> + <type name="Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand"> + <arguments> + <argument name="configs" xsi:type="array"> <item name="general/locale/code" xsi:type="const">Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED</item> </argument> </arguments> @@ -156,4 +162,11 @@ </argument> </arguments> </type> + <type name="Magento\Framework\View\Layout\Generator\Block"> + <arguments> + <argument name="defaultClass" xsi:type="string">Magento\Backend\Block\Template</argument> + </arguments> + </type> + <preference for="CsrfRequestValidator" type="Magento\Backend\App\Request\BackendValidator" /> + <preference for="Magento\Backend\Model\Image\UploadResizeConfigInterface" type="Magento\Backend\Model\Image\UploadResizeConfig" /> </config> diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 92df39bd65ee4..0fb7d89f924de 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -129,14 +129,14 @@ <field id="*/*/template_hints_storefront">1</field> <field id="*/*/template_hints_storefront_show_with_parameter">1</field> </depends> - <comment>Add the following paramater to the URL to show template hints ?templatehints=[parameter_value]</comment> + <comment>Add the following parameter to the URL to show template hints ?templatehints=[parameter_value]</comment> </field> <field id="template_hints_admin" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Enabled Template Path Hints for Admin</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="template_hints_blocks" translate="label" type="select" sortOrder="21" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Add Block Names to Hints</label> + <label>Add Block Class Type to Hints</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> </group> @@ -197,7 +197,7 @@ </group> <group id="image" translate="label" type="text" sortOrder="120" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Image Processing Settings</label> - <field id="default_adapter" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="default_adapter" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Image Adapter</label> <source_model>Magento\Config\Model\Config\Source\Image\Adapter</source_model> <backend_model>Magento\Config\Model\Config\Backend\Image\Adapter</backend_model> @@ -231,9 +231,10 @@ <label>European Union Countries</label> <source_model>Magento\Directory\Model\Config\Source\Country</source_model> </field> - <field id="destinations" translate="label" type="multiselect" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> + <field id="destinations" translate="label" type="multiselect" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Top destinations</label> <source_model>Magento\Directory\Model\Config\Source\Country</source_model> + <can_be_empty>1</can_be_empty> </field> </group> <group id="locale" translate="label" type="text" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -314,11 +315,11 @@ <label>Disable Email Communications</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="host" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="host" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Host</label> <comment>For Windows server only.</comment> </field> - <field id="port" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="port" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Port (25)</label> <comment>For Windows server only.</comment> </field> @@ -335,6 +336,30 @@ </depends> </field> </group> + <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Images Upload Configuration</label> + <field id="enable_resize" translate="label" type="select" sortOrder="200" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Enable Frontend Resize</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Resize performed via javascript before file upload.</comment> + </field> + <field id="max_width" translate="label comment" type="text" sortOrder="300" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Maximum Width</label> + <validate>validate-greater-than-zero validate-number required-entry</validate> + <comment>Maximum allowed width for uploaded image.</comment> + <depends> + <field id="enable_resize">1</field> + </depends> + </field> + <field id="max_height" translate="label comment" type="text" sortOrder="400" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Maximum Height</label> + <validate>validate-greater-than-zero validate-number required-entry</validate> + <comment>Maximum allowed height for uploaded image.</comment> + <depends> + <field id="enable_resize">1</field> + </depends> + </field> + </group> </section> <section id="admin" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Admin</label> @@ -425,17 +450,17 @@ <label>Web</label> <tab>general</tab> <resource>Magento_Config::web</resource> - <group id="url" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="0" showInStore="0"> + <group id="url" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Url Options</label> <field id="use_store" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Add Store Code to Urls</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Store</backend_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.).]]> + <![CDATA[<strong style="color:red">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]> </comment> </field> - <field id="redirect_to_base" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="redirect_to_base" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Auto-redirect to Base URL</label> <source_model>Magento\Config\Model\Config\Source\Web\Redirect</source_model> <comment>I.e. redirect from http://example.com/store/ to http://www.example.com/store/</comment> @@ -448,7 +473,7 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> </group> - <group id="unsecure" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="unsecure" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URLs</label> <comment>Any of the fields allow fully qualified URLs that end with '/' (slash) e.g. http://example.com/magento/</comment> <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -456,7 +481,7 @@ <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>Specify URL or {{base_url}} placeholder.</comment> </field> - <field id="base_link_url" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="base_link_url" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Base Link URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>May start with {{unsecure_base_url}} placeholder.</comment> @@ -466,13 +491,13 @@ <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> </field> - <field id="base_media_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="base_media_url" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URL for User Media Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> </field> </group> - <group id="secure" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="secure" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URLs (Secure)</label> <comment>Any of the fields allow fully qualified URLs that end with '/' (slash) e.g. https://example.com/magento/</comment> <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -480,7 +505,7 @@ <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.</comment> </field> - <field id="base_link_url" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="base_link_url" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Secure Base Link URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>May start with {{secure_base_url}} or {{unsecure_base_url}} placeholder.</comment> @@ -490,24 +515,24 @@ <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.</comment> </field> - <field id="base_media_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="base_media_url" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Secure Base URL for User Media Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> <comment>May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.</comment> </field> - <field id="use_in_frontend" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="use_in_frontend" translate="label comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Use Secure URLs on Storefront</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> <comment>Enter https protocol to use Secure URLs on Storefront.</comment> </field> - <field id="use_in_adminhtml" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="use_in_adminhtml" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Use Secure URLs in Admin</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> <comment>Enter https protocol to use Secure URLs in Admin.</comment> </field> - <field id="enable_hsts" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="enable_hsts" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Enable HTTP Strict Transport Security (HSTS)</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> @@ -517,7 +542,7 @@ <field id="use_in_adminhtml">1</field> </depends> </field> - <field id="enable_upgrade_insecure" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="enable_upgrade_insecure" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Upgrade Insecure Requests</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Secure</backend_model> diff --git a/app/code/Magento/Backend/etc/config.xml b/app/code/Magento/Backend/etc/config.xml index b7aaf8bf20dba..8283fa18dd370 100644 --- a/app/code/Magento/Backend/etc/config.xml +++ b/app/code/Magento/Backend/etc/config.xml @@ -28,6 +28,11 @@ <dashboard> <enable_charts>1</enable_charts> </dashboard> + <upload_configuration> + <enable_resize>1</enable_resize> + <max_width>1920</max_width> + <max_height>1200</max_height> + </upload_configuration> </system> <general> <validator_data> diff --git a/app/code/Magento/Backend/etc/module.xml b/app/code/Magento/Backend/etc/module.xml index 57e00489391f2..3a5cd8226753d 100644 --- a/app/code/Magento/Backend/etc/module.xml +++ b/app/code/Magento/Backend/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Backend" setup_version="2.0.0"> + <module name="Magento_Backend"> <sequence> <module name="Magento_Directory"/> </sequence> diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv index 36db734d44cd4..bfedd56b14313 100644 --- a/app/code/Magento/Backend/i18n/en_US.csv +++ b/app/code/Magento/Backend/i18n/en_US.csv @@ -214,10 +214,13 @@ YTD,YTD "Admin session lifetime must be greater than or equal to 60 seconds","Admin session lifetime must be greater than or equal to 60 seconds" Order,Order "Order #%1","Order #%1" -"Access denied","Access denied" -"Please try to sign out and sign in again.","Please try to sign out and sign in again." -"If you continue to receive this message, please contact the store owner.","If you continue to receive this message, please contact the store owner." "You need more permissions to access this.","You need more permissions to access this." +"Sorry, you need permissions to view this content.","Sorry, you need permissions to view this content." +"Next steps","Next steps" +"If you think this is an error, try signing out and signing in again.","If you think this is an error, try signing out and signing in again." +"Contact a system administrator or store owner to gain permissions.","Contact a system administrator or store owner to gain permissions." +"Return to","Return to" +"previous page","previous page" "Welcome, please sign in","Welcome, please sign in" Username,Username "user name","user name" @@ -402,9 +405,9 @@ Web,Web "Url Options","Url Options" "Add Store Code to Urls","Add Store Code to Urls" " - <strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.). + <strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.). "," - <strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.). + <strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.). " "Auto-redirect to Base URL","Auto-redirect to Base URL" "Search Engine Optimization","Search Engine Optimization" @@ -444,7 +447,7 @@ Tags,Tags "<h1 class=""page-heading"">404 Error</h1><p>Page not found.</p>","<h1 class=""page-heading"">404 Error</h1><p>Page not found.</p>" "Community Edition","Community Edition" "Default Theme","Default Theme" -"If no value is specified, the system default is used. The system default may be modified by third party extensions.","If no value is specified, the system default is used. The system default may be modified by third party extensions." +"If no value is specified, the system default is used. The system default may be modified by third-party extensions.","If no value is specified, the system default is used. The system default may be modified by third-party extensions." "Applied Theme","Applied Theme" "Design Rule","Design Rule" "User Agent Rules","User Agent Rules" @@ -458,4 +461,3 @@ Pagination,Pagination "Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used.","Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used." "Anchor Text for Next","Anchor Text for Next" "Theme Name","Theme Name" -"Config","Config" diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml index ab5ddc414b51f..4bbe70b6cdb92 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml @@ -11,7 +11,11 @@ <body> <referenceContainer name="content"> <block class="Magento\Backend\Block\Cache" name="adminhtml.cache.container"/> - <block class="Magento\Backend\Block\Cache\Additional" name="cache.additional" template="Magento_Backend::system/cache/additional.phtml"/> + <block class="Magento\Backend\Block\Cache\Additional" name="cache.additional" template="Magento_Backend::system/cache/additional.phtml"> + <arguments> + <argument name="permissions" xsi:type="object">Magento\Backend\Block\Cache\Permissions</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml index 852ecd5a07962..843328fbf17d7 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml @@ -12,12 +12,23 @@ * @see \Magento\Backend\Block\Denied */ ?> -<h1 class="page-heading"><?= /* @escapeNotVerified */ __('Access denied') ?></h1> -<?php if (!$block->hasAvailableResources()): ?> -<p> -<?= /* @escapeNotVerified */ __('Please try to sign out and sign in again.') ?><br/> -<?= /* @escapeNotVerified */ __('If you continue to receive this message, please contact the store owner.') ?> -</p> -<?php else: ?> -<p><?= /* @escapeNotVerified */ __('You need more permissions to access this.') ?></p> -<?php endif?> +<hr class="access-denied-hr"/> +<div class="access-denied-page"> + <h2 class="page-heading"><?= $block->escapeHtml(__('Sorry, you need permissions to view this content.')) ?></h2> + <strong><?= $block->escapeHtml(__('Next steps')) ?></strong> + <ul> + <li><span><?= $block->escapeHtml(__('If you think this is an error, try signing out and signing in again.')) ?></span></li> + <li><span><?= $block->escapeHtml(__('Contact a system administrator or store owner to gain permissions.')) ?></span></li> + <li> + <span><?= $block->escapeHtml(__('Return to ')) ?> + <?php if(isset($_SERVER['HTTP_REFERER'])): ?> + <a href="<?= $block->escapeUrl(__($_SERVER['HTTP_REFERER'])) ?>"> + <?= $block->escapeHtml(__('previous page')) ?></a><?= $block->escapeHtml(__('.')) ?> + <?php else: ?> + <a href="<?= $block->escapeHtmlAttr(__('javascript:history.back()')) ?>"> + <?= $block->escapeHtml(__('previous page')) ?></a><?= $block->escapeHtml(__('.')) ?> + <?php endif ?> + </span> + </li> + </ul> +</div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml index 805e9783f3f18..52d5dd6d114ee 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml @@ -43,7 +43,7 @@ data-validate="{required:true}" value="" placeholder="<?= /* @escapeNotVerified */ __('password') ?>" - autocomplete="new-password" + autocomplete="off" /> </div> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml index 1e14dd837634a..4d9ba6a8c4bad 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml @@ -13,8 +13,9 @@ data-mage-init='{ "Magento_Backend/js/media-uploader" : { "maxFileSize": <?= /* @escapeNotVerified */ $block->getFileSizeService()->getMaxFileSize() ?>, - "maxWidth":<?= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> , - "maxHeight": <?= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?> + "maxWidth": <?= /* @escapeNotVerified */ $block->getImageUploadMaxWidth() ?>, + "maxHeight": <?= /* @escapeNotVerified */ $block->getImageUploadMaxHeight() ?>, + "isResizeEnabled": <?= /* @noEscape */ $block->getImageUploadConfigData()->getIsResizeEnabled() ?> } }' > diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml index 40b7173f47417..f952001f5e2ff 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml @@ -17,7 +17,7 @@ <?= /* @escapeNotVerified */ $edition ?> class="logo"> <img class="logo-img" src="<?= /* @escapeNotVerified */ $block->getViewFileUrl($logoSrc) ?>" - alt="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>"/> + alt="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>" title="<?= $block->escapeHtml(__('Magento Admin Panel')) ?>"/> </a> <?php break; ?> <?php case 'user': ?> @@ -29,7 +29,7 @@ data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> <span class="admin__action-dropdown-text"> - <span class="admin-user-account-text"><?= $block->escapeHtml($block->getUser()->getUsername()) ?></span> + <span class="admin-user-account-text"><?= $block->escapeHtml($block->getUser()->getUserName()) ?></span> </span> </a> <ul class="admin__action-dropdown-menu"> @@ -39,7 +39,7 @@ href="<?= /* @escapeNotVerified */ $block->getUrl('adminhtml/system_account/index') ?>" <?= /* @escapeNotVerified */ $block->getUiId('user', 'account', 'settings') ?> title="<?= $block->escapeHtml(__('Account Setting')) ?>"> - <?= /* @escapeNotVerified */ __('Account Setting') ?> (<span class="admin-user-name"><?= $block->escapeHtml($block->getUser()->getUsername()) ?></span>) + <?= /* @escapeNotVerified */ __('Account Setting') ?> (<span class="admin-user-name"><?= $block->escapeHtml($block->getUser()->getUserName()) ?></span>) </a> </li> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml index 3a3dbd99bfba9..4ef6d378cc4a4 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/report.phtml @@ -8,5 +8,7 @@ ?> <?php if ($block->getBugreportUrl()): ?> - <a class="link-report" href="<?= /* @escapeNotVerified */ $block->getBugreportUrl() ?>" id="footer_bug_tracking"><?= /* @escapeNotVerified */ __('Report an Issue') ?></a> + <a class="link-report" href="<?= /* @escapeNotVerified */ $block->getBugreportUrl() ?>" id="footer_bug_tracking" target="_blank"> + <?= /* @escapeNotVerified */ __('Report an Issue') ?> + </a> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml index 84bbc4ea601ef..b4bc42b95d0aa 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml @@ -5,34 +5,39 @@ */ // @codingStandardsIgnoreFile + +/** @var \Magento\Backend\Block\Cache\Permissions|null $permissions */ +$permissions = $block->getData('permissions'); ?> -<div class="additional-cache-management"> - <h2> - <span><?= /* @escapeNotVerified */ __('Additional Cache Management') ?></span> - </h2> - <p> - <button onclick="setLocation('<?= /* @escapeNotVerified */ $block->getCleanImagesUrl() ?>')" type="button"> - <?= /* @escapeNotVerified */ __('Flush Catalog Images Cache') ?> - </button> - <span><?= /* @escapeNotVerified */ __('Pregenerated product images files') ?></span> - </p> - <p> - <button onclick="setLocation('<?= /* @escapeNotVerified */ $block->getCleanMediaUrl() ?>')" type="button"> - <?= /* @escapeNotVerified */ __('Flush JavaScript/CSS Cache') ?> - </button> - <span><?= /* @escapeNotVerified */ __('Themes JavaScript and CSS files combined to one file') ?></span> - </p> - <?php - if (!$block->isInProductionMode()): - ?> - <p> - <button onclick="setLocation('<?= /* @escapeNotVerified */ $block->getCleanStaticFilesUrl() ?>')" type="button"> - <?= /* @escapeNotVerified */ __('Flush Static Files Cache') ?> - </button> - <span><?= /* @escapeNotVerified */ __('Preprocessed view files and static files') ?></span> - </p> - <?php - endif; - ?> - <?= $block->getChildHtml() ?> -</div> +<?php if ($permissions && $permissions->hasAccessToAdditionalActions()): ?> + <div class="additional-cache-management"> + <h2> + <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> + </h2> + <?php if ($permissions->hasAccessToFlushCatalogImages()): ?> + <p> + <button onclick="setLocation('<?= $block->escapeJs($block->getCleanImagesUrl()); ?>')" type="button"> + <?= $block->escapeHtml(__('Flush Catalog Images Cache')); ?> + </button> + <span><?= $block->escapeHtml(__('Pregenerated product images files')); ?></span> + </p> + <?php endif; ?> + <?php if ($permissions->hasAccessToFlushJsCss()): ?> + <p> + <button onclick="setLocation('<?= $block->escapeJs($block->getCleanMediaUrl()); ?>')" type="button"> + <?= $block->escapeHtml(__('Flush JavaScript/CSS Cache')); ?> + </button> + <span><?= $block->escapeHtml(__('Themes JavaScript and CSS files combined to one file')) ?></span> + </p> + <?php endif; ?> + <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()): ?> + <p> + <button onclick="setLocation('<?= $block->escapeJs($block->getCleanStaticFilesUrl()); ?>')" type="button"> + <?= $block->escapeHtml(__('Flush Static Files Cache')); ?> + </button> + <span><?= $block->escapeHtml(__('Preprocessed view files and static files')); ?></span> + </p> + <?php endif; ?> + <?= $block->getChildHtml() ?> + </div> +<?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml index a528133b2bc3a..af369800287c1 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml @@ -28,21 +28,21 @@ <script data-template="search-suggest" type="text/x-magento-template"> <ul class="search-global-menu"> <li class="item"> - <a id="searchPreviewProducts" href="<?= /* @escapeNotVerified */ $block->getURL('catalog/product/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Products</a> + <a id="searchPreviewProducts" href="<?= /* @escapeNotVerified */ $block->getUrl('catalog/product/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Products</a> </li> <li class="item"> - <a id="searchPreviewOrders" href="<?= /* @escapeNotVerified */ $block->getURL('sales/order/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Orders</a> + <a id="searchPreviewOrders" href="<?= /* @escapeNotVerified */ $block->getUrl('sales/order/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Orders</a> </li> <li class="item"> - <a id="searchPreviewCustomers" href="<?= /* @escapeNotVerified */ $block->getURL('customer/index/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Customers</a> + <a id="searchPreviewCustomers" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/index/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Customers</a> </li> <li class="item"> - <a id="searchPreviewPages" href="<?= /* @escapeNotVerified */ $block->getURL('cms/page/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Pages</a> + <a id="searchPreviewPages" href="<?= /* @escapeNotVerified */ $block->getUrl('cms/page/index/') ?>?search=<%- data.term%>" class="title">"<%- data.term%>" in Pages</a> </li> <% if (data.items.length) { %> <% _.each(data.items, function(value){ %> <li class="item" - <%- data.optionData(value) %> + <%= data.optionData(value) %> > <a href="<%- value.url %>" class="title"><%- value.name %></a> <span class="type"><%- value.type %></span> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml index 27127e54e5be2..a115777624e91 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml @@ -41,11 +41,11 @@ <?php endif; ?> </div> -<script> -require(['jquery'], function($){ - $('.actions-split') - .on('click.splitDefault', '.action-default', function() { - $(this).siblings('.dropdown-menu').find('.item-default').trigger('click'); - }); -}); +<script type="text/x-magento-init"> + { + ".actions-split": { + "Magento_Ui/js/grid/controls/button/split": {} + } + } </script> + diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml index 06f77839ae466..720bc1e58259d 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml @@ -46,9 +46,6 @@ <input type="<?= /* @escapeNotVerified */ $element->getType() ?>" name="<?= /* @escapeNotVerified */ $element->getName() ?>" id="<?= $element->getHtmlId() ?>" value="<?= /* @escapeNotVerified */ $element->getValue() ?>" class="input-text <?= /* @escapeNotVerified */ $element->getClass() ?>" title="<?= /* @escapeNotVerified */ $element->getTitle() ?>"/> </span> <?php break; - case 'hidden': ?> - <input type="<?= /* @escapeNotVerified */ $element->getType() ?>" name="<?= /* @escapeNotVerified */ $element->getName() ?>" id="<?= $element->getHtmlId() ?>" value="<?= /* @escapeNotVerified */ $element->getValue() ?>"> - <?php break; case 'radios': ?> <span class="form_row"> <label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label> @@ -62,7 +59,7 @@ <label for="<?= $element->getHtmlId() ?>"><?= /* @escapeNotVerified */ $element->getLabel() ?>:</label> <script> require([ - 'tinymceDeprecated' + "wysiwygAdapter" ], function(tinyMCE){ tinyMCE.init({ diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml index 4f8d9a56a38a3..e11c0efc123ff 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml @@ -34,7 +34,7 @@ <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type): ?> <td class="gallery" align="center" style="vertical-align:bottom;"> <a href="<?= /* @escapeNotVerified */ $image->setType($type)->getSourceUrl() ?>" target="_blank" onclick="imagePreview('<?= $block->getElement()->getHtmlId() ?>_image_<?= /* @escapeNotVerified */ $type ?>_<?= /* @escapeNotVerified */ $image->getValueId() ?>');return false;"> - <img id="<?= $block->getElement()->getHtmlId() ?>_image_<?= /* @escapeNotVerified */ $type ?>_<?= /* @escapeNotVerified */ $image->getValueId() ?>" src="<?= /* @escapeNotVerified */ $image->setType($type)->getSourceUrl() ?>?<?= /* @escapeNotVerified */ time() ?>" alt="<?= /* @escapeNotVerified */ $image->getValue() ?>" height="25" class="small-image-preview v-middle"/></a><br/> + <img id="<?= $block->getElement()->getHtmlId() ?>_image_<?= /* @escapeNotVerified */ $type ?>_<?= /* @escapeNotVerified */ $image->getValueId() ?>" src="<?= /* @escapeNotVerified */ $image->setType($type)->getSourceUrl() ?>?<?= /* @escapeNotVerified */ time() ?>" alt="<?= /* @escapeNotVerified */ $image->getValue() ?>" title="<?= /* @escapeNotVerified */ $image->getValue() ?>" height="25" class="small-image-preview v-middle"/></a><br/> <input type="file" name="<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>_<?= /* @escapeNotVerified */ $type ?>[<?= /* @escapeNotVerified */ $image->getValueId() ?>]" size="1"></td> <?php endforeach; ?> <td class="gallery" align="center" style="vertical-align:bottom;"><input type="input" name="<?= /* @escapeNotVerified */ $block->getElement()->getParentName() ?>[position][<?= /* @escapeNotVerified */ $image->getValueId() ?>]" value="<?= /* @escapeNotVerified */ $image->getPosition() ?>" id="<?= $block->getElement()->getHtmlId() ?>_position_<?= /* @escapeNotVerified */ $image->getValueId() ?>" size="3"/></td> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml index c99759a60d1bf..fad8f5968009f 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -19,7 +19,7 @@ * */ /* @var $block \Magento\Backend\Block\Widget\Grid */ -$numColumns = sizeof($block->getColumns()); +$numColumns = !is_null($block->getColumns()) ? sizeof($block->getColumns()) : 0; ?> <?php if ($block->getCollection()): ?> @@ -107,7 +107,7 @@ $numColumns = sizeof($block->getColumns()); <?= /* @escapeNotVerified */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> <?php if ($_curPage < $_lastPage): ?> - <button title="<?= /* @escapeNotVerified */ __('Next page') ?>" + <button type="button" title="<?= /* @escapeNotVerified */ __('Next page') ?>" class="action-next" onclick="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setPage('<?= /* @escapeNotVerified */ ($_curPage + 1) ?>');return false;"> <span><?= /* @escapeNotVerified */ __('Next page') ?></span> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml index a31bf4d23abaa..f97db4ad993b1 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml @@ -72,7 +72,7 @@ $numColumns = sizeof($block->getColumns()); <?php if ($block->getPagerVisibility()): ?> <div class="admin__data-grid-pager-wrap"> <select name="<?= /* @escapeNotVerified */ $block->getVarNameLimit() ?>" - id="<?= $block->escapeHTML($block->getHtmlId()) ?>_page-limit" + id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" onchange="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.loadByElement(this)" class="admin__control-select"> <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> @@ -91,7 +91,7 @@ $numColumns = sizeof($block->getColumns()); selected="selected"<?php endif; ?>>200 </option> </select> - <label for="<?= $block->escapeHTML($block->getHtmlId()) ?><?= $block->escapeHTML($block->getHtmlId()) ?>_page-limit" + <label for="<?= $block->escapeHtml($block->getHtmlId()) ?><?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" class="admin__control-support-text"><?= /* @escapeNotVerified */ __('per page') ?></label> <div class="admin__data-grid-pager"> @@ -107,12 +107,12 @@ $numColumns = sizeof($block->getColumns()); <button type="button" class="action-previous disabled"><span><?= /* @escapeNotVerified */ __('Previous page') ?></span></button> <?php endif; ?> <input type="text" - id="<?= $block->escapeHTML($block->getHtmlId()) ?>_page-current" + id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current" name="<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>" value="<?= /* @escapeNotVerified */ $_curPage ?>" class="admin__control-text" onkeypress="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @escapeNotVerified */ $_lastPage ?>')" <?= /* @escapeNotVerified */ $block->getUiId('current-page') ?> /> - <label class="admin__control-support-text" for="<?= $block->escapeHTML($block->getHtmlId()) ?>_page-current"> + <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> <?= /* @escapeNotVerified */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> <?php if ($_curPage < $_lastPage): ?> diff --git a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml index 19a4ab1388006..79c987383299f 100644 --- a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml @@ -12,7 +12,7 @@ </settings> <field name="theme_theme_id" sortOrder="10" formElement="select"> <settings> - <notice translate="true">If no value is specified, the system default is used. The system default may be modified by third party extensions.</notice> + <notice translate="true">If no value is specified, the system default is used. The system default may be modified by third-party extensions.</notice> <dataType>text</dataType> <label translate="true">Applied Theme</label> <dataScope>theme_theme_id</dataScope> @@ -92,7 +92,7 @@ </select> </formElements> </field> - <actionDelete template="Magento_Backend/dynamic-rows/cells/action-delete" sortOrder="50"> + <actionDelete template="Magento_Backend/dynamic-rows/cells/action-delete"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="fit" xsi:type="boolean">false</item> diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js index 03403a4ec4a04..119e7a35747cb 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js @@ -13,11 +13,19 @@ define([ 'jquery', 'mage/template', 'Magento_Ui/js/modal/alert', + 'Magento_Ui/js/form/element/file-uploader', 'mage/translate', 'jquery/file-uploader' -], function ($, mageTemplate, alert) { +], function ($, mageTemplate, alert, FileUploader) { 'use strict'; + var fileUploader = new FileUploader({ + dataScope: '', + isMultipleFiles: true + }); + + fileUploader.initUploader(); + $.widget('mage.mediaUploader', { /** @@ -25,9 +33,20 @@ define([ * @private */ _create: function () { - var - self = this, - progressTmpl = mageTemplate('[data-template="uploader"]'); + var self = this, + progressTmpl = mageTemplate('[data-template="uploader"]'), + isResizeEnabled = this.options.isResizeEnabled, + resizeConfiguration = { + action: 'resize', + maxWidth: this.options.maxWidth, + maxHeight: this.options.maxHeight + }; + + if (!isResizeEnabled) { + resizeConfiguration = { + action: 'resize' + }; + } this.element.find('input[type=file]').fileupload({ dataType: 'json', @@ -44,8 +63,7 @@ define([ * @param {Object} data */ add: function (e, data) { - var - fileSize, + var fileSize, tmpl; $.each(data.files, function (index, file) { @@ -79,10 +97,9 @@ define([ if (data.result && !data.result.error) { self.element.trigger('addItem', data.result); } else { - alert({ - content: $.mage.__('We don\'t recognize or support this file extension type.') - }); + fileUploader.aggregateError(data.files[0].name, data.result.error); } + self.element.find('#' + data.fileId).remove(); }, @@ -108,18 +125,18 @@ define([ .delay(2000) .hide('highlight') .remove(); - } + }, + + stop: fileUploader.uploaderConfig.stop }); this.element.find('input[type=file]').fileupload('option', { process: [{ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/ - }, { - action: 'resize', - maxWidth: this.options.maxWidth, - maxHeight: this.options.maxHeight - }, { + }, + resizeConfiguration, + { action: 'save' }] }); diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js b/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js index 0a692a9b868cc..c2a0d4dab1fb3 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js @@ -67,6 +67,7 @@ define([ * 'Confirm' action handler. */ confirm: function () { + $('body').trigger('processStart'); dataPost().postData(requestData); } } diff --git a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html index 74621806fa5fb..fe30ca7e83f19 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html +++ b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html @@ -5,7 +5,7 @@ */ --> -<div class="admin__field-complex" if="element.addButton"> +<div class="admin__field-complex" if="$data.addButton"> <div class="admin__field-complex-title"> <span class="label" translate="'User Agent Rules'"></span> </div> @@ -27,10 +27,10 @@ <div class="admin__field admin__field-wide" visible="visible" disabled="disabled" - css="element.setClasses(element)" + css="$data.setClasses($data)" attr="'data-index': index"> - <label if="element.label" class="admin__field-label" attr="for: element.uid"> - <span translate="element.label"/> + <label if="$data.label" class="admin__field-label" attr="for: $data.uid"> + <span translate="$data.label"/> </label> <div class="admin__field-control" data-role="grid-wrapper"> @@ -85,7 +85,7 @@ <div class="messages"> <div class="message message-notice notice"> <span - translate="'Search strings are either normal strings or regular exceptions (PCRE). They are matched in the same order as entered.'"></span> + translate="'Search strings are either normal strings or regular expressions (PCRE). They are matched in the same order as entered.'"></span> <br> <span translate="'Examples'"></span>: diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php index dcafbc7370d2d..0edeb5565f288 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php @@ -5,21 +5,27 @@ */ namespace Magento\Backup\Controller\Adminhtml; +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Backup\Helper\Data as Helper; +use Magento\Framework\App\ObjectManager; + /** * Backup admin controller * * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.AllPurposeAction) */ -abstract class Index extends \Magento\Backend\App\Action +abstract class Index extends Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Backend::backup'; + const ADMIN_RESOURCE = 'Magento_Backup::backup'; /** * Core registry @@ -48,6 +54,11 @@ abstract class Index extends \Magento\Backend\App\Action */ protected $maintenanceMode; + /** + * @var Helper + */ + private $helper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry @@ -55,6 +66,7 @@ abstract class Index extends \Magento\Backend\App\Action * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Backup\Model\BackupFactory $backupModelFactory * @param \Magento\Framework\App\MaintenanceMode $maintenanceMode + * @param Helper|null $helper */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -62,13 +74,27 @@ public function __construct( \Magento\Framework\Backup\Factory $backupFactory, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Backup\Model\BackupFactory $backupModelFactory, - \Magento\Framework\App\MaintenanceMode $maintenanceMode + \Magento\Framework\App\MaintenanceMode $maintenanceMode, + ?Helper $helper = null ) { $this->_coreRegistry = $coreRegistry; $this->_backupFactory = $backupFactory; $this->_fileFactory = $fileFactory; $this->_backupModelFactory = $backupModelFactory; $this->maintenanceMode = $maintenanceMode; + $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); parent::__construct($context); } + + /** + * @inheritDoc + */ + public function dispatch(\Magento\Framework\App\RequestInterface $request) + { + if (!$this->helper->isEnabled()) { + return $this->_redirect('*/*/disabled'); + } + + return parent::dispatch($request); + } } diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php index ee0370c8ecb29..53f45aff50cbc 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php @@ -12,14 +12,14 @@ class Create extends \Magento\Backup\Controller\Adminhtml\Index { /** - * Create backup action + * Create backup action. * * @return void|\Magento\Backend\App\Action * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { - if (!$this->getRequest()->isAjax()) { + if (!$this->isRequestAllowed()) { return $this->_redirect('*/*/index'); } @@ -82,7 +82,7 @@ public function execute() $backupManager->create(); - $this->messageManager->addSuccess($successMessage); + $this->messageManager->addSuccessMessage($successMessage); $response->setRedirectUrl($this->getUrl('*/*/index')); } catch (\Magento\Framework\Backup\Exception\NotEnoughFreeSpace $e) { @@ -106,4 +106,14 @@ public function execute() $this->getResponse()->representJson($response->toJson()); } + + /** + * Check if request is allowed. + * + * @return bool + */ + private function isRequestAllowed() + { + return $this->getRequest()->isAjax() && $this->getRequest()->isPost(); + } } diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php new file mode 100644 index 0000000000000..f6fe430ae0838 --- /dev/null +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backup\Controller\Adminhtml\Index; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\View\Result\PageFactory; + +/** + * Inform that backup is disabled. + */ +class Disabled extends Action implements HttpGetActionInterface +{ + /** + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::backup'; + + /** + * @var PageFactory + */ + private $pageFactory; + + /** + * @param Context $context + * @param PageFactory $pageFactory + */ + public function __construct(Context $context, PageFactory $pageFactory) + { + parent::__construct($context); + $this->pageFactory = $pageFactory; + } + + /** + * @inheritDoc + */ + public function execute() + { + return $this->pageFactory->create(); + } +} diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php index 3bbda65cb4cf6..271e3713034d0 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backup\Controller\Adminhtml\Index; -class Index extends \Magento\Backup\Controller\Adminhtml\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backup\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * Backup list action diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php index 04292d2759093..90657fc2490ba 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/MassDelete.php @@ -49,13 +49,13 @@ public function execute() $resultData->setIsSuccess(true); if ($allBackupsDeleted) { - $this->messageManager->addSuccess(__('You deleted the selected backup(s).')); + $this->messageManager->addSuccessMessage(__('You deleted the selected backup(s).')); } else { throw new \Exception($deleteFailMessage); } } catch (\Exception $e) { $resultData->setIsSuccess(false); - $this->messageManager->addError($deleteFailMessage); + $this->messageManager->addErrorMessage($deleteFailMessage); } return $this->_redirect('backup/*/index'); diff --git a/app/code/Magento/Backup/Cron/SystemBackup.php b/app/code/Magento/Backup/Cron/SystemBackup.php index 750262ab1c14a..9502377a39d06 100644 --- a/app/code/Magento/Backup/Cron/SystemBackup.php +++ b/app/code/Magento/Backup/Cron/SystemBackup.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Store\Model\ScopeInterface; +/** + * Performs scheduled backup. + */ class SystemBackup { const XML_PATH_BACKUP_ENABLED = 'system/backup/enabled'; @@ -101,6 +104,10 @@ public function __construct( */ public function execute() { + if (!$this->_backupData->isEnabled()) { + return $this; + } + if (!$this->_scopeConfig->isSetFlag(self::XML_PATH_BACKUP_ENABLED, ScopeInterface::SCOPE_STORE)) { return $this; } diff --git a/app/code/Magento/Backup/Helper/Data.php b/app/code/Magento/Backup/Helper/Data.php index 3d60bf9d9c9cf..c6df6a7366852 100644 --- a/app/code/Magento/Backup/Helper/Data.php +++ b/app/code/Magento/Backup/Helper/Data.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backup\Helper; use Magento\Framework\App\Filesystem\DirectoryList; @@ -110,7 +113,7 @@ public function getBackupsDir() public function getExtensionByType($type) { $extensions = $this->getExtensions(); - return isset($extensions[$type]) ? $extensions[$type] : ''; + return $extensions[$type] ?? ''; } /** @@ -285,4 +288,14 @@ public function extractDataFromFilename($filename) return $result; } + + /** + * Is backup functionality enabled. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag('system/backup/functionality_enabled'); + } } diff --git a/app/code/Magento/Backup/Model/BackupFactory.php b/app/code/Magento/Backup/Model/BackupFactory.php index 28b0a7baf43cb..f88b410a2371b 100644 --- a/app/code/Magento/Backup/Model/BackupFactory.php +++ b/app/code/Magento/Backup/Model/BackupFactory.php @@ -39,8 +39,8 @@ public function __construct(\Magento\Framework\ObjectManagerInterface $objectMan */ public function create($timestamp, $type) { - $fsCollection = $this->_objectManager->get(\Magento\Backup\Model\Fs\Collection::class); - $backupInstance = $this->_objectManager->get(\Magento\Backup\Model\Backup::class); + $fsCollection = $this->_objectManager->create(\Magento\Backup\Model\Fs\Collection::class); + $backupInstance = $this->_objectManager->create(\Magento\Backup\Model\Backup::class); foreach ($fsCollection as $backup) { if ($backup->getTime() === (int) $timestamp && $backup->getType() === $type) { diff --git a/app/code/Magento/Backup/Model/Config/Backend/Cron.php b/app/code/Magento/Backup/Model/Config/Backend/Cron.php index 4855ef1129502..2f0e4069f0499 100644 --- a/app/code/Magento/Backup/Model/Config/Backend/Cron.php +++ b/app/code/Magento/Backup/Model/Config/Backend/Cron.php @@ -76,8 +76,8 @@ public function afterSave() if ($enabled) { $cronExprArray = [ - intval($time[1]), # Minute - intval($time[0]), # Hour + (int) $time[1], # Minute + (int) $time[0], # Hour $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month '*', # Month of the Year $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php index 776141249f92b..bc458a0a8e4bf 100644 --- a/app/code/Magento/Backup/Model/Db.php +++ b/app/code/Magento/Backup/Model/Db.php @@ -5,11 +5,16 @@ */ namespace Magento\Backup\Model; +use Magento\Backup\Helper\Data as Helper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\RuntimeException; + /** * Database backup model * * @api * @since 100.0.2 + * @deprecated Backup module is to be removed. */ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface { @@ -33,16 +38,24 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface */ protected $_resource = null; + /** + * @var Helper + */ + private $helper; + /** * @param \Magento\Backup\Model\ResourceModel\Db $resourceDb * @param \Magento\Framework\App\ResourceConnection $resource + * @param Helper|null $helper */ public function __construct( \Magento\Backup\Model\ResourceModel\Db $resourceDb, - \Magento\Framework\App\ResourceConnection $resource + \Magento\Framework\App\ResourceConnection $resource, + ?Helper $helper = null ) { $this->_resourceDb = $resourceDb; $this->_resource = $resource; + $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); } /** @@ -63,6 +76,8 @@ public function getResource() } /** + * Tables list. + * * @return array */ public function getTables() @@ -71,6 +86,8 @@ public function getTables() } /** + * Command to recreate given table. + * * @param string $tableName * @param bool $addDropIfExists * @return string @@ -81,6 +98,8 @@ public function getTableCreateScript($tableName, $addDropIfExists = false) } /** + * Generate table's data dump. + * * @param string $tableName * @return string */ @@ -90,6 +109,8 @@ public function getTableDataDump($tableName) } /** + * Header for dumps. + * * @return string */ public function getHeader() @@ -98,6 +119,8 @@ public function getHeader() } /** + * Footer for dumps. + * * @return string */ public function getFooter() @@ -106,6 +129,8 @@ public function getFooter() } /** + * Get backup SQL. + * * @return string */ public function renderSql() @@ -124,13 +149,14 @@ public function renderSql() } /** - * Create backup and stream write to adapter - * - * @param \Magento\Framework\Backup\Db\BackupInterface $backup - * @return $this + * @inheritDoc */ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backup) { + if (!$this->helper->isEnabled()) { + throw new RuntimeException(__('Backup functionality is disabled')); + } + $backup->open(true); $this->getResource()->beginTransaction(); @@ -154,7 +180,7 @@ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backu if ($tableStatus->getDataLength() > self::BUFFER_LENGTH) { if ($tableStatus->getAvgRowLength() < self::BUFFER_LENGTH) { - $limit = floor(self::BUFFER_LENGTH / $tableStatus->getAvgRowLength()); + $limit = floor(self::BUFFER_LENGTH / max($tableStatus->getAvgRowLength(), 1)); $multiRowsLength = ceil($tableStatus->getRows() / $limit); } else { $limit = 1; @@ -173,13 +199,12 @@ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backu } } $backup->write($this->getResource()->getTableForeignKeysSql()); + $backup->write($this->getResource()->getTableTriggersSql()); $backup->write($this->getResource()->getFooter()); $this->getResource()->commitTransaction(); $backup->close(); - - return $this; } /** diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index 2d08ac04528ac..b17c17f7074fb 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -115,7 +115,8 @@ protected function _generateRow($filename) if (isset($row['display_name']) && $row['display_name'] == '') { $row['display_name'] = 'WebSetupWizard'; } - $row['id'] = $row['time'] . '_' . $row['type'] . (isset($row['display_name']) ? $row['display_name'] : ''); + $row['id'] = $row['time'] . '_' . $row['type'] + . (isset($row['display_name']) ? '_' . $row['display_name'] : ''); return $row; } } diff --git a/app/code/Magento/Backup/Model/ResourceModel/Db.php b/app/code/Magento/Backup/Model/ResourceModel/Db.php index 3fbaf44ebb063..6e7d6f9863f33 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Db.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Db.php @@ -114,6 +114,31 @@ public function getTableForeignKeysSql($tableName = null) return $fkScript; } + /** + * Return triggers for table(s). + * + * @param string|null $tableName + * @param bool $addDropIfExists + * @return string + */ + public function getTableTriggersSql($tableName = null, $addDropIfExists = true) + { + $triggerScript = ''; + if (!$tableName) { + $tables = $this->getTables(); + foreach ($tables as $table) { + $tableTriggerScript = $this->_resourceHelper->getTableTriggersSql($table, $addDropIfExists); + if (!empty($tableTriggerScript)) { + $triggerScript .= "\n" . $tableTriggerScript; + } + } + } else { + $triggerScript = $this->getTableTriggersSql($tableName, $addDropIfExists); + } + + return $triggerScript; + } + /** * Retrieve table status * diff --git a/app/code/Magento/Backup/Model/ResourceModel/Helper.php b/app/code/Magento/Backup/Model/ResourceModel/Helper.php index b5418865339c3..dace56fd60688 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Helper.php @@ -337,4 +337,40 @@ public function restoreTransactionIsolationLevel() { $this->getConnection()->query('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ'); } + + /** + * Get create script for triggers. + * + * @param string $tableName + * @param boolean $addDropIfExists + * @param boolean $stripDefiner + * @return string + */ + public function getTableTriggersSql($tableName, $addDropIfExists = false, $stripDefiner = true) + { + $script = "--\n-- Triggers structure for table `{$tableName}`\n--\n"; + $triggers = $this->getConnection()->query('SHOW TRIGGERS LIKE \''. $tableName . '\'')->fetchAll(); + + if (!$triggers) { + return ''; + } + foreach ($triggers as $trigger) { + if ($addDropIfExists) { + $script .= 'DROP TRIGGER IF EXISTS ' . $trigger['Trigger'] . ";\n"; + } + $script .= "delimiter ;;\n"; + + $triggerData = $this->getConnection()->query('SHOW CREATE TRIGGER '. $trigger['Trigger'])->fetch(); + if ($stripDefiner) { + $cleanedScript = preg_replace('/DEFINER=[^\s]*/', '', $triggerData['SQL Original Statement']); + $script .= $cleanedScript . "\n"; + } else { + $script .= $triggerData['SQL Original Statement'] . "\n"; + } + $script .= ";;\n"; + $script .= "delimiter ;\n"; + } + + return $script; + } } diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/CreateBackupActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/CreateBackupActionGroup.xml new file mode 100644 index 0000000000000..89381112e6c66 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/CreateBackupActionGroup.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <actionGroup name="createSystemBackup"> + <arguments> + <argument name="backup" defaultValue="SystemBackup"/> + </arguments> + <click selector="{{AdminMainActionsSection.systemBackup}}" stepKey="clickCreateBackupButton"/> + <waitForElementVisible selector="{{AdminCreateBackupFormSection.backupNameField}}" stepKey="waitForForm"/> + <fillField selector="{{AdminCreateBackupFormSection.backupNameField}}" userInput="{{backup.name}}" stepKey="fillBackupName"/> + <click selector="{{AdminCreateBackupFormSection.ok}}" stepKey="clickOk"/> + <waitForElementNotVisible selector=".loading-mask" time="300" stepKey="waitForBackupProcess"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the system backup." stepKey="seeSuccessMessage"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> + <see selector="{{AdminGridTableSection.backupTypeByName(backup.name)}}" userInput="{{backup.type}}" stepKey="seeBackupType"/> + </actionGroup> + + <actionGroup name="createMediaBackup"> + <arguments> + <argument name="backup" defaultValue="MediaBackup"/> + </arguments> + <click selector="{{AdminMainActionsSection.mediaBackup}}" stepKey="clickCreateBackupButton"/> + <waitForElementVisible selector="{{AdminCreateBackupFormSection.backupNameField}}" stepKey="waitForForm"/> + <fillField selector="{{AdminCreateBackupFormSection.backupNameField}}" userInput="{{backup.name}}" stepKey="fillBackupName"/> + <click selector="{{AdminCreateBackupFormSection.ok}}" stepKey="clickOk"/> + <waitForPageLoad time="120" stepKey="waitForBackupProcess"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the database and media backup." stepKey="seeSuccessMessage"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> + <see selector="{{AdminGridTableSection.backupTypeByName(backup.name)}}" userInput="{{backup.type}}" stepKey="seeBackupType"/> + </actionGroup> + + <actionGroup name="createDatabaseBackup"> + <arguments> + <argument name="backup" defaultValue="DatabaseBackup"/> + </arguments> + <click selector="{{AdminMainActionsSection.databaseBackup}}" stepKey="clickCreateBackupButton"/> + <waitForElementVisible selector="{{AdminCreateBackupFormSection.backupNameField}}" stepKey="waitForForm"/> + <fillField selector="{{AdminCreateBackupFormSection.backupNameField}}" userInput="{{backup.name}}" stepKey="fillBackupName"/> + <click selector="{{AdminCreateBackupFormSection.ok}}" stepKey="clickOk"/> + <waitForPageLoad time="120" stepKey="waitForBackupProcess"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the database backup." stepKey="seeSuccessMessage"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> + <see selector="{{AdminGridTableSection.backupTypeByName(backup.name)}}" userInput="{{backup.type}}" stepKey="seeBackupType"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml new file mode 100644 index 0000000000000..4f34f24c3a806 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <actionGroup name="deleteBackup"> + <arguments> + <argument name="backup"/> + </arguments> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="seeBackupInGrid"/> + <click selector="{{AdminGridTableSection.backupRowCheckbox(backup.name)}}" stepKey="selectBackupRow"/> + <selectOption selector="{{AdminGridActionSection.actionSelect}}" userInput="Delete" stepKey="selectDeleteAction"/> + <click selector="{{AdminGridActionSection.submitButton}}" stepKey="clickSubmit"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to delete the selected backup(s)?" stepKey="seeConfirmationModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkConfirmDelete"/> + <dontSee selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="dontSeeBackupInGrid"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml new file mode 100644 index 0000000000000..ae97351cafcaf --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SystemBackup" type="backup"> + <data key="name" unique="suffix">systemBackup</data> + <data key="type">System</data> + </entity> + <entity name="MediaBackup" type="backup"> + <data key="name" unique="suffix">mediaBackup</data> + <data key="type">Database and Media</data> + </entity> + <entity name="DatabaseBackup" type="backup"> + <data key="name" unique="suffix">databaseBackup</data> + <data key="type">Database</data> + </entity> +</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CacheInvalidate/LICENSE.txt b/app/code/Magento/Backup/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CacheInvalidate/LICENSE.txt rename to app/code/Magento/Backup/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/BundleImportExport/LICENSE_AFL.txt b/app/code/Magento/Backup/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/BundleImportExport/LICENSE_AFL.txt rename to app/code/Magento/Backup/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Backup/Test/Mftf/Page/BackupIndexPage.xml b/app/code/Magento/Backup/Test/Mftf/Page/BackupIndexPage.xml new file mode 100644 index 0000000000000..aad29e6e6d566 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Page/BackupIndexPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="BackupIndexPage" url="/backup/index/" area="admin" module="Magento_Backup"> + <section name="AdminMainActionsSection"/> + <section name="AdminGridTableSection"/> + <section name="AdminCreateBackupFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Backup/Test/Mftf/README.md b/app/code/Magento/Backup/Test/Mftf/README.md new file mode 100644 index 0000000000000..6951acdf41400 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Backup Functional Tests + +The Functional Test Module for **Magento Backup** module. diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminCreateBackupFormSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminCreateBackupFormSection.xml new file mode 100644 index 0000000000000..af88146bcfb4b --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminCreateBackupFormSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCreateBackupFormSection"> + <element name="backupNameField" type="input" selector="input[name='backup_name']"/> + <element name="maintenanceModeCheckbox" type="checkbox" selector="input[name='maintenance_mode']"/> + <element name="excludeMediaCheckbox" type="checkbox" selector="input[name='exclude_media']"/> + <element name="ok" type="button" selector=".modal-header button.primary"/> + <element name="cancel" type="button" selector=".modal-header button.cancel"/> + </section> +</sections> diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminGridActionSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridActionSection.xml new file mode 100644 index 0000000000000..cca6b428a2820 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridActionSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminGridActionSection"> + <element name="actionSelect" type="select" selector="#backupsGrid_massaction-select"/> + <element name="submitButton" type="button" selector="#backupsGrid_massaction button[title='Submit']"/> + </section> +</sections> diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminGridTableSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridTableSection.xml new file mode 100644 index 0000000000000..72fb51684931f --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminGridTableSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminGridTableSection"> + <element name="backupNameColumn" type="text" selector="table.data-grid td[data-column='display_name']"/> + <element name="backupTypeByName" type="text" selector="//table//td[contains(., '{{name}}')]/parent::tr/td[@data-column='type']" parameterized="true"/> + <element name="backupRowCheckbox" type="checkbox" selector="//table//td[contains(., '{{name}}')]/parent::tr/td[@data-column='massaction']//input" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Backup/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backup/Test/Mftf/Section/AdminMainActionsSection.xml new file mode 100644 index 0000000000000..11ba79f32b5db --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Section/AdminMainActionsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMainActionsSection"> + <element name="systemBackup" type="button" selector="button[data-ui-id*='createsnapshotbutton']"/> + <element name="mediaBackup" type="button" selector="button[data-ui-id*='createmediabackupbutton']"/> + <element name="databaseBackup" type="button" selector="button.database-backup[data-ui-id*='createbutton']"/> + </section> +</sections> diff --git a/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml b/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml new file mode 100644 index 0000000000000..26f8817c0a1bb --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateAndDeleteBackupsTest"> + <annotations> + <features value="Backup"/> + <stories value="Create and delete backups"/> + <title value="Create and delete backups"/> + <description value="An admin user can create a backup of each type and delete each backup."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94176"/> + <group value="backup"/> + <skip> + <issueId value="MC-5807"/> + </skip> + </annotations> + + <!--Login to admin area--> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + + <!--Go to backup index page--> + <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupPage"/> + <waitForPageLoad stepKey="waitForBackupPage"/> + + <!--Create system backup--> + <actionGroup ref="createSystemBackup" stepKey="createSystemBackup"/> + + <!--Create database/media backup--> + <actionGroup ref="createMediaBackup" stepKey="createMediaBackup"/> + + <!--Create database backup--> + <actionGroup ref="createDatabaseBackup" stepKey="createDatabaseBackup"/> + + <!--Delete system backup--> + <actionGroup ref="deleteBackup" stepKey="deleteSystemBackup"> + <argument name="backup" value="SystemBackup"/> + </actionGroup> + + <!--Delete database/media backup--> + <actionGroup ref="deleteBackup" stepKey="deleteMediaBackup"> + <argument name="backup" value="MediaBackup"/> + </actionGroup> + + <!--Delete database backup--> + <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> + <argument name="backup" value="DatabaseBackup"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/CreateTest.php b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/CreateTest.php new file mode 100644 index 0000000000000..80477d9f6f9e4 --- /dev/null +++ b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/CreateTest.php @@ -0,0 +1,240 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backup\Test\Unit\Controller\Adminhtml\Index; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Tests \Magento\Backup\Controller\Adminhtml\Index\Create class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreateTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \Magento\Backend\App\Action\Context + */ + private $context; + + /** + * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseMock; + + /** + * @var \Magento\Backup\Model\Backup|\PHPUnit_Framework_MockObject_MockObject + */ + private $backupModelMock; + + /** + * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataBackendHelperMock; + + /** + * @var \Magento\Backup\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataBackupHelperMock; + + /** + * @var \Magento\Framework\App\Response\Http\FileFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileFactoryMock; + + /** + * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionMock; + + /** + * @var \Magento\Framework\App\MaintenanceMode|\PHPUnit_Framework_MockObject_MockObject + */ + private $maintenanceModeMock; + + /** + * @var \Magento\Framework\Backup\Factory|\PHPUnit_Framework_MockObject_MockObject + */ + private $backupFactoryMock; + + /** + * @var \Magento\Backup\Controller\Adminhtml\Index\Create + */ + private $createController; + + public function setUp() + { + $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->getMock(); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + ->disableOriginalConstructor() + ->setMethods(['isAjax', 'isPost', 'getParam']) + ->getMock(); + $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http::class) + ->disableOriginalConstructor() + ->setMethods(['representJson', 'setRedirect']) + ->getMock(); + $this->sessionMock = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->backupFactoryMock = $this->getMockBuilder(\Magento\Framework\Backup\Factory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->backupModelMock = $this->getMockBuilder(\Magento\Backup\Model\Backup::class) + ->disableOriginalConstructor() + ->setMethods(['setBackupExtension', 'setTime', 'setBackupsDir', 'setName', 'create']) + ->getMock(); + $this->dataBackendHelperMock = $this->getMockBuilder(\Magento\Backend\Helper\Data::class) + ->disableOriginalConstructor() + ->setMethods(['getUrl']) + ->getMock(); + $this->dataBackupHelperMock = $this->getMockBuilder(\Magento\Backup\Helper\Data::class) + ->disableOriginalConstructor() + ->setMethods(['getExtensionByType', 'getBackupsDir']) + ->getMock(); + $this->maintenanceModeMock = $this->getMockBuilder(\Magento\Framework\App\MaintenanceMode::class) + ->disableOriginalConstructor() + ->setMethods(['set']) + ->getMock(); + $this->fileFactoryMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http\FileFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManager = new ObjectManager($this); + $this->context = $this->objectManager->getObject( + \Magento\Backend\App\Action\Context::class, + [ + 'objectManager' => $this->objectManagerMock, + 'request' => $this->requestMock, + 'response' => $this->responseMock, + 'session' => $this->sessionMock, + 'helper' => $this->dataBackendHelperMock, + 'maintenanceMode' => $this->maintenanceModeMock, + ] + ); + $this->createController = $this->objectManager->getObject( + \Magento\Backup\Controller\Adminhtml\Index\Create::class, + [ + 'context' => $this->context, + 'backupFactory' => $this->backupFactoryMock, + 'fileFactory' => $this->fileFactoryMock, + ] + ); + } + + /** + * @covers \Magento\Backup\Controller\Adminhtml\Index\Create::execute + * @return void + */ + public function testExecuteNotPost() + { + $redirectUrl = '*/*/index'; + $redirectUrlBackup = 'backup/index/index'; + + $this->requestMock->expects($this->any()) + ->method('isAjax') + ->willReturn(true); + $this->requestMock->expects($this->any()) + ->method('isPost') + ->willReturn(false); + $this->dataBackendHelperMock->expects($this->any()) + ->method('getUrl') + ->with($redirectUrl, []) + ->willReturn($redirectUrlBackup); + $this->responseMock->expects($this->any()) + ->method('setRedirect') + ->with($redirectUrlBackup) + ->willReturnSelf(); + + $this->assertSame($this->responseMock, $this->createController->execute()); + } + + /** + * @covers \Magento\Backup\Controller\Adminhtml\Index\Create::execute + * @return void + */ + public function testExecutePermission() + { + $redirectUrl = '*/*/index'; + $redirectUrlBackup = 'backup/index/index'; + $backupType = 'db'; + $backupName = 'backup1'; + $response = '{"redirect_url":"backup\/index\/index"}'; + + $this->requestMock->expects($this->any()) + ->method('isAjax') + ->willReturn(true); + $this->requestMock->expects($this->any()) + ->method('isPost') + ->willReturn(true); + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['type', null, $backupType], + ['backup_name', null, $backupName], + ]); + $this->dataBackendHelperMock->expects($this->any()) + ->method('getUrl') + ->with($redirectUrl, []) + ->willReturn($redirectUrlBackup); + $this->responseMock->expects($this->any()) + ->method('representJson') + ->with($response) + ->willReturnSelf(); + $this->maintenanceModeMock->expects($this->any()) + ->method('set') + ->with(true) + ->willReturn(false); + $this->backupFactoryMock->expects($this->any()) + ->method('create') + ->with($backupType) + ->willReturn($this->backupModelMock); + $this->backupModelMock->expects($this->any()) + ->method('setBackupExtension') + ->with($backupType) + ->willReturnSelf(); + $this->backupModelMock->expects($this->any()) + ->method('setBackupsDir') + ->willReturnSelf(); + $this->backupModelMock->expects($this->any()) + ->method('setTime') + ->willReturnSelf(); + $this->backupModelMock->expects($this->any()) + ->method('setName') + ->with($backupName) + ->willReturnSelf(); + $this->backupModelMock->expects($this->once()) + ->method('create') + ->willReturnSelf(); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(\Magento\Backup\Helper\Data::class) + ->willReturn($this->dataBackupHelperMock); + $this->dataBackupHelperMock->expects($this->any()) + ->method('getExtensionByType') + ->with($backupType) + ->willReturn($backupType); + $this->dataBackupHelperMock->expects($this->any()) + ->method('getBackupsDir') + ->willReturn('dir'); + + $this->assertNull($this->createController->execute()); + } +} diff --git a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php index b7dfb30c0a1b3..56a7ef42a0bc2 100644 --- a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php +++ b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php @@ -3,134 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backup\Test\Unit\Cron; +use Magento\Backup\Cron\SystemBackup; +use PHPUnit\Framework\TestCase; +use Magento\Backup\Helper\Data as Helper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -class SystemBackupTest extends \PHPUnit\Framework\TestCase +class SystemBackupTest extends TestCase { /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - private $objectManager; - - /** - * @var \Magento\Backup\Cron\SystemBackup - */ - private $systemBackup; - - /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $scopeConfigMock; - - /** - * @var \Magento\Backup\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - private $backupDataMock; - - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - private $registryMock; - - /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Helper|\PHPUnit_Framework_MockObject_MockObject */ - private $loggerMock; + private $helperMock; /** - * Filesystem facade - * - * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + * @var SystemBackup */ - private $filesystemMock; + private $cron; /** - * @var \Magento\Framework\Backup\Factory|\PHPUnit_Framework_MockObject_MockObject + * @inheritDoc */ - private $backupFactoryMock; - - /** - * @var \Magento\Framework\App\MaintenanceMode|\PHPUnit_Framework_MockObject_MockObject - */ - private $maintenanceModeMock; - - /** - * @var \Magento\Framework\Backup\Db|\PHPUnit_Framework_MockObject_MockObject - */ - private $backupDbMock; - - /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $objectManagerMock; - protected function setUp() { - $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) - ->getMock(); - $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->getMock(); - $this->backupDataMock = $this->getMockBuilder(\Magento\Backup\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) - ->getMock(); - $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->backupFactoryMock = $this->getMockBuilder(\Magento\Framework\Backup\Factory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->maintenanceModeMock = $this->getMockBuilder(\Magento\Framework\App\MaintenanceMode::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->backupDbMock = $this->getMockBuilder(\Magento\Framework\Backup\Db::class) - ->disableOriginalConstructor() - ->getMock(); - $this->backupDbMock->expects($this->any())->method('setBackupExtension')->willReturnSelf(); - $this->backupDbMock->expects($this->any())->method('setTime')->willReturnSelf(); - $this->backupDbMock->expects($this->any())->method('setBackupsDir')->willReturnSelf(); - - $this->objectManager = new ObjectManager($this); - $this->systemBackup = $this->objectManager->getObject( - \Magento\Backup\Cron\SystemBackup::class, - [ - 'backupData' => $this->backupDataMock, - 'coreRegistry' => $this->registryMock, - 'logger' => $this->loggerMock, - 'scopeConfig' => $this->scopeConfigMock, - 'filesystem' => $this->filesystemMock, - 'backupFactory' => $this->backupFactoryMock, - 'maintenanceMode' => $this->maintenanceModeMock, - ] - ); + $objectManager = new ObjectManager($this); + $this->helperMock = $this->getMockBuilder(Helper::class)->disableOriginalConstructor()->getMock(); + $this->cron = $objectManager->getObject(SystemBackup::class, ['backupData' => $this->helperMock]); } /** - * @expectedException \Exception + * Test that cron doesn't do anything if backups are disabled. */ - public function testExecuteThrowsException() + public function testDisabled() { - $type = 'db'; - $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(true); - - $this->scopeConfigMock->expects($this->once())->method('getValue') - ->with('system/backup/type', 'store') - ->willReturn($type); - - $this->backupFactoryMock->expects($this->once())->method('create')->willReturn($this->backupDbMock); - - $this->backupDbMock->expects($this->once())->method('create')->willThrowException(new \Exception); - - $this->backupDataMock->expects($this->never())->method('getCreateSuccessMessageByType')->with($type); - $this->loggerMock->expects($this->never())->method('info'); - - $this->systemBackup->execute(); + $this->helperMock->expects($this->any())->method('isEnabled')->willReturn(false); + $this->cron->execute(); } } diff --git a/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php b/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php index 629028bfd6f15..abf5e63276afa 100644 --- a/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php +++ b/app/code/Magento/Backup/Test/Unit/Model/BackupFactoryTest.php @@ -56,7 +56,7 @@ protected function setUp() $this->_objectManager->expects( $this->at(0) )->method( - 'get' + 'create' )->with( \Magento\Backup\Model\Fs\Collection::class )->will( @@ -65,7 +65,7 @@ protected function setUp() $this->_objectManager->expects( $this->at(1) )->method( - 'get' + 'create' )->with( \Magento\Backup\Model\Backup::class )->will( diff --git a/app/code/Magento/Backup/composer.json b/app/code/Magento/Backup/composer.json index 5ae2ad06b7d01..81225430b0fc8 100644 --- a/app/code/Magento/Backup/composer.json +++ b/app/code/Magento/Backup/composer.json @@ -5,14 +5,13 @@ "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-cron": "100.3.*", - "magento/module-store": "100.3.*" + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-cron": "*", + "magento/module-store": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml index 4028452d04439..90f6fa861b40f 100644 --- a/app/code/Magento/Backup/etc/adminhtml/system.xml +++ b/app/code/Magento/Backup/etc/adminhtml/system.xml @@ -9,13 +9,21 @@ <system> <section id="system"> <group id="backup" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Scheduled Backup Settings</label> + <label>Backup Settings</label> + <field id="functionality_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable Backup</label> + <comment>Disabled by default for security reasons</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Enable Scheduled Backup</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="functionality_enabled">1</field> + </depends> </field> <field id="type" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Backup Type</label> + <label>Scheduled Backup Type</label> <depends> <field id="enabled">1</field> </depends> diff --git a/app/code/Magento/Backup/etc/config.xml b/app/code/Magento/Backup/etc/config.xml new file mode 100644 index 0000000000000..fb0808983b9c8 --- /dev/null +++ b/app/code/Magento/Backup/etc/config.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <backup> + <functionality_enabled>0</functionality_enabled> + </backup> + </system> + </default> +</config> diff --git a/app/code/Magento/Backup/etc/module.xml b/app/code/Magento/Backup/etc/module.xml index 667ec9d8a7461..3e9906a5ecd74 100644 --- a/app/code/Magento/Backup/etc/module.xml +++ b/app/code/Magento/Backup/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Backup" setup_version="2.0.0"> + <module name="Magento_Backup" > <sequence> <module name="Magento_Store"/> </sequence> diff --git a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml new file mode 100644 index 0000000000000..3470f528e5ceb --- /dev/null +++ b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <title>Backup functionality is disabled + + + + + + + diff --git a/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml b/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml new file mode 100644 index 0000000000000..a5308dce5cc52 --- /dev/null +++ b/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml @@ -0,0 +1,7 @@ + +

Backup functionality is currently disabled. Please use other means for backups

diff --git a/app/code/Magento/Braintree/Block/Form.php b/app/code/Magento/Braintree/Block/Form.php index f6cf62f5a2131..8a727ae87262f 100644 --- a/app/code/Magento/Braintree/Block/Form.php +++ b/app/code/Magento/Braintree/Block/Form.php @@ -9,7 +9,6 @@ use Magento\Braintree\Gateway\Config\Config as GatewayConfig; use Magento\Braintree\Model\Adminhtml\Source\CcType; use Magento\Braintree\Model\Ui\ConfigProvider; -use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\Template\Context; use Magento\Payment\Block\Form\Cc; use Magento\Payment\Helper\Data; @@ -21,7 +20,6 @@ */ class Form extends Cc { - /** * @var Quote */ @@ -48,6 +46,7 @@ class Form extends Cc * @param Quote $sessionQuote * @param GatewayConfig $gatewayConfig * @param CcType $ccType + * @param Data $paymentDataHelper * @param array $data */ public function __construct( @@ -56,12 +55,14 @@ public function __construct( Quote $sessionQuote, GatewayConfig $gatewayConfig, CcType $ccType, + Data $paymentDataHelper, array $data = [] ) { parent::__construct($context, $paymentConfig, $data); $this->sessionQuote = $sessionQuote; $this->gatewayConfig = $gatewayConfig; $this->ccType = $ccType; + $this->paymentDataHelper = $paymentDataHelper; } /** @@ -81,7 +82,7 @@ public function getCcAvailableTypes() */ public function useCvv() { - return $this->gatewayConfig->isCvvEnabled(); + return $this->gatewayConfig->isCvvEnabled($this->sessionQuote->getStoreId()); } /** @@ -90,9 +91,8 @@ public function useCvv() */ public function isVaultEnabled() { - $storeId = $this->_storeManager->getStore()->getId(); $vaultPayment = $this->getVaultPayment(); - return $vaultPayment->isActive($storeId); + return $vaultPayment->isActive($this->sessionQuote->getStoreId()); } /** @@ -102,7 +102,10 @@ public function isVaultEnabled() private function getConfiguredCardTypes() { $types = $this->ccType->getCcTypeLabelMap(); - $configCardTypes = array_fill_keys($this->gatewayConfig->getAvailableCardTypes(), ''); + $configCardTypes = array_fill_keys( + $this->gatewayConfig->getAvailableCardTypes($this->sessionQuote->getStoreId()), + '' + ); return array_intersect_key($types, $configCardTypes); } @@ -116,7 +119,11 @@ private function getConfiguredCardTypes() private function filterCardTypesForCountry(array $configCardTypes, $countryId) { $filtered = $configCardTypes; - $countryCardTypes = $this->gatewayConfig->getCountryAvailableCardTypes($countryId); + $countryCardTypes = $this->gatewayConfig->getCountryAvailableCardTypes( + $countryId, + $this->sessionQuote->getStoreId() + ); + // filter card types only if specific card types are set for country if (!empty($countryCardTypes)) { $availableTypes = array_fill_keys($countryCardTypes, ''); @@ -131,19 +138,6 @@ private function filterCardTypesForCountry(array $configCardTypes, $countryId) */ private function getVaultPayment() { - return $this->getPaymentDataHelper()->getMethodInstance(ConfigProvider::CC_VAULT_CODE); - } - - /** - * Get payment data helper instance - * @return Data - * @deprecated 100.1.0 - */ - private function getPaymentDataHelper() - { - if ($this->paymentDataHelper === null) { - $this->paymentDataHelper = ObjectManager::getInstance()->get(Data::class); - } - return $this->paymentDataHelper; + return $this->paymentDataHelper->getMethodInstance(ConfigProvider::CC_VAULT_CODE); } } diff --git a/app/code/Magento/Braintree/Block/Payment.php b/app/code/Magento/Braintree/Block/Payment.php index 8e05856d9b57a..1ba2f862e2fe5 100644 --- a/app/code/Magento/Braintree/Block/Payment.php +++ b/app/code/Magento/Braintree/Block/Payment.php @@ -48,6 +48,10 @@ public function getPaymentConfig() $payment = $this->config->getConfig()['payment']; $config = $payment[$this->getCode()]; $config['code'] = $this->getCode(); + $config['clientTokenUrl'] = $this->_urlBuilder->getUrl( + 'braintree/payment/getClientToken', + ['_secure' => true] + ); return json_encode($config, JSON_UNESCAPED_SLASHES); } diff --git a/app/code/Magento/Braintree/Controller/Adminhtml/Payment/GetClientToken.php b/app/code/Magento/Braintree/Controller/Adminhtml/Payment/GetClientToken.php new file mode 100644 index 0000000000000..4b9721a693f08 --- /dev/null +++ b/app/code/Magento/Braintree/Controller/Adminhtml/Payment/GetClientToken.php @@ -0,0 +1,76 @@ +config = $config; + $this->adapterFactory = $adapterFactory; + $this->quoteSession = $quoteSession; + } + + /** + * @inheritdoc + */ + public function execute() + { + $params = []; + $response = $this->resultFactory->create(ResultFactory::TYPE_JSON); + + $storeId = $this->quoteSession->getStoreId(); + $merchantAccountId = $this->config->getMerchantAccountId($storeId); + if (!empty($merchantAccountId)) { + $params[PaymentDataBuilder::MERCHANT_ACCOUNT_ID] = $merchantAccountId; + } + + $clientToken = $this->adapterFactory->create($storeId) + ->generate($params); + $response->setData(['clientToken' => $clientToken]); + + return $response; + } +} diff --git a/app/code/Magento/Braintree/Controller/Payment/GetNonce.php b/app/code/Magento/Braintree/Controller/Payment/GetNonce.php index aecde869ca196..f8b152ded1556 100644 --- a/app/code/Magento/Braintree/Controller/Payment/GetNonce.php +++ b/app/code/Magento/Braintree/Controller/Payment/GetNonce.php @@ -62,7 +62,10 @@ public function execute() try { $publicHash = $this->getRequest()->getParam('public_hash'); $customerId = $this->session->getCustomerId(); - $result = $this->command->execute(['public_hash' => $publicHash, 'customer_id' => $customerId])->get(); + $result = $this->command->execute( + ['public_hash' => $publicHash, 'customer_id' => $customerId, 'store_id' => $this->session->getStoreId()] + ) + ->get(); $response->setData(['paymentMethodNonce' => $result['paymentMethodNonce']]); } catch (\Exception $e) { $this->logger->critical($e); diff --git a/app/code/Magento/Braintree/Controller/Paypal/AbstractAction.php b/app/code/Magento/Braintree/Controller/Paypal/AbstractAction.php index d6a81eefc6fdb..e0ed996019576 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/AbstractAction.php +++ b/app/code/Magento/Braintree/Controller/Paypal/AbstractAction.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Braintree\Controller\Paypal; use Magento\Checkout\Model\Session; @@ -73,7 +74,7 @@ public function dispatch(RequestInterface $request) protected function validateQuote($quote) { if (!$quote || !$quote->getItemsCount()) { - throw new \InvalidArgumentException(__('We can\'t initialize checkout.')); + throw new \InvalidArgumentException(__('Checkout failed to initialize. Verify and try again.')); } } } diff --git a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php index 1acd16708ff42..418cb93900610 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php +++ b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php @@ -9,6 +9,7 @@ use Magento\Braintree\Model\Paypal\Helper; use Magento\Checkout\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; @@ -17,7 +18,7 @@ /** * Class PlaceOrder */ -class PlaceOrder extends AbstractAction +class PlaceOrder extends AbstractAction implements HttpPostActionInterface { /** * @var Helper\OrderPlace @@ -54,6 +55,7 @@ public function __construct( /** * @inheritdoc + * * @throws LocalizedException */ public function execute() @@ -71,7 +73,10 @@ public function execute() return $resultRedirect->setPath('checkout/onepage/success', ['_secure' => true]); } catch (\Exception $e) { $this->logger->critical($e); - $this->messageManager->addExceptionMessage($e, $e->getMessage()); + $this->messageManager->addExceptionMessage( + $e, + 'The order #' . $quote->getReservedOrderId() . ' cannot be processed.' + ); } return $resultRedirect->setPath('checkout/cart', ['_secure' => true]); diff --git a/app/code/Magento/Braintree/Controller/Paypal/Review.php b/app/code/Magento/Braintree/Controller/Paypal/Review.php index 4576e3b033df8..14ec829d98024 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/Review.php +++ b/app/code/Magento/Braintree/Controller/Paypal/Review.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Braintree\Controller\Paypal; use Magento\Checkout\Model\Session; @@ -11,11 +12,12 @@ use Magento\Braintree\Gateway\Config\PayPal\Config; use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Class Review */ -class Review extends AbstractAction +class Review extends AbstractAction implements HttpPostActionInterface { /** * @var QuoteUpdater @@ -59,14 +61,14 @@ public function execute() try { $this->validateQuote($quote); - if ($this->validateRequestData($requestData)) { + if ($requestData && $this->validateRequestData($requestData)) { $this->quoteUpdater->execute( $requestData['nonce'], $requestData['details'], $quote ); } elseif (!$quote->getPayment()->getAdditionalInformation(self::$paymentMethodNonce)) { - throw new LocalizedException(__('We can\'t initialize checkout.')); + throw new LocalizedException(__('Checkout failed to initialize. Verify and try again.')); } /** @var \Magento\Framework\View\Result\Page $resultPage */ @@ -90,6 +92,8 @@ public function execute() } /** + * Validate request data + * * @param array $requestData * @return boolean */ diff --git a/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php b/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php index f972eecf9f92d..de439aad2defd 100644 --- a/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php +++ b/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php @@ -6,18 +6,19 @@ namespace Magento\Braintree\Gateway\Command; use Braintree\Transaction; -use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Braintree\Model\Adapter\BraintreeSearchAdapter; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Payment\Gateway\Command; use Magento\Payment\Gateway\Command\CommandPoolInterface; use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Payment\Gateway\Helper\ContextHelper; -use Magento\Braintree\Gateway\Helper\SubjectReader; use Magento\Sales\Api\Data\OrderPaymentInterface; -use Magento\Sales\Api\TransactionRepositoryInterface; use Magento\Sales\Api\Data\TransactionInterface; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; /** * Class CaptureStrategyCommand @@ -66,9 +67,9 @@ class CaptureStrategyCommand implements CommandInterface private $subjectReader; /** - * @var BraintreeAdapter + * @var BraintreeAdapterFactory */ - private $braintreeAdapter; + private $braintreeAdapterFactory; /** * @var BraintreeSearchAdapter @@ -83,7 +84,7 @@ class CaptureStrategyCommand implements CommandInterface * @param FilterBuilder $filterBuilder * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param SubjectReader $subjectReader - * @param BraintreeAdapter $braintreeAdapter + * @param BraintreeAdapterFactory $braintreeAdapterFactory, * @param BraintreeSearchAdapter $braintreeSearchAdapter */ public function __construct( @@ -92,7 +93,7 @@ public function __construct( FilterBuilder $filterBuilder, SearchCriteriaBuilder $searchCriteriaBuilder, SubjectReader $subjectReader, - BraintreeAdapter $braintreeAdapter, + BraintreeAdapterFactory $braintreeAdapterFactory, BraintreeSearchAdapter $braintreeSearchAdapter ) { $this->commandPool = $commandPool; @@ -100,7 +101,7 @@ public function __construct( $this->filterBuilder = $filterBuilder; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->subjectReader = $subjectReader; - $this->braintreeAdapter = $braintreeAdapter; + $this->braintreeAdapterFactory = $braintreeAdapterFactory; $this->braintreeSearchAdapter = $braintreeSearchAdapter; } @@ -112,29 +113,29 @@ public function execute(array $commandSubject) /** @var \Magento\Payment\Gateway\Data\PaymentDataObjectInterface $paymentDO */ $paymentDO = $this->subjectReader->readPayment($commandSubject); - /** @var \Magento\Sales\Api\Data\OrderPaymentInterface $paymentInfo */ - $paymentInfo = $paymentDO->getPayment(); - ContextHelper::assertOrderPayment($paymentInfo); - - $command = $this->getCommand($paymentInfo); + $command = $this->getCommand($paymentDO); $this->commandPool->get($command)->execute($commandSubject); } /** - * Get execution command name - * @param OrderPaymentInterface $payment + * Get execution command name. + * + * @param PaymentDataObjectInterface $paymentDO * @return string */ - private function getCommand(OrderPaymentInterface $payment) + private function getCommand(PaymentDataObjectInterface $paymentDO) { - // if auth transaction is not exists execute authorize&capture command + $payment = $paymentDO->getPayment(); + ContextHelper::assertOrderPayment($payment); + + // if auth transaction does not exist then execute authorize&capture command $existsCapture = $this->isExistsCaptureTransaction($payment); if (!$payment->getAuthorizationTransaction() && !$existsCapture) { return self::SALE; } // do capture for authorization transaction - if (!$existsCapture && !$this->isExpiredAuthorization($payment)) { + if (!$existsCapture && !$this->isExpiredAuthorization($payment, $paymentDO->getOrder())) { return self::CAPTURE; } @@ -143,12 +144,16 @@ private function getCommand(OrderPaymentInterface $payment) } /** + * Checks if authorization transaction does not expired yet. + * * @param OrderPaymentInterface $payment - * @return boolean + * @param OrderAdapterInterface $orderAdapter + * @return bool */ - private function isExpiredAuthorization(OrderPaymentInterface $payment) + private function isExpiredAuthorization(OrderPaymentInterface $payment, OrderAdapterInterface $orderAdapter) { - $collection = $this->braintreeAdapter->search( + $adapter = $this->braintreeAdapterFactory->create($orderAdapter->getStoreId()); + $collection = $adapter->search( [ $this->braintreeSearchAdapter->id()->is($payment->getLastTransId()), $this->braintreeSearchAdapter->status()->is(Transaction::AUTHORIZATION_EXPIRED) diff --git a/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php b/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php index 91c9f6c14bb5d..672aa02a0db6d 100644 --- a/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php +++ b/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php @@ -6,11 +6,9 @@ namespace Magento\Braintree\Gateway\Command; -use Exception; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator; -use Magento\Braintree\Model\Adapter\BraintreeAdapter; -use Magento\Payment\Gateway\Command; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Payment\Gateway\Command\Result\ArrayResultFactory; use Magento\Payment\Gateway\CommandInterface; use Magento\Vault\Api\PaymentTokenManagementInterface; @@ -27,9 +25,9 @@ class GetPaymentNonceCommand implements CommandInterface private $tokenManagement; /** - * @var BraintreeAdapter + * @var BraintreeAdapterFactory */ - private $adapter; + private $adapterFactory; /** * @var ArrayResultFactory @@ -48,20 +46,20 @@ class GetPaymentNonceCommand implements CommandInterface /** * @param PaymentTokenManagementInterface $tokenManagement - * @param BraintreeAdapter $adapter + * @param BraintreeAdapterFactory $adapterFactory * @param ArrayResultFactory $resultFactory * @param SubjectReader $subjectReader * @param PaymentNonceResponseValidator $responseValidator */ public function __construct( PaymentTokenManagementInterface $tokenManagement, - BraintreeAdapter $adapter, + BraintreeAdapterFactory $adapterFactory, ArrayResultFactory $resultFactory, SubjectReader $subjectReader, PaymentNonceResponseValidator $responseValidator ) { $this->tokenManagement = $tokenManagement; - $this->adapter = $adapter; + $this->adapterFactory = $adapterFactory; $this->resultFactory = $resultFactory; $this->subjectReader = $subjectReader; $this->responseValidator = $responseValidator; @@ -77,14 +75,16 @@ public function execute(array $commandSubject) $customerId = $this->subjectReader->readCustomerId($commandSubject); $paymentToken = $this->tokenManagement->getByPublicHash($publicHash, $customerId); if (!$paymentToken) { - throw new Exception('No available payment tokens'); + throw new \Exception('No available payment tokens'); } - $data = $this->adapter->createNonce($paymentToken->getGatewayToken()); + $storeId = $this->subjectReader->readStoreId($commandSubject); + $data = $this->adapterFactory->create($storeId) + ->createNonce($paymentToken->getGatewayToken()); $result = $this->responseValidator->validate(['response' => ['object' => $data]]); if (!$result->isValid()) { - throw new Exception(__(implode("\n", $result->getFailsDescription()))); + throw new \Exception(__(implode("\n", $result->getFailsDescription()))); } return $this->resultFactory->create(['array' => ['paymentMethodNonce' => $data->paymentMethodNonce->nonce]]); diff --git a/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php b/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php index 9dd52dc91afb9..0466216bdb7d2 100644 --- a/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php +++ b/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Gateway\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Config\ValueHandlerInterface; use Magento\Sales\Model\Order\Payment; diff --git a/app/code/Magento/Braintree/Gateway/Config/Config.php b/app/code/Magento/Braintree/Gateway/Config/Config.php index 7badd2b87ac34..2089a9646ae94 100644 --- a/app/code/Magento/Braintree/Gateway/Config/Config.php +++ b/app/code/Magento/Braintree/Gateway/Config/Config.php @@ -68,11 +68,12 @@ public function __construct( /** * Return the country specific card type config * + * @param int|null $storeId * @return array */ - public function getCountrySpecificCardTypeConfig() + public function getCountrySpecificCardTypeConfig($storeId = null) { - $countryCardTypes = $this->getValue(self::KEY_COUNTRY_CREDIT_CARD); + $countryCardTypes = $this->getValue(self::KEY_COUNTRY_CREDIT_CARD, $storeId); if (!$countryCardTypes) { return []; } @@ -83,11 +84,12 @@ public function getCountrySpecificCardTypeConfig() /** * Retrieve available credit card types * + * @param int|null $storeId * @return array */ - public function getAvailableCardTypes() + public function getAvailableCardTypes($storeId = null) { - $ccTypes = $this->getValue(self::KEY_CC_TYPES); + $ccTypes = $this->getValue(self::KEY_CC_TYPES, $storeId); return !empty($ccTypes) ? explode(',', $ccTypes) : []; } @@ -108,79 +110,110 @@ public function getCcTypesMapper() } /** - * Get list of card types available for country + * Gets list of card types available for country. + * * @param string $country + * @param int|null $storeId * @return array */ - public function getCountryAvailableCardTypes($country) + public function getCountryAvailableCardTypes($country, $storeId = null) { - $types = $this->getCountrySpecificCardTypeConfig(); + $types = $this->getCountrySpecificCardTypeConfig($storeId); return (!empty($types[$country])) ? $types[$country] : []; } /** - * Check if cvv field is enabled - * @return boolean + * Checks if cvv field is enabled. + * + * @param int|null $storeId + * @return bool */ - public function isCvvEnabled() + public function isCvvEnabled($storeId = null) { - return (bool) $this->getValue(self::KEY_USE_CVV); + return (bool) $this->getValue(self::KEY_USE_CVV, $storeId); } /** - * Check if 3d secure verification enabled + * Checks if 3d secure verification enabled. + * + * @param int|null $storeId * @return bool */ - public function isVerify3DSecure() + public function isVerify3DSecure($storeId = null) { - return (bool) $this->getValue(self::KEY_VERIFY_3DSECURE); + return (bool) $this->getValue(self::KEY_VERIFY_3DSECURE, $storeId); } /** - * Get threshold amount for 3d secure + * Gets threshold amount for 3d secure. + * + * @param int|null $storeId * @return float */ - public function getThresholdAmount() + public function getThresholdAmount($storeId = null) { - return (double) $this->getValue(self::KEY_THRESHOLD_AMOUNT); + return (double) $this->getValue(self::KEY_THRESHOLD_AMOUNT, $storeId); } /** - * Get list of specific countries for 3d secure + * Gets list of specific countries for 3d secure. + * + * @param int|null $storeId * @return array */ - public function get3DSecureSpecificCountries() + public function get3DSecureSpecificCountries($storeId = null) { - if ((int) $this->getValue(self::KEY_VERIFY_ALLOW_SPECIFIC) == self::VALUE_3DSECURE_ALL) { + if ((int) $this->getValue(self::KEY_VERIFY_ALLOW_SPECIFIC, $storeId) == self::VALUE_3DSECURE_ALL) { return []; } - return explode(',', $this->getValue(self::KEY_VERIFY_SPECIFIC)); + return explode(',', $this->getValue(self::KEY_VERIFY_SPECIFIC, $storeId)); + } + + /** + * Gets value of configured environment. + * Possible values: production or sandbox. + * + * @param int|null $storeId + * @return string + */ + public function getEnvironment($storeId = null) + { + return $this->getValue(Config::KEY_ENVIRONMENT, $storeId); } /** + * Gets Kount merchant ID. + * + * @param int|null $storeId * @return string */ - public function getEnvironment() + public function getKountMerchantId($storeId = null) { - return $this->getValue(Config::KEY_ENVIRONMENT); + return $this->getValue(Config::KEY_KOUNT_MERCHANT_ID, $storeId); } /** + * Gets merchant ID. + * + * @param int|null $storeId * @return string */ - public function getKountMerchantId() + public function getMerchantId($storeId = null) { - return $this->getValue(Config::KEY_KOUNT_MERCHANT_ID); + return $this->getValue(Config::KEY_MERCHANT_ID, $storeId); } /** + * Gets Merchant account ID. + * + * @param int|null $storeId * @return string */ - public function getMerchantId() + public function getMerchantAccountId($storeId = null) { - return $this->getValue(Config::KEY_MERCHANT_ID); + return $this->getValue(self::KEY_MERCHANT_ACCOUNT_ID, $storeId); } /** @@ -192,45 +225,42 @@ public function getSdkUrl() } /** + * Checks if fraud protection is enabled. + * + * @param int|null $storeId * @return bool */ - public function hasFraudProtection() + public function hasFraudProtection($storeId = null) { - return (bool) $this->getValue(Config::FRAUD_PROTECTION); + return (bool) $this->getValue(Config::FRAUD_PROTECTION, $storeId); } /** - * Get Payment configuration status + * Gets Payment configuration status. + * + * @param int|null $storeId * @return bool */ - public function isActive() + public function isActive($storeId = null) { - return (bool) $this->getValue(self::KEY_ACTIVE); + return (bool) $this->getValue(self::KEY_ACTIVE, $storeId); } /** - * Get list of configured dynamic descriptors + * Gets list of configured dynamic descriptors. + * + * @param int|null $storeId * @return array */ - public function getDynamicDescriptors() + public function getDynamicDescriptors($storeId = null) { $values = []; foreach (self::$dynamicDescriptorKeys as $key) { - $value = $this->getValue('descriptor_' . $key); + $value = $this->getValue('descriptor_' . $key, $storeId); if (!empty($value)) { $values[$key] = $value; } } return $values; } - - /** - * Get Merchant account ID - * - * @return string - */ - public function getMerchantAccountId() - { - return $this->getValue(self::KEY_MERCHANT_ACCOUNT_ID); - } } diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php b/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php index caeaaa7fe45a2..ef35152bf7e95 100644 --- a/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php +++ b/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Http\Client; -use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Payment\Gateway\Http\ClientException; use Magento\Payment\Gateway\Http\ClientInterface; use Magento\Payment\Gateway\Http\TransferInterface; @@ -29,22 +29,22 @@ abstract class AbstractTransaction implements ClientInterface protected $customLogger; /** - * @var BraintreeAdapter + * @var BraintreeAdapterFactory */ - protected $adapter; + protected $adapterFactory; /** * Constructor * * @param LoggerInterface $logger * @param Logger $customLogger - * @param BraintreeAdapter $transaction + * @param BraintreeAdapterFactory $adapterFactory */ - public function __construct(LoggerInterface $logger, Logger $customLogger, BraintreeAdapter $adapter) + public function __construct(LoggerInterface $logger, Logger $customLogger, BraintreeAdapterFactory $adapterFactory) { $this->logger = $logger; $this->customLogger = $customLogger; - $this->adapter = $adapter; + $this->adapterFactory = $adapterFactory; } /** diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php index 180344ab7263f..6d43929db7675 100644 --- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php +++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php @@ -16,9 +16,9 @@ class TransactionRefund extends AbstractTransaction */ protected function process(array $data) { - return $this->adapter->refund( - $data['transaction_id'], - $data[PaymentDataBuilder::AMOUNT] - ); + $storeId = $data['store_id'] ?? null; + + return $this->adapterFactory->create($storeId) + ->refund($data['transaction_id'], $data[PaymentDataBuilder::AMOUNT]); } } diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php index 81c79e522907d..0e4481a43bc89 100644 --- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php +++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php @@ -15,6 +15,10 @@ class TransactionSale extends AbstractTransaction */ protected function process(array $data) { - return $this->adapter->sale($data); + $storeId = $data['store_id'] ?? null; + // sending store id and other additional keys are restricted by Braintree API + unset($data['store_id']); + + return $this->adapterFactory->create($storeId)->sale($data); } } diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php index 0df5799b54b83..6760e724fd3a6 100644 --- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php +++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php @@ -18,9 +18,9 @@ class TransactionSubmitForSettlement extends AbstractTransaction */ protected function process(array $data) { - return $this->adapter->submitForSettlement( - $data[CaptureDataBuilder::TRANSACTION_ID], - $data[PaymentDataBuilder::AMOUNT] - ); + $storeId = $data['store_id'] ?? null; + + return $this->adapterFactory->create($storeId) + ->submitForSettlement($data[CaptureDataBuilder::TRANSACTION_ID], $data[PaymentDataBuilder::AMOUNT]); } } diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php index a774065365b47..0a940839fc154 100644 --- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php +++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php @@ -14,6 +14,8 @@ class TransactionVoid extends AbstractTransaction */ protected function process(array $data) { - return $this->adapter->void($data['transaction_id']); + $storeId = $data['store_id'] ?? null; + + return $this->adapterFactory->create($storeId)->void($data['transaction_id']); } } diff --git a/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php index 9ff65149894f8..f7d3aae823e56 100644 --- a/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Payment\Gateway\Request\BuilderInterface; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; /** * Class AddressDataBuilder diff --git a/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php index c6588cfaca05f..6f3a262d7efb4 100644 --- a/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Framework\Exception\LocalizedException; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; diff --git a/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php index 6b03f418c2545..6b3403bcd15c1 100644 --- a/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Payment\Gateway\Request\BuilderInterface; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; /** * Class CustomerDataBuilder diff --git a/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php index 3794058c2be8c..aac603bfb621a 100644 --- a/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php @@ -5,6 +5,7 @@ */ namespace Magento\Braintree\Gateway\Request; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Braintree\Gateway\Config\Config; @@ -24,21 +25,29 @@ class DescriptorDataBuilder implements BuilderInterface private $config; /** - * DescriptorDataBuilder constructor. + * @var SubjectReader + */ + private $subjectReader; + + /** * @param Config $config + * @param SubjectReader $subjectReader */ - public function __construct(Config $config) + public function __construct(Config $config, SubjectReader $subjectReader) { $this->config = $config; + $this->subjectReader = $subjectReader; } /** * @inheritdoc - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function build(array $buildSubject) { - $values = $this->config->getDynamicDescriptors(); + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + $values = $this->config->getDynamicDescriptors($order->getStoreId()); return !empty($values) ? [self::$descriptorKey => $values] : []; } } diff --git a/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php index 7eebbca1d4290..8538667778504 100644 --- a/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Observer\DataAssignObserver; use Magento\Payment\Gateway\Request\BuilderInterface; @@ -48,10 +48,12 @@ public function __construct(Config $config, SubjectReader $subjectReader) public function build(array $buildSubject) { $result = []; - if (!$this->config->hasFraudProtection()) { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + if (!$this->config->hasFraudProtection($order->getStoreId())) { return $result; } - $paymentDO = $this->subjectReader->readPayment($buildSubject); $payment = $paymentDO->getPayment(); $data = $payment->getAdditionalInformation(); diff --git a/app/code/Magento/Braintree/Gateway/Request/MerchantAccountDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/MerchantAccountDataBuilder.php new file mode 100644 index 0000000000000..6dc40e76322df --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Request/MerchantAccountDataBuilder.php @@ -0,0 +1,64 @@ +config = $config; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + $result = []; + $merchantAccountId = $this->config->getMerchantAccountId($order->getStoreId()); + if (!empty($merchantAccountId)) { + $result[self::$merchantAccountId] = $merchantAccountId; + } + + return $result; + } +} diff --git a/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php index cea0f8f1291bb..7d0d9dad0db06 100644 --- a/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Gateway\Request\PayPal; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Observer\DataAssignObserver; use Magento\Payment\Gateway\Request\BuilderInterface; diff --git a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php index 6f76c3415c31a..4d63ee4125b74 100644 --- a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Gateway\Request\PayPal; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Vault\Model\Ui\VaultConfigProvider; @@ -49,6 +49,8 @@ public function build(array $buildSubject) $payment = $paymentDO->getPayment(); $data = $payment->getAdditionalInformation(); + // the payment token could be stored only if a customer checks the Vault flow on storefront + // see https://developers.braintreepayments.com/guides/paypal/vault/javascript/v2#invoking-the-vault-flow if (!empty($data[VaultConfigProvider::IS_ACTIVE_CODE])) { $result[self::$optionsKey] = [ self::$storeInVaultOnSuccess => true diff --git a/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php index a3341c3fc1873..fe75ce86cca2f 100644 --- a/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php @@ -6,8 +6,8 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Braintree\Gateway\Config\Config; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Observer\DataAssignObserver; -use Magento\Braintree\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; @@ -36,9 +36,8 @@ class PaymentDataBuilder implements BuilderInterface const PAYMENT_METHOD_NONCE = 'paymentMethodNonce'; /** - * The merchant account ID used to create a transaction. - * Currency is also determined by merchant account ID. - * If no merchant account ID is specified, Braintree will use your default merchant account. + * @deprecated + * @see \Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder */ const MERCHANT_ACCOUNT_ID = 'merchantAccountId'; @@ -47,25 +46,18 @@ class PaymentDataBuilder implements BuilderInterface */ const ORDER_ID = 'orderId'; - /** - * @var Config - */ - private $config; - /** * @var SubjectReader */ private $subjectReader; /** - * Constructor - * * @param Config $config * @param SubjectReader $subjectReader + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct(Config $config, SubjectReader $subjectReader) { - $this->config = $config; $this->subjectReader = $subjectReader; } @@ -87,11 +79,6 @@ public function build(array $buildSubject) self::ORDER_ID => $order->getOrderIncrementId() ]; - $merchantAccountId = $this->config->getMerchantAccountId(); - if (!empty($merchantAccountId)) { - $result[self::MERCHANT_ACCOUNT_ID] = $merchantAccountId; - } - return $result; } } diff --git a/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php index 82de8e84cbfea..1c25646311160 100644 --- a/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Gateway\Request; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; use Magento\Sales\Api\Data\TransactionInterface; diff --git a/app/code/Magento/Braintree/Gateway/Request/StoreConfigBuilder.php b/app/code/Magento/Braintree/Gateway/Request/StoreConfigBuilder.php new file mode 100644 index 0000000000000..014df33690fa0 --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Request/StoreConfigBuilder.php @@ -0,0 +1,42 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + return [ + 'store_id' => $order->getStoreId() + ]; + } +} diff --git a/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php index c366a67701521..520aa58457753 100644 --- a/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php @@ -7,7 +7,7 @@ use Magento\Braintree\Gateway\Config\Config; use Magento\Payment\Gateway\Data\OrderAdapterInterface; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; @@ -64,12 +64,15 @@ public function build(array $buildSubject) */ private function is3DSecureEnabled(OrderAdapterInterface $order, $amount) { - if (!$this->config->isVerify3DSecure() || $amount < $this->config->getThresholdAmount()) { + $storeId = $order->getStoreId(); + if (!$this->config->isVerify3DSecure($storeId) + || $amount < $this->config->getThresholdAmount($storeId) + ) { return false; } $billingAddress = $order->getBillingAddress(); - $specificCounties = $this->config->get3DSecureSpecificCountries(); + $specificCounties = $this->config->get3DSecureSpecificCountries($storeId); if (!empty($specificCounties) && !in_array($billingAddress->getCountryId(), $specificCounties)) { return false; } diff --git a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php index 7182f87e082d1..950634ba2d9e2 100644 --- a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php @@ -5,7 +5,9 @@ */ namespace Magento\Braintree\Gateway\Request; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; @@ -41,6 +43,9 @@ public function build(array $buildSubject) $payment = $paymentDO->getPayment(); $extensionAttributes = $payment->getExtensionAttributes(); $paymentToken = $extensionAttributes->getVaultPaymentToken(); + if ($paymentToken === null) { + throw new CommandException(__('The Payment Token is not available to perform the request.')); + } return [ 'amount' => $this->formatPrice($this->subjectReader->readAmount($buildSubject)), 'paymentMethodToken' => $paymentToken->getGatewayToken() diff --git a/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php index 2328ea10f78c7..0bbda28cd344b 100644 --- a/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Gateway\Request; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Sales\Model\Order\Payment; diff --git a/app/code/Magento/Braintree/Gateway/Response/CancelDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/CancelDetailsHandler.php new file mode 100644 index 0000000000000..3d6ed025791bf --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Response/CancelDetailsHandler.php @@ -0,0 +1,43 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + /** @var Payment $orderPayment */ + $orderPayment = $paymentDO->getPayment(); + $orderPayment->setIsTransactionClosed(true); + $orderPayment->setShouldCloseParentTransaction(true); + } +} diff --git a/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php index 2715da5d3c419..32abeac4c8ffb 100644 --- a/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Response; use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Helper\ContextHelper; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; @@ -85,7 +85,7 @@ public function handle(array $handlingSubject, array $response) private function getCreditCardType($type) { $replaced = str_replace(' ', '-', strtolower($type)); - $mapper = $this->config->getCctypesMapper(); + $mapper = $this->config->getCcTypesMapper(); return $mapper[$replaced]; } diff --git a/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php index 766b385851ab8..7d38c2bf7d2ed 100644 --- a/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php @@ -6,14 +6,14 @@ namespace Magento\Braintree\Gateway\Response\PayPal; use Braintree\Transaction; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Framework\Intl\DateTimeFactory; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Payment\Model\InfoInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory; +use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; use Magento\Vault\Api\Data\PaymentTokenInterface; -use Magento\Vault\Api\Data\PaymentTokenInterfaceFactory; /** * Vault Details Handler @@ -21,7 +21,7 @@ class VaultDetailsHandler implements HandlerInterface { /** - * @var PaymentTokenInterfaceFactory + * @var PaymentTokenFactoryInterface */ private $paymentTokenFactory; @@ -41,15 +41,13 @@ class VaultDetailsHandler implements HandlerInterface private $dateTimeFactory; /** - * Constructor - * - * @param PaymentTokenInterfaceFactory $paymentTokenFactory + * @param PaymentTokenFactoryInterface $paymentTokenFactory * @param OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory * @param SubjectReader $subjectReader * @param DateTimeFactory $dateTimeFactory */ public function __construct( - PaymentTokenInterfaceFactory $paymentTokenFactory, + PaymentTokenFactoryInterface $paymentTokenFactory, OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory, SubjectReader $subjectReader, DateTimeFactory $dateTimeFactory @@ -92,7 +90,7 @@ private function getVaultPaymentToken(Transaction $transaction) } /** @var PaymentTokenInterface $paymentToken */ - $paymentToken = $this->paymentTokenFactory->create(); + $paymentToken = $this->paymentTokenFactory->create(PaymentTokenFactoryInterface::TOKEN_TYPE_ACCOUNT); $paymentToken->setGatewayToken($token); $paymentToken->setExpiresAt($this->getExpirationDate()); $details = json_encode([ diff --git a/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php index 83d7e44a6b612..97bb312af4bd4 100644 --- a/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Response; use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Sales\Api\Data\OrderPaymentInterface; /** diff --git a/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php index 3941640aaeeb1..a95ea76c2e633 100644 --- a/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php @@ -5,10 +5,8 @@ */ namespace Magento\Braintree\Gateway\Response; -use Braintree\Transaction; use Magento\Braintree\Observer\DataAssignObserver; -use Magento\Payment\Gateway\Helper\ContextHelper; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; diff --git a/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php b/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php index 95660e10b394c..d4976ff18e0ee 100644 --- a/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Gateway\Response; use Magento\Payment\Gateway\Helper\ContextHelper; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; /** diff --git a/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php index 93f37cb561feb..8d61660f03ce5 100644 --- a/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php @@ -7,7 +7,7 @@ use Braintree\Transaction; use Magento\Payment\Gateway\Helper\ContextHelper; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; diff --git a/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php b/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php index 7dd79143736e5..18888bdcf3d4a 100644 --- a/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Gateway\Response; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Sales\Model\Order\Payment; diff --git a/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php index a8332b9409f78..8880f9c1b1a3e 100644 --- a/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php @@ -7,13 +7,15 @@ use Braintree\Transaction; use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Payment\Model\InfoInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory; +use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; use Magento\Vault\Api\Data\PaymentTokenInterface; -use Magento\Vault\Api\Data\PaymentTokenInterfaceFactory; /** * Vault Details Handler @@ -22,7 +24,7 @@ class VaultDetailsHandler implements HandlerInterface { /** - * @var PaymentTokenInterfaceFactory + * @var PaymentTokenFactoryInterface */ protected $paymentTokenFactory; @@ -42,33 +44,32 @@ class VaultDetailsHandler implements HandlerInterface protected $config; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var Json */ private $serializer; /** * VaultDetailsHandler constructor. * - * @param PaymentTokenInterfaceFactory $paymentTokenFactory + * @param PaymentTokenFactoryInterface $paymentTokenFactory * @param OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory * @param Config $config * @param SubjectReader $subjectReader - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param Json|null $serializer * @throws \RuntimeException */ public function __construct( - PaymentTokenInterfaceFactory $paymentTokenFactory, + PaymentTokenFactoryInterface $paymentTokenFactory, OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory, Config $config, SubjectReader $subjectReader, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + Json $serializer = null ) { $this->paymentTokenFactory = $paymentTokenFactory; $this->paymentExtensionFactory = $paymentExtensionFactory; $this->config = $config; $this->subjectReader = $subjectReader; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } /** @@ -103,7 +104,7 @@ protected function getVaultPaymentToken(Transaction $transaction) } /** @var PaymentTokenInterface $paymentToken */ - $paymentToken = $this->paymentTokenFactory->create(); + $paymentToken = $this->paymentTokenFactory->create(PaymentTokenFactoryInterface::TOKEN_TYPE_CREDIT_CARD); $paymentToken->setGatewayToken($token); $paymentToken->setExpiresAt($this->getExpirationDate($transaction)); @@ -156,7 +157,7 @@ private function convertDetailsToJSON($details) private function getCreditCardType($type) { $replaced = str_replace(' ', '-', strtolower($type)); - $mapper = $this->config->getCctypesMapper(); + $mapper = $this->config->getCcTypesMapper(); return $mapper[$replaced]; } diff --git a/app/code/Magento/Braintree/Gateway/Helper/SubjectReader.php b/app/code/Magento/Braintree/Gateway/SubjectReader.php similarity index 85% rename from app/code/Magento/Braintree/Gateway/Helper/SubjectReader.php rename to app/code/Magento/Braintree/Gateway/SubjectReader.php index e98597655190f..7cf00233e7f8f 100644 --- a/app/code/Magento/Braintree/Gateway/Helper/SubjectReader.php +++ b/app/code/Magento/Braintree/Gateway/SubjectReader.php @@ -3,13 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Braintree\Gateway\Helper; +namespace Magento\Braintree\Gateway; use Braintree\Transaction; -use Magento\Quote\Model\Quote; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Payment\Gateway\Helper; use Magento\Vault\Api\Data\PaymentTokenInterface; -use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; /** * Class SubjectReader @@ -44,19 +43,20 @@ public function readPayment(array $subject) } /** - * Reads transaction from subject + * Reads transaction from the subject. * * @param array $subject - * @return \Braintree\Transaction + * @return Transaction + * @throws \InvalidArgumentException if the subject doesn't contain transaction details. */ public function readTransaction(array $subject) { if (!isset($subject['object']) || !is_object($subject['object'])) { - throw new \InvalidArgumentException('Response object does not exist'); + throw new \InvalidArgumentException('Response object does not exist.'); } if (!isset($subject['object']->transaction) - && !$subject['object']->transaction instanceof Transaction + || !$subject['object']->transaction instanceof Transaction ) { throw new \InvalidArgumentException('The object is not a class \Braintree\Transaction.'); } @@ -119,4 +119,15 @@ public function readPayPal(Transaction $transaction) return $transaction->paypal; } + + /** + * Reads store's ID, otherwise returns null. + * + * @param array $subject + * @return int|null + */ + public function readStoreId(array $subject) + { + return $subject['store_id'] ?? null; + } } diff --git a/app/code/Magento/Braintree/Gateway/Validator/CancelResponseValidator.php b/app/code/Magento/Braintree/Gateway/Validator/CancelResponseValidator.php new file mode 100644 index 0000000000000..5e31547e9503c --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Validator/CancelResponseValidator.php @@ -0,0 +1,90 @@ +generalResponseValidator = $generalResponseValidator; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject): ResultInterface + { + $result = $this->generalResponseValidator->validate($validationSubject); + if (!$result->isValid()) { + $response = $this->subjectReader->readResponseObject($validationSubject); + if ($this->isErrorAcceptable($response->errors)) { + $result = $this->createResult(true, [__('Transaction is cancelled offline.')]); + } + } + + return $result; + } + + /** + * Checks if error collection has an acceptable error code. + * + * @param ErrorCollection $errorCollection + * @return bool + */ + private function isErrorAcceptable(ErrorCollection $errorCollection): bool + { + $errors = $errorCollection->deepAll(); + // there is should be only one acceptable error + if (count($errors) > 1) { + return false; + } + + /** @var Validation $error */ + $error = array_pop($errors); + + return (int)$error->code === self::$acceptableTransactionCode; + } +} diff --git a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php new file mode 100644 index 0000000000000..167fcb1569cbf --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php @@ -0,0 +1,43 @@ +errors; + + /** @var Validation $error */ + foreach ($collection->deepAll() as $error) { + $result[] = $error->code; + } + + return $result; + } +} diff --git a/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php b/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php index a70bc2d02e0e5..6aac588c38374 100644 --- a/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php +++ b/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php @@ -8,7 +8,7 @@ use Braintree\Result\Error; use Braintree\Result\Successful; use Magento\Payment\Gateway\Validator\AbstractValidator; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; class GeneralResponseValidator extends AbstractValidator @@ -18,16 +18,26 @@ class GeneralResponseValidator extends AbstractValidator */ protected $subjectReader; + /** + * @var ErrorCodeProvider + */ + private $errorCodeProvider; + /** * Constructor * * @param ResultInterfaceFactory $resultFactory * @param SubjectReader $subjectReader + * @param ErrorCodeProvider $errorCodeProvider */ - public function __construct(ResultInterfaceFactory $resultFactory, SubjectReader $subjectReader) - { + public function __construct( + ResultInterfaceFactory $resultFactory, + SubjectReader $subjectReader, + ErrorCodeProvider $errorCodeProvider + ) { parent::__construct($resultFactory); $this->subjectReader = $subjectReader; + $this->errorCodeProvider = $errorCodeProvider; } /** @@ -49,8 +59,9 @@ public function validate(array $validationSubject) $errorMessages = array_merge($errorMessages, $validationResult[1]); } } + $errorCodes = $this->errorCodeProvider->getErrorCodes($response); - return $this->createResult($isValid, $errorMessages); + return $this->createResult($isValid, $errorMessages, $errorCodes); } /** @@ -62,7 +73,7 @@ protected function getResponseValidators() function ($response) { return [ property_exists($response, 'success') && $response->success === true, - [__('Braintree error response.')] + [$response->message ?? __('Braintree error response.')] ]; } ]; diff --git a/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php index 71f3828ebe9d4..fd1fe81b5eba8 100644 --- a/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php +++ b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php @@ -15,29 +15,40 @@ /** * Class BraintreeAdapter + * Use \Magento\Braintree\Model\Adapter\BraintreeAdapterFactory to create new instance of adapter. * @codeCoverageIgnore */ class BraintreeAdapter { - /** * @var Config */ private $config; /** - * @param Config $config + * @param string $merchantId + * @param string $publicKey + * @param string $privateKey + * @param string $environment */ - public function __construct(Config $config) + public function __construct($merchantId, $publicKey, $privateKey, $environment) { - $this->config = $config; - $this->initCredentials(); + $this->merchantId($merchantId); + $this->publicKey($publicKey); + $this->privateKey($privateKey); + + if ($environment === Environment::ENVIRONMENT_PRODUCTION) { + $this->environment(Environment::ENVIRONMENT_PRODUCTION); + } else { + $this->environment(Environment::ENVIRONMENT_SANDBOX); + } } /** * Initializes credentials. * * @return void + * @deprecated is not used anymore */ protected function initCredentials() { diff --git a/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapterFactory.php b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapterFactory.php new file mode 100644 index 0000000000000..2c3f137eb1686 --- /dev/null +++ b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapterFactory.php @@ -0,0 +1,56 @@ +config = $config; + $this->objectManager = $objectManager; + } + + /** + * Creates instance of Braintree Adapter. + * + * @param int|null $storeId if null is provided as an argument, then current scope will be resolved + * by \Magento\Framework\App\Config\ScopeCodeResolver (useful for most cases) but for adminhtml area the store + * should be provided as the argument for correct config settings loading. + * @return BraintreeAdapter + */ + public function create($storeId = null) + { + return $this->objectManager->create( + BraintreeAdapter::class, + [ + 'merchantId' => $this->config->getMerchantId($storeId), + 'publicKey' => $this->config->getValue(Config::KEY_PUBLIC_KEY, $storeId), + 'privateKey' => $this->config->getValue(Config::KEY_PRIVATE_KEY, $storeId), + 'environment' => $this->config->getEnvironment($storeId), + ] + ); + } +} diff --git a/app/code/Magento/Braintree/Model/Adminhtml/Source/PaymentAction.php b/app/code/Magento/Braintree/Model/Adminhtml/Source/PaymentAction.php index e0d684a7aa976..595d8b4792a62 100644 --- a/app/code/Magento/Braintree/Model/Adminhtml/Source/PaymentAction.php +++ b/app/code/Magento/Braintree/Model/Adminhtml/Source/PaymentAction.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Model\Adminhtml\Source; use Magento\Framework\Option\ArrayInterface; -use Magento\Payment\Model\Method\AbstractMethod; +use Magento\Payment\Model\MethodInterface; /** * Class PaymentAction @@ -22,11 +22,11 @@ public function toOptionArray() { return [ [ - 'value' => AbstractMethod::ACTION_AUTHORIZE, + 'value' => MethodInterface::ACTION_AUTHORIZE, 'label' => __('Authorize'), ], [ - 'value' => AbstractMethod::ACTION_AUTHORIZE_CAPTURE, + 'value' => MethodInterface::ACTION_AUTHORIZE_CAPTURE, 'label' => __('Authorize and Capture'), ] ]; diff --git a/app/code/Magento/Braintree/Model/Adminhtml/System/Config/CountryCreditCard.php b/app/code/Magento/Braintree/Model/Adminhtml/System/Config/CountryCreditCard.php index f68b7eca047f5..2a9923a333cef 100644 --- a/app/code/Magento/Braintree/Model/Adminhtml/System/Config/CountryCreditCard.php +++ b/app/code/Magento/Braintree/Model/Adminhtml/System/Config/CountryCreditCard.php @@ -66,6 +66,13 @@ public function __construct( public function beforeSave() { $value = $this->getValue(); + if (!is_array($value)) { + try { + $value = $this->serializer->unserialize($value); + } catch (\InvalidArgumentException $e) { + $value = []; + } + } $result = []; foreach ($value as $data) { if (empty($data['country_id']) || empty($data['cc_types'])) { diff --git a/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php b/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php index 1d5057d83d6cf..f9fae8a469b1d 100644 --- a/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php +++ b/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php @@ -24,7 +24,7 @@ class AvsEmsCodeMapper implements PaymentVerificationInterface * * @var string */ - private static $unavailableCode = 'U'; + private static $unavailableCode = ''; /** * List of mapping AVS codes diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php b/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php index b833798eabf90..314404c79939c 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php @@ -3,16 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Braintree\Model\Paypal\Helper; -use Magento\Quote\Model\Quote; +use Magento\Braintree\Model\Paypal\OrderCancellationService; +use Magento\Checkout\Api\AgreementsValidatorInterface; use Magento\Checkout\Helper\Data; +use Magento\Checkout\Model\Type\Onepage; use Magento\Customer\Model\Group; use Magento\Customer\Model\Session; -use Magento\Checkout\Model\Type\Onepage; -use Magento\Quote\Api\CartManagementInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Checkout\Api\AgreementsValidatorInterface; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Model\Quote; /** * Class OrderPlace @@ -41,23 +43,29 @@ class OrderPlace extends AbstractHelper private $checkoutHelper; /** - * Constructor - * + * @var OrderCancellationService + */ + private $orderCancellationService; + + /** * @param CartManagementInterface $cartManagement * @param AgreementsValidatorInterface $agreementsValidator * @param Session $customerSession * @param Data $checkoutHelper + * @param OrderCancellationService $orderCancellationService */ public function __construct( CartManagementInterface $cartManagement, AgreementsValidatorInterface $agreementsValidator, Session $customerSession, - Data $checkoutHelper + Data $checkoutHelper, + OrderCancellationService $orderCancellationService ) { $this->cartManagement = $cartManagement; $this->agreementsValidator = $agreementsValidator; $this->customerSession = $customerSession; $this->checkoutHelper = $checkoutHelper; + $this->orderCancellationService = $orderCancellationService; } /** @@ -66,12 +74,14 @@ public function __construct( * @param Quote $quote * @param array $agreement * @return void - * @throws LocalizedException + * @throws \Exception */ public function execute(Quote $quote, array $agreement) { if (!$this->agreementsValidator->isValid($agreement)) { - throw new LocalizedException(__('Please agree to all the terms and conditions before placing the order.')); + throw new LocalizedException(__( + "The order wasn't placed. First, agree to the terms and conditions, then try placing your order again." + )); } if ($this->getCheckoutMethod($quote) === Onepage::METHOD_GUEST) { @@ -81,7 +91,12 @@ public function execute(Quote $quote, array $agreement) $this->disabledQuoteAddressValidation($quote); $quote->collectTotals(); - $this->cartManagement->placeOrder($quote->getId()); + try { + $this->cartManagement->placeOrder($quote->getId()); + } catch (\Exception $e) { + $this->orderCancellationService->execute($quote->getReservedOrderId()); + throw $e; + } } /** diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php index fe5895541543d..aa23fa767d1ed 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php @@ -148,7 +148,7 @@ private function updateBillingAddress(Quote $quote, array $details) { $billingAddress = $quote->getBillingAddress(); - if ($this->config->isRequiredBillingAddress()) { + if ($this->config->isRequiredBillingAddress() && !empty($details['billingAddress'])) { $this->updateAddressData($billingAddress, $details['billingAddress']); } else { $this->updateAddressData($billingAddress, $details['shippingAddress']); diff --git a/app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php b/app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php new file mode 100644 index 0000000000000..29757e35ea6f4 --- /dev/null +++ b/app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php @@ -0,0 +1,77 @@ +searchCriteriaBuilder = $searchCriteriaBuilder; + $this->orderRepository = $orderRepository; + } + + /** + * Cancels an order and authorization transaction. + * + * @param string $incrementId + * @return bool + */ + public function execute(string $incrementId): bool + { + $order = $this->getOrder($incrementId); + if ($order === null) { + return false; + } + + // `\Magento\Sales\Model\Service\OrderService::cancel` cannot be used for cancellation as the service uses + // the order repository with outdated payment method instance (ex. contains Vault instead of Braintree) + $order->cancel(); + $this->orderRepository->save($order); + return true; + } + + /** + * Gets order by increment ID. + * + * @param string $incrementId + * @return OrderInterface|null + */ + private function getOrder(string $incrementId) + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementId) + ->create(); + + $items = $this->orderRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php b/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php index edac6e3533849..a237b64bf58ee 100644 --- a/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php +++ b/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php @@ -5,7 +5,8 @@ */ namespace Magento\Braintree\Model\Report; -use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Braintree\Model\Report\Row\TransactionMap; use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Data\Collection; @@ -26,7 +27,7 @@ class TransactionsCollection extends Collection implements SearchResultInterface * * @var string */ - protected $_itemObjectClass = \Magento\Braintree\Model\Report\Row\TransactionMap::class; + protected $_itemObjectClass = TransactionMap::class; /** * @var array @@ -39,9 +40,9 @@ class TransactionsCollection extends Collection implements SearchResultInterface private $filterMapper; /** - * @var BraintreeAdapter + * @var BraintreeAdapterFactory */ - private $braintreeAdapter; + private $braintreeAdapterFactory; /** * @var \Braintree\ResourceCollection | null @@ -50,17 +51,17 @@ class TransactionsCollection extends Collection implements SearchResultInterface /** * @param EntityFactoryInterface $entityFactory - * @param BraintreeAdapter $braintreeAdapter + * @param BraintreeAdapterFactory $braintreeAdapterFactory * @param FilterMapper $filterMapper */ public function __construct( EntityFactoryInterface $entityFactory, - BraintreeAdapter $braintreeAdapter, + BraintreeAdapterFactory $braintreeAdapterFactory, FilterMapper $filterMapper ) { parent::__construct($entityFactory); $this->filterMapper = $filterMapper; - $this->braintreeAdapter = $braintreeAdapter; + $this->braintreeAdapterFactory = $braintreeAdapterFactory; } /** @@ -110,7 +111,8 @@ protected function fetchIdsCollection() // Fetch all transaction IDs in order to filter if (empty($this->collection)) { $filters = $this->getFilters(); - $this->collection = $this->braintreeAdapter->search($filters); + $this->collection = $this->braintreeAdapterFactory->create() + ->search($filters); } return $this->collection; diff --git a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php index c420195446270..928769498a035 100644 --- a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php @@ -5,10 +5,11 @@ */ namespace Magento\Braintree\Model\Ui; +use Magento\Braintree\Gateway\Config\Config; use Magento\Braintree\Gateway\Request\PaymentDataBuilder; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Checkout\Model\ConfigProviderInterface; -use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Framework\Session\SessionManagerInterface; /** * Class ConfigProvider @@ -25,27 +26,35 @@ class ConfigProvider implements ConfigProviderInterface private $config; /** - * @var BraintreeAdapter + * @var BraintreeAdapterFactory */ - private $adapter; + private $adapterFactory; /** * @var string */ private $clientToken = ''; + /** + * @var SessionManagerInterface + */ + private $session; + /** * Constructor * * @param Config $config - * @param BraintreeAdapter $adapter + * @param BraintreeAdapterFactory $adapterFactory + * @param SessionManagerInterface $session */ public function __construct( Config $config, - BraintreeAdapter $adapter + BraintreeAdapterFactory $adapterFactory, + SessionManagerInterface $session ) { $this->config = $config; - $this->adapter = $adapter; + $this->adapterFactory = $adapterFactory; + $this->session = $session; } /** @@ -55,28 +64,29 @@ public function __construct( */ public function getConfig() { + $storeId = $this->session->getStoreId(); return [ 'payment' => [ self::CODE => [ - 'isActive' => $this->config->isActive(), + 'isActive' => $this->config->isActive($storeId), 'clientToken' => $this->getClientToken(), - 'ccTypesMapper' => $this->config->getCctypesMapper(), + 'ccTypesMapper' => $this->config->getCcTypesMapper(), 'sdkUrl' => $this->config->getSdkUrl(), - 'countrySpecificCardTypes' => $this->config->getCountrySpecificCardTypeConfig(), - 'availableCardTypes' => $this->config->getAvailableCardTypes(), - 'useCvv' => $this->config->isCvvEnabled(), - 'environment' => $this->config->getEnvironment(), - 'kountMerchantId' => $this->config->getKountMerchantId(), - 'hasFraudProtection' => $this->config->hasFraudProtection(), - 'merchantId' => $this->config->getMerchantId(), - 'ccVaultCode' => self::CC_VAULT_CODE + 'countrySpecificCardTypes' => $this->config->getCountrySpecificCardTypeConfig($storeId), + 'availableCardTypes' => $this->config->getAvailableCardTypes($storeId), + 'useCvv' => $this->config->isCvvEnabled($storeId), + 'environment' => $this->config->getEnvironment($storeId), + 'kountMerchantId' => $this->config->getKountMerchantId($storeId), + 'hasFraudProtection' => $this->config->hasFraudProtection($storeId), + 'merchantId' => $this->config->getMerchantId($storeId), + 'ccVaultCode' => self::CC_VAULT_CODE, ], Config::CODE_3DSECURE => [ - 'enabled' => $this->config->isVerify3DSecure(), - 'thresholdAmount' => $this->config->getThresholdAmount(), - 'specificCountries' => $this->config->get3DSecureSpecificCountries() + 'enabled' => $this->config->isVerify3DSecure($storeId), + 'thresholdAmount' => $this->config->getThresholdAmount($storeId), + 'specificCountries' => $this->config->get3DSecureSpecificCountries($storeId), ], - ] + ], ]; } @@ -89,12 +99,14 @@ public function getClientToken() if (empty($this->clientToken)) { $params = []; - $merchantAccountId = $this->config->getMerchantAccountId(); + $storeId = $this->session->getStoreId(); + $merchantAccountId = $this->config->getMerchantAccountId($storeId); if (!empty($merchantAccountId)) { $params[PaymentDataBuilder::MERCHANT_ACCOUNT_ID] = $merchantAccountId; } - $this->clientToken = $this->adapter->generate($params); + $this->clientToken = $this->adapterFactory->create($storeId) + ->generate($params); } return $this->clientToken; diff --git a/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php index e06b913db8ef4..e6c5ee22c62b4 100644 --- a/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php @@ -47,6 +47,8 @@ public function __construct(Config $config, ResolverInterface $resolver) */ public function getConfig() { + $requireBillingAddressAll = \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL; + return [ 'payment' => [ self::PAYPAL_CODE => [ @@ -60,6 +62,8 @@ public function getConfig() 'vaultCode' => self::PAYPAL_VAULT_CODE, 'skipOrderReview' => $this->config->isSkipOrderReview(), 'paymentIcon' => $this->config->getPayPalIcon(), + 'isRequiredBillingAddress' => + (int)$this->config->isRequiredBillingAddress() === $requireBillingAddressAll ] ] ]; diff --git a/app/code/Magento/Braintree/Plugin/OrderCancellation.php b/app/code/Magento/Braintree/Plugin/OrderCancellation.php new file mode 100644 index 0000000000000..90c72839d9777 --- /dev/null +++ b/app/code/Magento/Braintree/Plugin/OrderCancellation.php @@ -0,0 +1,81 @@ +orderCancellationService = $orderCancellationService; + $this->quoteRepository = $quoteRepository; + } + + /** + * Cancels an order if an exception occurs during the order creation. + * + * @param CartManagementInterface $subject + * @param \Closure $proceed + * @param int $cartId + * @param PaymentInterface $payment + * @return int + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundPlaceOrder( + CartManagementInterface $subject, + \Closure $proceed, + $cartId, + PaymentInterface $payment = null + ) { + try { + return $proceed($cartId, $payment); + } catch (\Exception $e) { + $quote = $this->quoteRepository->get((int) $cartId); + $payment = $quote->getPayment(); + $paymentCodes = [ + ConfigProvider::CODE, + ConfigProvider::CC_VAULT_CODE, + PayPalConfigProvider::PAYPAL_CODE, + PayPalConfigProvider::PAYPAL_VAULT_CODE + ]; + if (in_array($payment->getMethod(), $paymentCodes)) { + $incrementId = $quote->getReservedOrderId(); + $this->orderCancellationService->execute($incrementId); + } + + throw $e; + } + } +} diff --git a/app/code/Magento/Braintree/Setup/Patch/Data/ConvertSerializedDataToJson.php b/app/code/Magento/Braintree/Setup/Patch/Data/ConvertSerializedDataToJson.php new file mode 100644 index 0000000000000..d08bf62da8e4f --- /dev/null +++ b/app/code/Magento/Braintree/Setup/Patch/Data/ConvertSerializedDataToJson.php @@ -0,0 +1,111 @@ +moduleDataSetup = $moduleDataSetup; + $this->fieldDataConverterFactory = $fieldDataConverterFactory; + $this->queryModifierFactory = $queryModifierFactory; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $this->convertSerializedDataToJson(); + } + + /** + * Upgrade data to version 2.0.1, converts row data in the core_config_data table that uses the path + * payment/braintree/countrycreditcard from serialized to JSON + * + * @return void + */ + private function convertSerializedDataToJson() + { + $fieldDataConverter = $this->fieldDataConverterFactory->create( + \Magento\Framework\DB\DataConverter\SerializedToJson::class + ); + + $queryModifier = $this->queryModifierFactory->create( + 'in', + [ + 'values' => [ + 'path' => ['payment/braintree/countrycreditcard'] + ] + ] + ); + + $fieldDataConverter->convert( + $this->moduleDataSetup->getConnection(), + $this->moduleDataSetup->getTable('core_config_data'), + 'config_id', + 'value', + $queryModifier + ); + } + + /** + * {@inheritdoc} + */ + public static function getDependencies() + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getVersion() + { + return '2.0.1'; + } + + /** + * {@inheritdoc} + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Braintree/Setup/UpgradeData.php b/app/code/Magento/Braintree/Setup/UpgradeData.php deleted file mode 100644 index a7b39f12273e2..0000000000000 --- a/app/code/Magento/Braintree/Setup/UpgradeData.php +++ /dev/null @@ -1,83 +0,0 @@ -fieldDataConverterFactory = $fieldDataConverterFactory; - $this->queryModifierFactory = $queryModifierFactory; - } - - /** - * Upgrades data for Braintree module - * - * @param ModuleDataSetupInterface $setup - * @param ModuleContextInterface $context - * @return void - */ - public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) - { - if (version_compare($context->getVersion(), '2.0.1', '<')) { - $this->convertSerializedDataToJson($setup); - } - } - - /** - * Upgrade data to version 2.0.1, converts row data in the core_config_data table that uses the path - * payment/braintree/countrycreditcard from serialized to JSON - * - * @param ModuleDataSetupInterface $setup - * @return void - */ - private function convertSerializedDataToJson(ModuleDataSetupInterface $setup) - { - $fieldDataConverter = $this->fieldDataConverterFactory->create( - \Magento\Framework\DB\DataConverter\SerializedToJson::class - ); - - $queryModifier = $this->queryModifierFactory->create( - 'in', - [ - 'values' => [ - 'path' => ['payment/braintree/countrycreditcard'] - ] - ] - ); - - $fieldDataConverter->convert( - $setup->getConnection(), - $setup->getTable('core_config_data'), - 'config_id', - 'value', - $queryModifier - ); - } -} diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml new file mode 100644 index 0000000000000..412513c59c63c --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml new file mode 100644 index 0000000000000..9eaae8b33e73f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml new file mode 100644 index 0000000000000..a68042127ec48 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml new file mode 100644 index 0000000000000..17d634c009b3e --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml new file mode 100644 index 0000000000000..19de3e859ae9a --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml new file mode 100644 index 0000000000000..4d531214db150 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml new file mode 100644 index 0000000000000..724c6d92846c4 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml new file mode 100644 index 0000000000000..bc6d6c2b46dc9 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml new file mode 100644 index 0000000000000..7c774a634b369 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml new file mode 100644 index 0000000000000..f00e3fa286b08 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml @@ -0,0 +1,151 @@ + + + + + + SampleTitle + SamplePaymentAction + SampleEnvironment + SampleMerchantId + SamplePublicKey + SamplePrivateKey + + + Sample Braintree Config + + + authorize + + + sandbox + + + someMerchantId + + + somePublicKey + + + somePrivateKey + + + + BraintreeTitle + PaymentAction + Environment + MerchantId + PublicKey + PrivateKey + Status + + + Credit Card (Braintree) + + + authorize + + + sandbox + + + d4pdjhxgjfrsmzbf + + + m7q4wmh43xrgyrst + + + 67de364080b1b4e2492d7a3de413a572 + + + + + DefaultTitle + DefaultPaymentAction + DefaultEnvironment + DefaultMerchantId + DefaultPublicKey + DefaultPrivateKey + + + + + + + + + + + + + + + + + + + + + + BraintreeValuteActive + EnableSolution + + + 1 + + + 1 + + + + DefaultBraintreeValuteActive + DefaultEnableSolution + + + 0 + + + 0 + + + + Website + new_website + Store + new_store + Block + + + + Role + + + admin + John + Smith + admin123 + mail@mail.com + + + + 5105105105105100 + 12 + 20 + 113 + + + + Credit Card (Braintree) + d4pdjhxgjfrsmzbf + m7q4wmh43xrgyrst + 67de364080b1b4e2492d7a3de413a572 + Magneto + PayPal (Braintree) + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml new file mode 100644 index 0000000000000..30345ec31bacd --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml @@ -0,0 +1,24 @@ + + + + + + Abgar + Abgaryan + m@m.com + Abgar + Abgaryan + Street + Yerevan + 9999 + 9999 + Armenia + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml new file mode 100644 index 0000000000000..72661ae94076f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml @@ -0,0 +1,17 @@ + + + + + + ProductTest + 100 + 100 + + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Captcha/LICENSE.txt b/app/code/Magento/Braintree/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Captcha/LICENSE.txt rename to app/code/Magento/Braintree/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CacheInvalidate/LICENSE_AFL.txt b/app/code/Magento/Braintree/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/CacheInvalidate/LICENSE_AFL.txt rename to app/code/Magento/Braintree/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml b/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml new file mode 100644 index 0000000000000..5e3b870d65c67 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + string + + + string + + + string + + + string + + + string + + + string + + + + + + + + + + + + + + + + + integer + + + string + + + + + + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/README.md b/app/code/Magento/Braintree/Test/Mftf/README.md new file mode 100644 index 0000000000000..6ee177a9cdcd2 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Braintree Functional Tests + +The Functional Test Module for **Magento Braintree** module. diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml new file mode 100644 index 0000000000000..1158f471d51f0 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml @@ -0,0 +1,24 @@ + + + +
+ + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml new file mode 100644 index 0000000000000..98d748b5a30ea --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml @@ -0,0 +1,23 @@ + + + +
+ + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml new file mode 100644 index 0000000000000..220c9a444b02f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml @@ -0,0 +1,15 @@ + + + +
+ + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml new file mode 100644 index 0000000000000..bf2e2b44eb602 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml @@ -0,0 +1,15 @@ + + + +
+ + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml new file mode 100644 index 0000000000000..e37ce8f4738b3 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -0,0 +1,21 @@ + + + +
+ + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml new file mode 100644 index 0000000000000..e999413c96d74 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml @@ -0,0 +1,17 @@ + + + +
+ + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml new file mode 100644 index 0000000000000..2e5fcfb7b5c8d --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml @@ -0,0 +1,28 @@ + + + +
+ + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml new file mode 100644 index 0000000000000..eb7a9ce2c376e --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml @@ -0,0 +1,23 @@ + + + + +
+ + + + + + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml new file mode 100644 index 0000000000000..63cbadc71d3d3 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml @@ -0,0 +1,17 @@ + + + +
+ + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml new file mode 100644 index 0000000000000..9564bc61f799c --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml @@ -0,0 +1,17 @@ + + + +
+ + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml new file mode 100644 index 0000000000000..016af2e102744 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml @@ -0,0 +1,34 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfigurationPaymentSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfigurationPaymentSection.xml new file mode 100644 index 0000000000000..d9f2b14a40e2f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfigurationPaymentSection.xml @@ -0,0 +1,24 @@ + + + + +
+ + + + + + + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml new file mode 100644 index 0000000000000..32f02a69f817e --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml new file mode 100644 index 0000000000000..100407438eaae --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml new file mode 100644 index 0000000000000..885a45be721f1 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml new file mode 100644 index 0000000000000..e4a75b1b6a842 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml new file mode 100644 index 0000000000000..937afb83da96f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml new file mode 100644 index 0000000000000..d302f9c7d0cba --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml @@ -0,0 +1,33 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml new file mode 100644 index 0000000000000..13f59ad2cf18e --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml @@ -0,0 +1,35 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml new file mode 100644 index 0000000000000..42e451940c91b --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml new file mode 100644 index 0000000000000..267efdf3d0e5e --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml @@ -0,0 +1,20 @@ + + + + +
+ + + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml new file mode 100644 index 0000000000000..f094baa9f3446 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml new file mode 100644 index 0000000000000..3a07cbc6dd145 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml @@ -0,0 +1,24 @@ + + + + + +
+ + + +
+ +
+ + +
+ +
+ diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml b/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml new file mode 100644 index 0000000000000..f27477ce8a672 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml @@ -0,0 +1,110 @@ + + + + + + + + + + <description value="Use saved for Braintree credit card on checkout with selecting billing address"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93767"/> + <group value="braintree"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <createData entity="BraintreeConfig" stepKey="BraintreeConfigurationData"/> + <createData entity="CustomBraintreeConfigurationData" stepKey="CustomBraintreeConfigurationData"/> + </before> + + <after> + <deleteData createDataKey="product" stepKey="deleteProduct1"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <createData entity="DefaultBraintreeConfig" stepKey="DefaultBraintreeConfig"/> + <createData entity="RollBackCustomBraintreeConfigurationData" stepKey="RollBackCustomBraintreeConfigurationData"/> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="StorefrontSignOutActionGroup"/> + </after> + <!--Go to storefront--> + <amOnPage url="" stepKey="DoToStorefront"/> + <!--Create account--> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUserFromStorefrontActionGroup"> + <argument name="Customer" value="Simple_US_Customer"/> + </actionGroup> + + <!--Add product to cart--> + <amOnPage url="$$product.sku$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <argument name="Address" value="US_Address_CA"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <!--Fill cart data--> + <click selector="{{BraintreeConfigurationPaymentSection.creditCart}}" stepKey="SelectBraintreePaymentMethod"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <scrollTo selector="{{BraintreeConfigurationPaymentSection.creditCart}}" stepKey="ScrollToCreditCardSection"/> + <actionGroup ref="StorefrontFillCartDataActionGroup" stepKey="StorefrontFillCartDataActionGroup"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <!--Place order--> + <click selector="{{BraintreeConfigurationPaymentSection.paymentMethodContainer}}{{CheckoutPaymentSection.placeOrder}}" + stepKey="PlaceOrder"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + + <!--Add product to cart again--> + <amOnPage url="$$product.sku$$.html" stepKey="goToProductPage1"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart1"/> + <waitForPageLoad stepKey="waitForPageLoad7"/> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup1"/> + <click selector="{{CheckoutPaymentSection.addressAction('New Address')}}" stepKey="clickOnNewAddress"/> + <waitForPageLoad stepKey="waitForPageLoad8"/> + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="LoggedInCheckoutFillNewBillingAddressActionGroup1"> + <argument name="Address" value="US_Address_NY"/> + </actionGroup> + <click selector="{{CheckoutPaymentSection.addressAction('Save Address')}}" stepKey="SaveAddress"/> + <waitForPageLoad stepKey="waitForPageLoad9"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> + <waitForPageLoad stepKey="waitForPageLoad10"/> + <click selector="{{BraintreeConfigurationPaymentSection.paymentMethod}}" stepKey="SelectBraintreePaymentMethod1"/> + <waitForPageLoad stepKey="waitForPageLoad11"/> + <click selector="{{CheckoutPaymentSection.shippingAndBillingAddressSame}}" stepKey="UncheckCheckBox"/> + + <click selector="{{CheckoutShippingSection.updateAddress}}" stepKey="clickToUpdate"/> + <waitForPageLoad stepKey="waitForPageLoad12"/> + <!--Place order--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="PlaceOrder1"/> + <waitForPageLoad stepKey="waitForPageLoad13"/> + + <click selector="{{CheckoutOrderSummarySection.orderNumber}}" stepKey="ClickOnOrderNumber"/> + <waitForPageLoad stepKey="waitForPageLoad14"/> + <!--Check billing and shipping addresses also additional Address info--> + <click selector="{{CheckoutPaymentSection.addressBook}}" stepKey="goToAddressBook"/> + <grabTextFrom selector="{{CheckoutOrderSummarySection.shippingAddress}}" stepKey="shippingAddr"/> + <grabTextFrom selector="{{CheckoutOrderSummarySection.billingAddress}}" stepKey="billingAddr"/> + <grabTextFrom selector="{{CheckoutOrderSummarySection.additionalAddress}}" stepKey="additionalAddress"/> + <see userInput="Shipping Address" stepKey="seeShippingAddress"/> + <see userInput="Billing Address" stepKey="seeBillingAddress"/> + <assertEquals stepKey="assertValuesAreEqual" actual="$billingAddr" expected="$shippingAddr"/> + <assertNotEquals stepKey="assertValuesAreNotEqual" actual="$billingAddr" expected="$additionalAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml new file mode 100644 index 0000000000000..2ddefa40b536c --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CreateAnAdminOrderUsingBraintreePaymentTest1"> + <annotations> + <features value="Backend"/> + <stories value="Creation an admin order using Braintree payment"/> + <title value="Create order using Braintree payment"/> + <description value="Admin should be able to create order using Braintree payment"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-93677"/> + <group value="braintree"/> + </annotations> + + + <before> + <!--Login As Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--CreateNewProduct--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create New Customer--> + <createData stepKey="createCustomer" entity="Simple_US_Customer"/> + </before> + + + <!--Configure Braintree--> + <actionGroup ref="ConfigureBraintree" stepKey="configureBraintree"/> + + <!--Create New Role--> + <actionGroup ref="GoToUserRoles" stepKey="GoToUserRoles"/> + <actionGroup ref="AdminCreateRole" stepKey="AdminCreateNewRole"/> + + <!--Create New User With Specific Role--> + <actionGroup ref="GoToAllUsers" stepKey="GoToAllUsers"/> + <actionGroup ref="AdminCreateUserAction" stepKey="AdminCreateNewUser"/> + + <!--SignOut--> + <actionGroup ref="logout" stepKey="signOutFromAdmin"/> + + <!--SignIn New User--> + <actionGroup ref="LoginNewUser" stepKey="signInNewUser"/> + <waitForPageLoad stepKey="waitForLogin" time="3"/> + + <!--Create New Order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrder"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + + <actionGroup ref="addSimpleProductToOrder" stepKey="addProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + + <waitForPageLoad stepKey="waitForShippingToFinish"/> + + <actionGroup ref="useBraintreeForMasterCard" stepKey="selectCardWithBraintree"/> + + <click stepKey="submitOrder" selector="{{NewOrderSection.submitOrder}}"/> + <waitForPageLoad stepKey="waitForSaveConfig" time="5"/> + <waitForElementVisible selector="{{NewOrderSection.successMessage}}" stepKey="waitForSuccessMessage" time="1"/> + + <after> + <!-- Disable BrainTree --> + <actionGroup ref="DisableBrainTree" stepKey="disableBrainTree"/> + + <!--SignOut--> + <actionGroup ref="SignOut" stepKey="signOutFromNewUser"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Delete Product--> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete Customer--> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!--Delete User --> + <actionGroup ref="GoToAllUsers" stepKey="GoBackToAllUsers"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="AdminDeleteUserActionGroup"/> + + <!--Delete Role--> + <actionGroup ref="GoToUserRoles" stepKey="GoBackToUserRoles"/> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="AdminDeleteRoleActionGroup"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/CretateAdminOrderWithOnlinePaymentIncludingTaxAndDiscount.xml b/app/code/Magento/Braintree/Test/Mftf/Test/CretateAdminOrderWithOnlinePaymentIncludingTaxAndDiscount.xml new file mode 100644 index 0000000000000..cf51f29db79f9 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Test/CretateAdminOrderWithOnlinePaymentIncludingTaxAndDiscount.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CreateAdminOrderPayedWithOnlinePaymentIncludingTaxAndDiscount"> + <annotations> + <features value="Braintree"/> + <stories value="Get access to a New Credit Memo Page from Invoice for Order payed with online payment via Admin"/> + <title value="Admin should be able to open a New Credit Memo Page from Invoice Page for Order with tax and discount and payed using online payment method"/> + <description value="Admin should be able to open a New Credit Memo Page from Invoice Page for Order with tax and discount and payed using online payment method"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94472"/> + <group value="braintree"/> + </annotations> + + <before> + <!--Create Default Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!--Create Simple product--> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create Tax Rule is based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + + <!--Configure Braintree Payment method--> + <createData entity="BraintreeConfig" stepKey="BraintreeConfigurationData"/> + <createData entity="CustomBraintreeConfigurationData" stepKey="enableBraintree"/> + + <!--Create Retailer Customer with US_CA address--> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"> + <field key="group_id">3</field> + </createData> + + <!--Login as Admin User--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <!--Delete Cart Price Rule--> + <actionGroup ref="AdminDeleteCartPriceRuleForRetailerActionGroup" stepKey="deleteSalesRule"/> + + <!--Set to default configuration Tax Shipping Class--> + <actionGroup ref="setDefaultShippingTaxClass" stepKey="setdefaultClass"/> + + <!--Delete Simple Sub Category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Delete Simple Product--> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete Tax Rule --> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + + <!-- Rollback Braintree to Default --> + <createData entity="RollBackCustomBraintreeConfigurationData" stepKey="rollbackBraintreeConfig"/> + + <!--Delete Customer--> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + + <!--Log Out--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create a cart price rule with 10% discount for whole cart --> + <click selector="{{AdminMenuSection.marketing}}" stepKey="clickOnMarketing" /> + <waitForPageLoad stepKey="waitForMarketing"/> + <click selector="{{CartPriceRulesSubmenuSection.cartPriceRules}}" stepKey="clickOnCartPriceRules"/> + <waitForPageLoad stepKey="waitForCartPriceRules"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectRetailerCustomerGroup" stepKey="selectRetailerCustomerGroup"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Percent of product price discount" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="10" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCartRuleLoad"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!--Set Taxable Goods for Shipping Tax Class--> + <actionGroup ref="changeShippingTaxClass" stepKey="changeShippingTaxClass"/> + + <!--Adding Special price to product--> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct.id$$)}}" stepKey="openAdminProductEditPage"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="addSpecialPrice"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Create New Order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Add a product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Select Braintree online Payment method --> + <actionGroup ref="AdminOrderBraintreeFillActionGroup" stepKey="selectCreditCardPayment"/> + + <!--Submit Order--> + <click stepKey="submitOrder" selector="{{NewOrderSection.submitOrder}}"/> + <waitForPageLoad stepKey="waitForSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeOrderSuccessMessage" after="waitForSubmitOrder"/> + + <!-- Create New invoice--> + <actionGroup ref="adminFastCreateInvoice" stepKey="createInvoice"/> + + <!--Get access to Credit Memo page from Invoice page--> + <click selector="{{AdminInvoiceMainActionsSection.openNewCreditMemoFromInvoice}}" stepKey="clickCreateNewCreditMemo"/> + <waitForPageLoad stepKey="waitForLoadNewCreditMemoPage"/> + <see selector="{{AdminCreditMemoOrderInformationSection.orderStatus}}" userInput="Processing" stepKey="seeNewCreditMemo"/> + </test> +</tests> diff --git a/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php b/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php index 5b28e6d8aedcd..fa5512ca2db9f 100644 --- a/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php @@ -13,13 +13,11 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Payment\Helper\Data; use Magento\Payment\Model\Config; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\StoreManagerInterface; use Magento\Vault\Model\VaultPaymentInterface; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class FormTest + * Tests \Magento\Braintree\Block\Form. */ class FormTest extends \PHPUnit\Framework\TestCase { @@ -45,36 +43,35 @@ class FormTest extends \PHPUnit\Framework\TestCase /** * @var Quote|MockObject */ - private $sessionQuote; + private $sessionQuoteMock; /** * @var Config|MockObject */ - private $gatewayConfig; + private $gatewayConfigMock; /** * @var CcType|MockObject */ - private $ccType; + private $ccTypeMock; /** - * @var StoreManagerInterface|MockObject + * @var Data|MockObject */ - private $storeManager; + private $paymentDataHelperMock; /** - * @var Data|MockObject + * @var string */ - private $paymentDataHelper; + private $storeId = '1'; protected function setUp() { $this->initCcTypeMock(); $this->initSessionQuoteMock(); $this->initGatewayConfigMock(); - - $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); - $this->paymentDataHelper = $this->getMockBuilder(Data::class) + + $this->paymentDataHelperMock = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->setMethods(['getMethodInstance']) ->getMock(); @@ -82,13 +79,11 @@ protected function setUp() $managerHelper = new ObjectManager($this); $this->block = $managerHelper->getObject(Form::class, [ 'paymentConfig' => $managerHelper->getObject(Config::class), - 'sessionQuote' => $this->sessionQuote, - 'gatewayConfig' => $this->gatewayConfig, - 'ccType' => $this->ccType, - 'storeManager' => $this->storeManager + 'sessionQuote' => $this->sessionQuoteMock, + 'gatewayConfig' => $this->gatewayConfigMock, + 'ccType' => $this->ccTypeMock, + 'paymentDataHelper' =>$this->paymentDataHelperMock, ]); - - $managerHelper->setBackwardCompatibleProperty($this->block, 'paymentDataHelper', $this->paymentDataHelper); } /** @@ -100,17 +95,18 @@ protected function setUp() */ public function testGetCcAvailableTypes($countryId, array $availableTypes, array $expected) { - $this->sessionQuote->expects(static::once()) + $this->sessionQuoteMock->expects(static::once()) ->method('getCountryId') ->willReturn($countryId); - $this->gatewayConfig->expects(static::once()) + $this->gatewayConfigMock->expects(static::once()) ->method('getAvailableCardTypes') + ->with($this->storeId) ->willReturn(self::$configCardTypes); - $this->gatewayConfig->expects(static::once()) + $this->gatewayConfigMock->expects(static::once()) ->method('getCountryAvailableCardTypes') - ->with($countryId) + ->with($countryId, $this->storeId) ->willReturn($availableTypes); $result = $this->block->getCcAvailableTypes(); @@ -127,7 +123,7 @@ public function countryCardTypesDataProvider() ['US', ['AE', 'VI'], ['American Express', 'Visa']], ['UK', ['VI'], ['Visa']], ['CA', ['MC'], ['MasterCard']], - ['UA', [], ['American Express', 'Visa', 'MasterCard', 'Discover', 'JBC']] + ['UA', [], ['American Express', 'Visa', 'MasterCard', 'Discover', 'JBC']], ]; } @@ -136,25 +132,15 @@ public function countryCardTypesDataProvider() */ public function testIsVaultEnabled() { - $storeId = 1; - $store = $this->getMockForAbstractClass(StoreInterface::class); - $this->storeManager->expects(static::once()) - ->method('getStore') - ->willReturn($store); - - $store->expects(static::once()) - ->method('getId') - ->willReturn($storeId); - $vaultPayment = $this->getMockForAbstractClass(VaultPaymentInterface::class); - $this->paymentDataHelper->expects(static::once()) + $this->paymentDataHelperMock->expects(static::once()) ->method('getMethodInstance') ->with(ConfigProvider::CC_VAULT_CODE) ->willReturn($vaultPayment); $vaultPayment->expects(static::once()) ->method('isActive') - ->with($storeId) + ->with($this->storeId) ->willReturn(true); static::assertTrue($this->block->isVaultEnabled()); @@ -165,12 +151,12 @@ public function testIsVaultEnabled() */ private function initCcTypeMock() { - $this->ccType = $this->getMockBuilder(CcType::class) + $this->ccTypeMock = $this->getMockBuilder(CcType::class) ->disableOriginalConstructor() ->setMethods(['getCcTypeLabelMap']) ->getMock(); - $this->ccType->expects(static::any()) + $this->ccTypeMock->expects(static::any()) ->method('getCcTypeLabelMap') ->willReturn(self::$baseCardTypes); } @@ -180,17 +166,20 @@ private function initCcTypeMock() */ private function initSessionQuoteMock() { - $this->sessionQuote = $this->getMockBuilder(Quote::class) + $this->sessionQuoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() - ->setMethods(['getQuote', 'getBillingAddress', 'getCountryId', '__wakeup']) + ->setMethods(['getQuote', 'getBillingAddress', 'getCountryId', '__wakeup', 'getStoreId']) ->getMock(); - $this->sessionQuote->expects(static::any()) + $this->sessionQuoteMock->expects(static::any()) ->method('getQuote') ->willReturnSelf(); - $this->sessionQuote->expects(static::any()) + $this->sessionQuoteMock->expects(static::any()) ->method('getBillingAddress') ->willReturnSelf(); + $this->sessionQuoteMock->expects(static::any()) + ->method('getStoreId') + ->willReturn($this->storeId); } /** @@ -198,7 +187,7 @@ private function initSessionQuoteMock() */ private function initGatewayConfigMock() { - $this->gatewayConfig = $this->getMockBuilder(GatewayConfig::class) + $this->gatewayConfigMock = $this->getMockBuilder(GatewayConfig::class) ->disableOriginalConstructor() ->setMethods(['getCountryAvailableCardTypes', 'getAvailableCardTypes']) ->getMock(); diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Adminhtml/Payment/GetClientTokenTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Adminhtml/Payment/GetClientTokenTest.php new file mode 100644 index 0000000000000..95ea2a07d4368 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Adminhtml/Payment/GetClientTokenTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Braintree\Test\Unit\Controller\Adminhtml\Payment; + +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\Session\Quote; +use Magento\Braintree\Controller\Adminhtml\Payment\GetClientToken; +use Magento\Braintree\Gateway\Config\Config; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Tests \Magento\Braintree\Controller\Adminhtml\Payment\GetClientToken + */ +class GetClientTokenTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GetClientToken + */ + private $action; + + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var BraintreeAdapterFactory|MockObject + */ + private $adapterFactoryMock; + + /** + * @var Quote|MockObject + */ + private $quoteSessionMock; + + /** + * @var ResultFactory|MockObject + */ + private $resultFactoryMock; + + protected function setUp() + { + $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->setMethods(['getResultFactory']) + ->getMock(); + $context->expects(static::any()) + ->method('getResultFactory') + ->willReturn($this->resultFactoryMock); + $this->configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->setMethods(['getMerchantAccountId']) + ->getMock(); + $this->adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->quoteSessionMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods(['getStoreId']) + ->getMock(); + + $managerHelper = new ObjectManager($this); + $this->action = $managerHelper->getObject(GetClientToken::class, [ + 'context' => $context, + 'config' => $this->configMock, + 'adapterFactory' => $this->adapterFactoryMock, + 'quoteSession' => $this->quoteSessionMock, + ]); + } + + public function testExecute() + { + $storeId = '1'; + $clientToken = 'client_token'; + $responseMock = $this->getMockBuilder(ResultInterface::class) + ->setMethods(['setHttpResponseCode', 'renderResult', 'setHeader', 'setData']) + ->getMock(); + $responseMock->expects(static::once()) + ->method('setData') + ->with(['clientToken' => $clientToken]) + ->willReturn($responseMock); + $this->resultFactoryMock->expects(static::once()) + ->method('create') + ->willReturn($responseMock); + $this->quoteSessionMock->expects(static::once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->configMock->expects(static::once()) + ->method('getMerchantAccountId') + ->with($storeId) + ->willReturn(null); + $adapterMock = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['generate']) + ->getMock(); + $adapterMock->expects(static::once()) + ->method('generate') + ->willReturn($clientToken); + $this->adapterFactoryMock->expects(static::once()) + ->method('create') + ->willReturn($adapterMock); + + $this->action->execute(); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php index e78e54f011d44..4af63a9c87151 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php @@ -15,6 +15,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Webapi\Exception; use Magento\Payment\Gateway\Command\ResultInterface as CommandResultInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; /** @@ -30,81 +31,84 @@ class GetNonceTest extends \PHPUnit\Framework\TestCase private $action; /** - * @var GetPaymentNonceCommand|\PHPUnit_Framework_MockObject_MockObject + * @var GetPaymentNonceCommand|MockObject */ - private $command; + private $commandMock; /** - * @var Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - private $session; + private $sessionMock; /** - * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var LoggerInterface|MockObject */ - private $logger; + private $loggerMock; /** - * @var ResultFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResultFactory|MockObject */ - private $resultFactory; + private $resultFactoryMock; /** - * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResultInterface|MockObject */ - private $result; + private $resultMock; /** - * @var Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|MockObject */ - private $request; + private $requestMock; /** - * @var CommandResultInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CommandResultInterface|MockObject */ - private $commandResult; + private $commandResultMock; protected function setUp() { $this->initResultFactoryMock(); - $this->request = $this->getMockBuilder(RequestInterface::class) + $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->setMethods(['getParam']) ->getMock(); - $this->command = $this->getMockBuilder(GetPaymentNonceCommand::class) + $this->commandMock = $this->getMockBuilder(GetPaymentNonceCommand::class) ->disableOriginalConstructor() ->setMethods(['execute', '__wakeup']) ->getMock(); - $this->commandResult = $this->getMockBuilder(CommandResultInterface::class) + $this->commandResultMock = $this->getMockBuilder(CommandResultInterface::class) ->setMethods(['get']) ->getMock(); - $this->session = $this->getMockBuilder(Session::class) + $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() - ->setMethods(['getCustomerId']) + ->setMethods(['getCustomerId', 'getStoreId']) ->getMock(); + $this->sessionMock->expects(static::once()) + ->method('getStoreId') + ->willReturn(null); - $this->logger = $this->createMock(LoggerInterface::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); $context->expects(static::any()) ->method('getRequest') - ->willReturn($this->request); + ->willReturn($this->requestMock); $context->expects(static::any()) ->method('getResultFactory') - ->willReturn($this->resultFactory); + ->willReturn($this->resultFactoryMock); $managerHelper = new ObjectManager($this); $this->action = $managerHelper->getObject(GetNonce::class, [ 'context' => $context, - 'logger' => $this->logger, - 'session' => $this->session, - 'command' => $this->command + 'logger' => $this->loggerMock, + 'session' => $this->sessionMock, + 'command' => $this->commandMock, ]); } @@ -113,28 +117,28 @@ protected function setUp() */ public function testExecuteWithException() { - $this->request->expects(static::once()) + $this->requestMock->expects(static::once()) ->method('getParam') ->with('public_hash') ->willReturn(null); - $this->session->expects(static::once()) + $this->sessionMock->expects(static::once()) ->method('getCustomerId') ->willReturn(null); $exception = new \Exception('The "publicHash" field does not exists'); - $this->command->expects(static::once()) + $this->commandMock->expects(static::once()) ->method('execute') ->willThrowException($exception); - $this->logger->expects(static::once()) + $this->loggerMock->expects(static::once()) ->method('critical') ->with($exception); - $this->result->expects(static::once()) + $this->resultMock->expects(static::once()) ->method('setHttpResponseCode') ->with(Exception::HTTP_BAD_REQUEST); - $this->result->expects(static::once()) + $this->resultMock->expects(static::once()) ->method('setData') ->with(['message' => 'Sorry, but something went wrong']); @@ -150,32 +154,32 @@ public function testExecute() $publicHash = '65b7bae0dcb690d93'; $nonce = 'f1hc45'; - $this->request->expects(static::once()) + $this->requestMock->expects(static::once()) ->method('getParam') ->with('public_hash') ->willReturn($publicHash); - $this->session->expects(static::once()) + $this->sessionMock->expects(static::once()) ->method('getCustomerId') ->willReturn($customerId); - $this->commandResult->expects(static::once()) + $this->commandResultMock->expects(static::once()) ->method('get') ->willReturn([ 'paymentMethodNonce' => $nonce ]); - $this->command->expects(static::once()) + $this->commandMock->expects(static::once()) ->method('execute') - ->willReturn($this->commandResult); + ->willReturn($this->commandResultMock); - $this->result->expects(static::once()) + $this->resultMock->expects(static::once()) ->method('setData') ->with(['paymentMethodNonce' => $nonce]); - $this->logger->expects(static::never()) + $this->loggerMock->expects(static::never()) ->method('critical'); - $this->result->expects(static::never()) + $this->resultMock->expects(static::never()) ->method('setHttpResponseCode'); $this->action->execute(); @@ -186,17 +190,17 @@ public function testExecute() */ private function initResultFactoryMock() { - $this->result = $this->getMockBuilder(ResultInterface::class) + $this->resultMock = $this->getMockBuilder(ResultInterface::class) ->setMethods(['setHttpResponseCode', 'renderResult', 'setHeader', 'setData']) ->getMock(); - $this->resultFactory = $this->getMockBuilder(ResultFactory::class) + $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->resultFactory->expects(static::once()) + $this->resultFactoryMock->expects(static::once()) ->method('create') - ->willReturn($this->result); + ->willReturn($this->resultMock); } } diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php index 5a10b4abb3fbc..9c25846e56da0 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Controller\Paypal; use Magento\Braintree\Controller\Paypal\PlaceOrder; @@ -15,6 +17,8 @@ use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Quote\Model\Quote; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; /** * Class PlaceOrderTest @@ -26,34 +30,34 @@ class PlaceOrderTest extends \PHPUnit\Framework\TestCase { /** - * @var OrderPlace|\PHPUnit_Framework_MockObject_MockObject + * @var OrderPlace|MockObject */ - private $orderPlaceMock; + private $orderPlace; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ - private $configMock; + private $config; /** - * @var Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - private $checkoutSessionMock; + private $checkoutSession; /** - * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RequestInterface|MockObject */ - private $requestMock; + private $request; /** - * @var ResultFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResultFactory|MockObject */ - private $resultFactoryMock; + private $resultFactory; /** - * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ManagerInterface|MockObject */ - protected $messageManagerMock; + private $messageManager; /** * @var PlaceOrder @@ -61,139 +65,143 @@ class PlaceOrderTest extends \PHPUnit\Framework\TestCase private $placeOrder; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - private $loggerMock; + private $logger; + /** + * @inheritdoc + */ protected function setUp() { - /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ - $contextMock = $this->getMockBuilder(Context::class) + /** @var Context|MockObject $context */ + $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); - $this->requestMock = $this->getMockBuilder(RequestInterface::class) + $this->request = $this->getMockBuilder(RequestInterface::class) ->setMethods(['getPostValue']) ->getMockForAbstractClass(); - $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + $this->resultFactory = $this->getMockBuilder(ResultFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->checkoutSessionMock = $this->getMockBuilder(Session::class) + $this->checkoutSession = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() ->getMock(); - $this->configMock = $this->getMockBuilder(Config::class) + $this->config = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->orderPlaceMock = $this->getMockBuilder(OrderPlace::class) + $this->orderPlace = $this->getMockBuilder(OrderPlace::class) ->disableOriginalConstructor() ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) + $this->messageManager = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); - $contextMock->expects(self::once()) - ->method('getRequest') - ->willReturn($this->requestMock); - $contextMock->expects(self::once()) - ->method('getResultFactory') - ->willReturn($this->resultFactoryMock); - $contextMock->expects(self::once()) - ->method('getMessageManager') - ->willReturn($this->messageManagerMock); - - $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + $context->method('getRequest') + ->willReturn($this->request); + $context->method('getResultFactory') + ->willReturn($this->resultFactory); + $context->method('getMessageManager') + ->willReturn($this->messageManager); + + $this->logger = $this->getMockBuilder(LoggerInterface::class) ->disableOriginalConstructor() ->getMock(); $this->placeOrder = new PlaceOrder( - $contextMock, - $this->configMock, - $this->checkoutSessionMock, - $this->orderPlaceMock, - $this->loggerMock + $context, + $this->config, + $this->checkoutSession, + $this->orderPlace, + $this->logger ); } + /** + * Checks if an order is placed successfully. + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NotFoundException + */ public function testExecute() { $agreement = ['test-data']; $quoteMock = $this->getQuoteMock(); - $quoteMock->expects(self::once()) - ->method('getItemsCount') + $quoteMock->method('getItemsCount') ->willReturn(1); $resultMock = $this->getResultMock(); - $resultMock->expects(self::once()) - ->method('setPath') + $resultMock->method('setPath') ->with('checkout/onepage/success') ->willReturnSelf(); - $this->resultFactoryMock->expects(self::once()) - ->method('create') + $this->resultFactory->method('create') ->with(ResultFactory::TYPE_REDIRECT) ->willReturn($resultMock); - $this->requestMock->expects(self::once()) - ->method('getPostValue') + $this->request->method('getPostValue') ->with('agreement', []) ->willReturn($agreement); - $this->checkoutSessionMock->expects(self::once()) - ->method('getQuote') + $this->checkoutSession->method('getQuote') ->willReturn($quoteMock); - $this->orderPlaceMock->expects(self::once()) - ->method('execute') + $this->orderPlace->method('execute') ->with($quoteMock, [0]); - $this->messageManagerMock->expects(self::never()) + $this->messageManager->expects(self::never()) ->method('addExceptionMessage'); self::assertEquals($this->placeOrder->execute(), $resultMock); } + /** + * Checks a negative scenario during place order action. + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NotFoundException + */ public function testExecuteException() { $agreement = ['test-data']; $quote = $this->getQuoteMock(); - $quote->expects(self::once()) - ->method('getItemsCount') + $quote->method('getItemsCount') ->willReturn(0); + $quote->method('getReservedOrderId') + ->willReturn('000000111'); $resultMock = $this->getResultMock(); - $resultMock->expects(self::once()) - ->method('setPath') + $resultMock->method('setPath') ->with('checkout/cart') ->willReturnSelf(); - $this->resultFactoryMock->expects(self::once()) - ->method('create') + $this->resultFactory->method('create') ->with(ResultFactory::TYPE_REDIRECT) ->willReturn($resultMock); - $this->requestMock->expects(self::once()) - ->method('getPostValue') + $this->request->method('getPostValue') ->with('agreement', []) ->willReturn($agreement); - $this->checkoutSessionMock->expects(self::once()) - ->method('getQuote') + $this->checkoutSession->method('getQuote') ->willReturn($quote); - $this->orderPlaceMock->expects(self::never()) + $this->orderPlace->expects(self::never()) ->method('execute'); - $this->messageManagerMock->expects(self::once()) - ->method('addExceptionMessage') + $this->messageManager->method('addExceptionMessage') ->with( self::isInstanceOf('\InvalidArgumentException'), - 'We can\'t initialize checkout.' + 'The order #000000111 cannot be processed.' ); self::assertEquals($this->placeOrder->execute(), $resultMock); } /** - * @return ResultInterface|\PHPUnit_Framework_MockObject_MockObject + * Gets mock object for a result. + * + * @return ResultInterface|MockObject */ private function getResultMock() { @@ -203,7 +211,9 @@ private function getResultMock() } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Gets mock object for a quote. + * + * @return Quote|MockObject */ private function getQuoteMock() { diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php index cb911a8396b36..609b7f21dbf87 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Braintree\Test\Unit\Controller\Paypal; use Magento\Quote\Model\Quote; @@ -188,7 +189,7 @@ public function testExecuteException() ->method('addExceptionMessage') ->with( self::isInstanceOf('\InvalidArgumentException'), - 'We can\'t initialize checkout.' + 'Checkout failed to initialize. Verify and try again.' ); $this->resultFactoryMock->expects(self::once()) @@ -235,7 +236,7 @@ public function testExecuteExceptionPaymentWithoutNonce() ->method('addExceptionMessage') ->with( self::isInstanceOf(\Magento\Framework\Exception\LocalizedException::class), - 'We can\'t initialize checkout.' + 'Checkout failed to initialize. Verify and try again.' ); $this->resultFactoryMock->expects(self::once()) diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/SaveShippingMethodTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/SaveShippingMethodTest.php index 5be5df0e33c49..32ed698189fa7 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/SaveShippingMethodTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/SaveShippingMethodTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Braintree\Test\Unit\Controller\Paypal; use Magento\Quote\Model\Quote; @@ -225,7 +226,10 @@ public function testExecuteAjaxException() $this->messageManagerMock->expects(self::once()) ->method('addExceptionMessage') - ->with(self::isInstanceOf('\InvalidArgumentException'), 'We can\'t initialize checkout.'); + ->with( + self::isInstanceOf('\InvalidArgumentException'), + 'Checkout failed to initialize. Verify and try again.' + ); $this->urlMock->expects(self::once()) ->method('getUrl') @@ -265,7 +269,10 @@ public function testExecuteException() $this->messageManagerMock->expects(self::once()) ->method('addExceptionMessage') - ->with(self::isInstanceOf('\InvalidArgumentException'), 'We can\'t initialize checkout.'); + ->with( + self::isInstanceOf('\InvalidArgumentException'), + 'Checkout failed to initialize. Verify and try again.' + ); $this->urlMock->expects(self::once()) ->method('getUrl') diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php index 56ea1f97fa165..845a02930d709 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php @@ -6,26 +6,25 @@ namespace Magento\Braintree\Test\Unit\Gateway\Command; use Braintree\IsNode; -use Braintree\MultipleValueNode; -use Braintree\TextNode; use Magento\Braintree\Gateway\Command\CaptureStrategyCommand; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Braintree\Model\Adapter\BraintreeSearchAdapter; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\Search\SearchCriteria; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Payment\Gateway\Command; use Magento\Payment\Gateway\Command\CommandPoolInterface; use Magento\Payment\Gateway\Command\GatewayCommand; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Payment\Gateway\Data\PaymentDataObject; use Magento\Sales\Api\TransactionRepositoryInterface; use Magento\Sales\Model\Order\Payment; -use Magento\Sales\Model\Order\Payment\Transaction; use Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\CollectionFactory; -use Magento\Braintree\Model\Adapter\BraintreeAdapter; -use Magento\Braintree\Model\Adapter\BraintreeSearchAdapter; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class CaptureStrategyCommandTest + * Tests \Magento\Braintree\Gateway\Command\CaptureStrategyCommand. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -37,44 +36,44 @@ class CaptureStrategyCommandTest extends \PHPUnit\Framework\TestCase private $strategyCommand; /** - * @var CommandPoolInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CommandPoolInterface|MockObject */ - private $commandPool; + private $commandPoolMock; /** - * @var TransactionRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var TransactionRepositoryInterface|MockObject */ - private $transactionRepository; + private $transactionRepositoryMock; /** - * @var FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var FilterBuilder|MockObject */ - private $filterBuilder; + private $filterBuilderMock; /** - * @var SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var SearchCriteriaBuilder|MockObject */ - private $searchCriteriaBuilder; + private $searchCriteriaBuilderMock; /** - * @var Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Payment|MockObject */ - private $payment; + private $paymentMock; /** - * @var GatewayCommand|\PHPUnit_Framework_MockObject_MockObject + * @var GatewayCommand|MockObject */ - private $command; + private $commandMock; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ private $subjectReaderMock; /** - * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var BraintreeAdapter|MockObject */ - private $braintreeAdapter; + private $braintreeAdapterMock; /** * @var BraintreeSearchAdapter @@ -83,7 +82,7 @@ class CaptureStrategyCommandTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->commandPool = $this->getMockBuilder(CommandPoolInterface::class) + $this->commandPoolMock = $this->getMockBuilder(CommandPoolInterface::class) ->disableOriginalConstructor() ->setMethods(['get', '__wakeup']) ->getMock(); @@ -97,18 +96,26 @@ protected function setUp() $this->initFilterBuilderMock(); $this->initSearchCriteriaBuilderMock(); - $this->braintreeAdapter = $this->getMockBuilder(BraintreeAdapter::class) + $this->braintreeAdapterMock = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) ->disableOriginalConstructor() ->getMock(); + $adapterFactoryMock->expects(self::any()) + ->method('create') + ->willReturn($this->braintreeAdapterMock); + $this->braintreeSearchAdapter = new BraintreeSearchAdapter(); $this->strategyCommand = new CaptureStrategyCommand( - $this->commandPool, - $this->transactionRepository, - $this->filterBuilder, - $this->searchCriteriaBuilder, + $this->commandPoolMock, + $this->transactionRepositoryMock, + $this->filterBuilderMock, + $this->searchCriteriaBuilderMock, $this->subjectReaderMock, - $this->braintreeAdapter, + $adapterFactoryMock, $this->braintreeSearchAdapter ); } @@ -126,24 +133,24 @@ public function testSaleExecute() ->with($subject) ->willReturn($paymentData); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getAuthorizationTransaction') ->willReturn(false); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getId') ->willReturn(1); $this->buildSearchCriteria(); - $this->transactionRepository->expects(static::once()) + $this->transactionRepositoryMock->expects(static::once()) ->method('getTotalCount') ->willReturn(0); - $this->commandPool->expects(static::once()) + $this->commandPoolMock->expects(static::once()) ->method('get') ->with(CaptureStrategyCommand::SALE) - ->willReturn($this->command); + ->willReturn($this->commandMock); $this->strategyCommand->execute($subject); } @@ -162,20 +169,20 @@ public function testCaptureExecute() ->with($subject) ->willReturn($paymentData); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getAuthorizationTransaction') ->willReturn(true); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getLastTransId') ->willReturn($lastTransId); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getId') ->willReturn(1); $this->buildSearchCriteria(); - $this->transactionRepository->expects(static::once()) + $this->transactionRepositoryMock->expects(static::once()) ->method('getTotalCount') ->willReturn(0); @@ -185,17 +192,17 @@ public function testCaptureExecute() ->method('maximumCount') ->willReturn(0); - $this->commandPool->expects(static::once()) + $this->commandPoolMock->expects(static::once()) ->method('get') ->with(CaptureStrategyCommand::CAPTURE) - ->willReturn($this->command); + ->willReturn($this->commandMock); $this->strategyCommand->execute($subject); } /** * @param string $lastTransactionId - * @return \Braintree\ResourceCollection|\PHPUnit_Framework_MockObject_MockObject + * @return \Braintree\ResourceCollection|MockObject */ private function getNotExpiredExpectedCollection($lastTransactionId) { @@ -208,7 +215,7 @@ private function getNotExpiredExpectedCollection($lastTransactionId) ->disableOriginalConstructor() ->getMock(); - $this->braintreeAdapter->expects(static::once()) + $this->braintreeAdapterMock->expects(static::once()) ->method('search') ->with( static::callback( @@ -247,20 +254,20 @@ public function testExpiredAuthorizationPerformVaultCaptureExecute() ->with($subject) ->willReturn($paymentData); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getAuthorizationTransaction') ->willReturn(true); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getLastTransId') ->willReturn($lastTransId); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getId') ->willReturn(1); $this->buildSearchCriteria(); - $this->transactionRepository->expects(static::once()) + $this->transactionRepositoryMock->expects(static::once()) ->method('getTotalCount') ->willReturn(0); @@ -270,10 +277,10 @@ public function testExpiredAuthorizationPerformVaultCaptureExecute() ->method('maximumCount') ->willReturn(1); - $this->commandPool->expects(static::once()) + $this->commandPoolMock->expects(static::once()) ->method('get') ->with(CaptureStrategyCommand::VAULT_CAPTURE) - ->willReturn($this->command); + ->willReturn($this->commandMock); $this->strategyCommand->execute($subject); } @@ -291,97 +298,104 @@ public function testVaultCaptureExecute() ->with($subject) ->willReturn($paymentData); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getAuthorizationTransaction') ->willReturn(true); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('getId') ->willReturn(1); $this->buildSearchCriteria(); - $this->transactionRepository->expects(static::once()) + $this->transactionRepositoryMock->expects(static::once()) ->method('getTotalCount') ->willReturn(1); - $this->commandPool->expects(static::once()) + $this->commandPoolMock->expects(static::once()) ->method('get') ->with(CaptureStrategyCommand::VAULT_CAPTURE) - ->willReturn($this->command); + ->willReturn($this->commandMock); $this->strategyCommand->execute($subject); } /** - * Create mock for payment data object and order payment - * @return \PHPUnit_Framework_MockObject_MockObject + * Creates mock for payment data object and order payment + * @return MockObject */ private function getPaymentDataObjectMock() { - $this->payment = $this->getMockBuilder(Payment::class) + $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->getMock(); $mock = $this->getMockBuilder(PaymentDataObject::class) - ->setMethods(['getPayment']) + ->setMethods(['getPayment', 'getOrder']) ->disableOriginalConstructor() ->getMock(); $mock->expects(static::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); + + $orderMock = $this->getMockBuilder(OrderAdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $mock->method('getOrder') + ->willReturn($orderMock); return $mock; } /** - * Create mock for gateway command object + * Creates mock for gateway command object */ private function initCommandMock() { - $this->command = $this->getMockBuilder(GatewayCommand::class) + $this->commandMock = $this->getMockBuilder(GatewayCommand::class) ->disableOriginalConstructor() ->setMethods(['execute']) ->getMock(); - $this->command->expects(static::once()) + $this->commandMock->expects(static::once()) ->method('execute') ->willReturn([]); } /** - * Create mock for filter object + * Creates mock for filter object */ private function initFilterBuilderMock() { - $this->filterBuilder = $this->getMockBuilder(FilterBuilder::class) + $this->filterBuilderMock = $this->getMockBuilder(FilterBuilder::class) ->disableOriginalConstructor() ->setMethods(['setField', 'setValue', 'create', '__wakeup']) ->getMock(); } /** - * Build search criteria + * Builds search criteria */ private function buildSearchCriteria() { - $this->filterBuilder->expects(static::exactly(2)) + $this->filterBuilderMock->expects(static::exactly(2)) ->method('setField') ->willReturnSelf(); - $this->filterBuilder->expects(static::exactly(2)) + $this->filterBuilderMock->expects(static::exactly(2)) ->method('setValue') ->willReturnSelf(); $searchCriteria = new SearchCriteria(); - $this->searchCriteriaBuilder->expects(static::exactly(2)) + $this->searchCriteriaBuilderMock->expects(static::exactly(2)) ->method('addFilters') ->willReturnSelf(); - $this->searchCriteriaBuilder->expects(static::once()) + $this->searchCriteriaBuilderMock->expects(static::once()) ->method('create') ->willReturn($searchCriteria); - $this->transactionRepository->expects(static::once()) + $this->transactionRepositoryMock->expects(static::once()) ->method('getList') ->with($searchCriteria) ->willReturnSelf(); @@ -392,7 +406,7 @@ private function buildSearchCriteria() */ private function initSearchCriteriaBuilderMock() { - $this->searchCriteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class) + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) ->disableOriginalConstructor() ->setMethods(['addFilters', 'create', '__wakeup']) ->getMock(); @@ -403,7 +417,7 @@ private function initSearchCriteriaBuilderMock() */ private function initTransactionRepositoryMock() { - $this->transactionRepository = $this->getMockBuilder(TransactionRepositoryInterface::class) + $this->transactionRepositoryMock = $this->getMockBuilder(TransactionRepositoryInterface::class) ->disableOriginalConstructor() ->setMethods(['getList', 'getTotalCount', 'delete', 'get', 'save', 'create', '__wakeup']) ->getMock(); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php index 333f29eb29136..23167af02a97b 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php @@ -6,15 +6,16 @@ namespace Magento\Braintree\Test\Unit\Gateway\Command; use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator; use Magento\Braintree\Model\Adapter\BraintreeAdapter; -use Magento\Payment\Gateway\Command; -use Magento\Payment\Gateway\Command\Result\ArrayResultFactory; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Payment\Gateway\Command\Result\ArrayResult; +use Magento\Payment\Gateway\Command\Result\ArrayResultFactory; use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Vault\Model\PaymentToken; use Magento\Vault\Model\PaymentTokenManagement; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class GetPaymentNonceCommandTest @@ -29,82 +30,89 @@ class GetPaymentNonceCommandTest extends \PHPUnit\Framework\TestCase private $command; /** - * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var BraintreeAdapter|MockObject */ - private $adapter; + private $adapterMock; /** - * @var PaymentTokenManagement|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentTokenManagement|MockObject */ - private $tokenManagement; + private $tokenManagementMock; /** - * @var PaymentToken|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentToken|MockObject */ - private $paymentToken; + private $paymentTokenMock; /** - * @var ArrayResultFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ArrayResultFactory|MockObject */ - private $resultFactory; + private $resultFactoryMock; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ - private $subjectReader; + private $subjectReaderMock; /** - * @var PaymentNonceResponseValidator|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentNonceResponseValidator|MockObject */ - private $responseValidator; + private $responseValidatorMock; /** - * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResultInterface|MockObject */ - private $validationResult; + private $validationResultMock; protected function setUp() { - $this->paymentToken = $this->getMockBuilder(PaymentToken::class) + $this->paymentTokenMock = $this->getMockBuilder(PaymentToken::class) ->disableOriginalConstructor() ->setMethods(['getGatewayToken']) ->getMock(); - $this->tokenManagement = $this->getMockBuilder(PaymentTokenManagement::class) + $this->tokenManagementMock = $this->getMockBuilder(PaymentTokenManagement::class) ->disableOriginalConstructor() ->setMethods(['getByPublicHash']) ->getMock(); - $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + $this->adapterMock = $this->getMockBuilder(BraintreeAdapter::class) ->disableOriginalConstructor() ->setMethods(['createNonce']) ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactoryMock->expects(self::any()) + ->method('create') + ->willReturn($this->adapterMock); - $this->resultFactory = $this->getMockBuilder(ArrayResultFactory::class) + $this->resultFactoryMock = $this->getMockBuilder(ArrayResultFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->setMethods(['readPublicHash', 'readCustomerId']) ->getMock(); - $this->validationResult = $this->getMockBuilder(ResultInterface::class) - ->setMethods(['isValid', 'getFailsDescription']) + $this->validationResultMock = $this->getMockBuilder(ResultInterface::class) + ->setMethods(['isValid', 'getFailsDescription', 'getErrorCodes']) ->getMock(); - $this->responseValidator = $this->getMockBuilder(PaymentNonceResponseValidator::class) + $this->responseValidatorMock = $this->getMockBuilder(PaymentNonceResponseValidator::class) ->disableOriginalConstructor() ->setMethods(['validate', 'isValid', 'getFailsDescription']) ->getMock(); $this->command = new GetPaymentNonceCommand( - $this->tokenManagement, - $this->adapter, - $this->resultFactory, - $this->subjectReader, - $this->responseValidator + $this->tokenManagementMock, + $adapterFactoryMock, + $this->resultFactoryMock, + $this->subjectReaderMock, + $this->responseValidatorMock ); } @@ -117,11 +125,11 @@ public function testExecuteWithExceptionForPublicHash() { $exception = new \InvalidArgumentException('The "publicHash" field does not exists'); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPublicHash') ->willThrowException($exception); - $this->subjectReader->expects(static::never()) + $this->subjectReaderMock->expects(self::never()) ->method('readCustomerId'); $this->command->execute([]); @@ -136,16 +144,16 @@ public function testExecuteWithExceptionForCustomerId() { $publicHash = '3wv2m24d2er3'; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPublicHash') ->willReturn($publicHash); $exception = new \InvalidArgumentException('The "customerId" field does not exists'); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readCustomerId') ->willThrowException($exception); - $this->tokenManagement->expects(static::never()) + $this->tokenManagementMock->expects(static::never()) ->method('getByPublicHash'); $this->command->execute(['publicHash' => $publicHash]); @@ -161,20 +169,20 @@ public function testExecuteWithExceptionForTokenManagement() $publicHash = '3wv2m24d2er3'; $customerId = 1; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPublicHash') ->willReturn($publicHash); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readCustomerId') ->willReturn($customerId); $exception = new \Exception('No available payment tokens'); - $this->tokenManagement->expects(static::once()) + $this->tokenManagementMock->expects(static::once()) ->method('getByPublicHash') ->willThrowException($exception); - $this->paymentToken->expects(static::never()) + $this->paymentTokenMock->expects(self::never()) ->method('getGatewayToken'); $this->command->execute(['publicHash' => $publicHash, 'customerId' => $customerId]); @@ -191,44 +199,44 @@ public function testExecuteWithFailedValidation() $customerId = 1; $token = 'jd2vnq'; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPublicHash') ->willReturn($publicHash); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readCustomerId') ->willReturn($customerId); - $this->tokenManagement->expects(static::once()) + $this->tokenManagementMock->expects(static::once()) ->method('getByPublicHash') ->with($publicHash, $customerId) - ->willReturn($this->paymentToken); + ->willReturn($this->paymentTokenMock); - $this->paymentToken->expects(static::once()) + $this->paymentTokenMock->expects(static::once()) ->method('getGatewayToken') ->willReturn($token); $obj = new \stdClass(); $obj->success = false; - $this->adapter->expects(static::once()) + $this->adapterMock->expects(static::once()) ->method('createNonce') ->with($token) ->willReturn($obj); - $this->responseValidator->expects(static::once()) + $this->responseValidatorMock->expects(static::once()) ->method('validate') ->with(['response' => ['object' => $obj]]) - ->willReturn($this->validationResult); + ->willReturn($this->validationResultMock); - $this->validationResult->expects(static::once()) + $this->validationResultMock->expects(static::once()) ->method('isValid') ->willReturn(false); - $this->validationResult->expects(static::once()) + $this->validationResultMock->expects(static::once()) ->method('getFailsDescription') ->willReturn(['Payment method nonce can\'t be retrieved.']); - $this->resultFactory->expects(static::never()) + $this->resultFactoryMock->expects(static::never()) ->method('create'); $this->command->execute(['publicHash' => $publicHash, 'customerId' => $customerId]); @@ -244,20 +252,20 @@ public function testExecute() $token = 'jd2vnq'; $nonce = 's1dj23'; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPublicHash') ->willReturn($publicHash); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readCustomerId') ->willReturn($customerId); - $this->tokenManagement->expects(static::once()) + $this->tokenManagementMock->expects(static::once()) ->method('getByPublicHash') ->with($publicHash, $customerId) - ->willReturn($this->paymentToken); + ->willReturn($this->paymentTokenMock); - $this->paymentToken->expects(static::once()) + $this->paymentTokenMock->expects(static::once()) ->method('getGatewayToken') ->willReturn($token); @@ -265,21 +273,21 @@ public function testExecute() $obj->success = true; $obj->paymentMethodNonce = new \stdClass(); $obj->paymentMethodNonce->nonce = $nonce; - $this->adapter->expects(static::once()) + $this->adapterMock->expects(static::once()) ->method('createNonce') ->with($token) ->willReturn($obj); - $this->responseValidator->expects(static::once()) + $this->responseValidatorMock->expects(static::once()) ->method('validate') ->with(['response' => ['object' => $obj]]) - ->willReturn($this->validationResult); + ->willReturn($this->validationResultMock); - $this->validationResult->expects(static::once()) + $this->validationResultMock->expects(static::once()) ->method('isValid') ->willReturn(true); - $this->validationResult->expects(static::never()) + $this->validationResultMock->expects(self::never()) ->method('getFailsDescription'); $expected = $this->getMockBuilder(ArrayResult::class) @@ -289,12 +297,12 @@ public function testExecute() $expected->expects(static::once()) ->method('get') ->willReturn(['paymentMethodNonce' => $nonce]); - $this->resultFactory->expects(static::once()) + $this->resultFactoryMock->expects(static::once()) ->method('create') ->willReturn($expected); $actual = $this->command->execute(['publicHash' => $publicHash, 'customerId' => $customerId]); - static::assertEquals($expected, $actual); - static::assertEquals($nonce, $actual->get()['paymentMethodNonce']); + self::assertEquals($expected, $actual); + self::assertEquals($nonce, $actual->get()['paymentMethodNonce']); } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php index 793700ab1971f..031e53690451f 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Test\Unit\Gateway\Config; use Magento\Braintree\Gateway\Config\CanVoidHandler; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Payment\Model\InfoInterface; use Magento\Sales\Model\Order\Payment; @@ -39,7 +39,7 @@ public function testHandleNotOrderPayment() static::assertFalse($voidHandler->handle($subject)); } - public function testHandleSomeAmoutWasPaid() + public function testHandleSomeAmountWasPaid() { $paymentDO = $this->createMock(PaymentDataObjectInterface::class); $subject = [ diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php index 7b9d59a5bc482..36ea3aea465dd 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php @@ -142,7 +142,7 @@ public function testGetCcTypesMapper($value, $expected) static::assertEquals( $expected, - $this->model->getCctypesMapper() + $this->model->getCcTypesMapper() ); } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php deleted file mode 100644 index b2207563b8b0f..0000000000000 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Braintree\Test\Unit\Gateway\Helper; - -use Braintree\Transaction; -use InvalidArgumentException; -use Magento\Braintree\Gateway\Helper\SubjectReader; - -/** - * Class SubjectReaderTest - */ -class SubjectReaderTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var SubjectReader - */ - private $subjectReader; - - protected function setUp() - { - $this->subjectReader = new SubjectReader(); - } - - /** - * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readCustomerId - * @expectedException InvalidArgumentException - * @expectedExceptionMessage The "customerId" field does not exists - */ - public function testReadCustomerIdWithException() - { - $this->subjectReader->readCustomerId([]); - } - - /** - * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readCustomerId - */ - public function testReadCustomerId() - { - $customerId = 1; - static::assertEquals($customerId, $this->subjectReader->readCustomerId(['customer_id' => $customerId])); - } - - /** - * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPublicHash - * @expectedException InvalidArgumentException - * @expectedExceptionMessage The "public_hash" field does not exists - */ - public function testReadPublicHashWithException() - { - $this->subjectReader->readPublicHash([]); - } - - /** - * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPublicHash - */ - public function testReadPublicHash() - { - $hash = 'fj23djf2o1fd'; - static::assertEquals($hash, $this->subjectReader->readPublicHash(['public_hash' => $hash])); - } - - /** - * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPayPal - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Transaction has't paypal attribute - */ - public function testReadPayPalWithException() - { - $transaction = Transaction::factory([ - 'id' => 'u38rf8kg6vn' - ]); - $this->subjectReader->readPayPal($transaction); - } - - /** - * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPayPal - */ - public function testReadPayPal() - { - $paypal = [ - 'paymentId' => '3ek7dk7fn0vi1', - 'payerEmail' => 'payer@example.com' - ]; - $transaction = Transaction::factory([ - 'id' => '4yr95vb', - 'paypal' => $paypal - ]); - - static::assertEquals($paypal, $this->subjectReader->readPayPal($transaction)); - } -} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionRefundTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionRefundTest.php new file mode 100644 index 0000000000000..c871dc69a5370 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionRefundTest.php @@ -0,0 +1,155 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Braintree\Test\Unit\Gateway\Http\Client; + +use Magento\Braintree\Gateway\Http\Client\TransactionRefund; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Model\Method\Logger; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Log\LoggerInterface; +use Magento\Braintree\Gateway\Request\PaymentDataBuilder; + +/** + * Tests \Magento\Braintree\Gateway\Http\Client\TransactionRefund. + */ +class TransactionRefundTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var TransactionRefund + */ + private $client; + + /** + * @var Logger|MockObject + */ + private $loggerMock; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapterMock; + + /** + * @var string + */ + private $transactionId = 'px4kpev5'; + + /** + * @var string + */ + private $paymentAmount = '100.00'; + + /** + * @inheritdoc + */ + protected function setUp() + { + /** @var LoggerInterface|MockObject $criticalLoggerMock */ + $criticalLoggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapterMock = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($this->adapterMock); + + $this->client = new TransactionRefund($criticalLoggerMock, $this->loggerMock, $adapterFactoryMock); + } + + /** + * @return void + * + * @expectedException \Magento\Payment\Gateway\Http\ClientException + * @expectedExceptionMessage Test messages + */ + public function testPlaceRequestException() + { + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + [ + 'request' => $this->getTransferData(), + 'client' => TransactionRefund::class, + 'response' => [], + ] + ); + + $this->adapterMock->expects($this->once()) + ->method('refund') + ->with($this->transactionId, $this->paymentAmount) + ->willThrowException(new \Exception('Test messages')); + + /** @var TransferInterface|MockObject $transferObjectMock */ + $transferObjectMock = $this->getTransferObjectMock(); + + $this->client->placeRequest($transferObjectMock); + } + + /** + * @return void + */ + public function testPlaceRequestSuccess() + { + $response = new \stdClass; + $response->success = true; + $this->adapterMock->expects($this->once()) + ->method('refund') + ->with($this->transactionId, $this->paymentAmount) + ->willReturn($response); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + [ + 'request' => $this->getTransferData(), + 'client' => TransactionRefund::class, + 'response' => ['success' => 1], + ] + ); + + $actualResult = $this->client->placeRequest($this->getTransferObjectMock()); + + $this->assertTrue(is_object($actualResult['object'])); + $this->assertEquals(['object' => $response], $actualResult); + } + + /** + * Creates mock object for TransferInterface. + * + * @return TransferInterface|MockObject + */ + private function getTransferObjectMock() + { + $transferObjectMock = $this->createMock(TransferInterface::class); + $transferObjectMock->expects($this->once()) + ->method('getBody') + ->willReturn($this->getTransferData()); + + return $transferObjectMock; + } + + /** + * Creates stub request data. + * + * @return array + */ + private function getTransferData() + { + return [ + 'transaction_id' => $this->transactionId, + PaymentDataBuilder::AMOUNT => $this->paymentAmount, + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php index 0837ecaea7a13..1317deeddb7fe 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php @@ -7,12 +7,14 @@ use Magento\Braintree\Gateway\Http\Client\TransactionSale; use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Payment\Gateway\Http\TransferInterface; use Magento\Payment\Model\Method\Logger; +use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; /** - * Class TransactionSaleTest + * Tests \Magento\Braintree\Gateway\Http\Client\TransactionSale. */ class TransactionSaleTest extends \PHPUnit\Framework\TestCase { @@ -22,35 +24,41 @@ class TransactionSaleTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var Logger|\PHPUnit_Framework_MockObject_MockObject + * @var Logger|MockObject */ private $loggerMock; /** - * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var BraintreeAdapter|MockObject */ - private $adapter; + private $adapterMock; /** - * Set up - * - * @return void + * @inheritdoc */ protected function setUp() { + /** @var LoggerInterface|MockObject $criticalLoggerMock */ $criticalLoggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->loggerMock = $this->getMockBuilder(Logger::class) ->disableOriginalConstructor() ->getMock(); - $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + $this->adapterMock = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) ->disableOriginalConstructor() ->getMock(); + $adapterFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($this->adapterMock); - $this->model = new TransactionSale($criticalLoggerMock, $this->loggerMock, $this->adapter); + $this->model = new TransactionSale($criticalLoggerMock, $this->loggerMock, $adapterFactoryMock); } /** - * Run test placeRequest method (exception) + * Runs test placeRequest method (exception) * * @return void * @@ -69,11 +77,11 @@ public function testPlaceRequestException() ] ); - $this->adapter->expects($this->once()) + $this->adapterMock->expects($this->once()) ->method('sale') ->willThrowException(new \Exception('Test messages')); - /** @var TransferInterface|\PHPUnit_Framework_MockObject_MockObject $transferObjectMock */ + /** @var TransferInterface|MockObject $transferObjectMock */ $transferObjectMock = $this->getTransferObjectMock(); $this->model->placeRequest($transferObjectMock); @@ -87,7 +95,7 @@ public function testPlaceRequestException() public function testPlaceRequestSuccess() { $response = $this->getResponseObject(); - $this->adapter->expects($this->once()) + $this->adapterMock->expects($this->once()) ->method('sale') ->with($this->getTransferData()) ->willReturn($response); @@ -109,7 +117,9 @@ public function testPlaceRequestSuccess() } /** - * @return TransferInterface|\PHPUnit_Framework_MockObject_MockObject + * Creates mock object for TransferInterface. + * + * @return TransferInterface|MockObject */ private function getTransferObjectMock() { @@ -122,6 +132,8 @@ private function getTransferObjectMock() } /** + * Creates stub for a response. + * * @return \stdClass */ private function getResponseObject() @@ -133,6 +145,8 @@ private function getResponseObject() } /** + * Creates stub request data. + * * @return array */ private function getTransferData() diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php index 86113c34ba218..2e77824817942 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php @@ -8,12 +8,14 @@ use Braintree\Result\Successful; use Magento\Braintree\Gateway\Http\Client\TransactionSubmitForSettlement; use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Payment\Gateway\Http\TransferInterface; use Magento\Payment\Model\Method\Logger; +use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; /** - * Class TransactionSubmitForSettlementTest + * Tests \Magento\Braintree\Gateway\Http\Client\TransactionSubmitForSettlement. */ class TransactionSubmitForSettlementTest extends \PHPUnit\Framework\TestCase { @@ -23,31 +25,39 @@ class TransactionSubmitForSettlementTest extends \PHPUnit\Framework\TestCase private $client; /** - * @var Logger|\PHPUnit_Framework_MockObject_MockObject + * @var Logger|MockObject */ - private $logger; + private $loggerMock; /** - * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var BraintreeAdapter|MockObject */ - private $adapter; + private $adapterMock; protected function setUp() { + /** @var LoggerInterface|MockObject $criticalLoggerMock */ $criticalLoggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $this->logger = $this->getMockBuilder(Logger::class) + $this->loggerMock = $this->getMockBuilder(Logger::class) ->disableOriginalConstructor() ->setMethods(['debug']) ->getMock(); - $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + + $this->adapterMock = $this->getMockBuilder(BraintreeAdapter::class) ->disableOriginalConstructor() ->setMethods(['submitForSettlement']) ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactoryMock->method('create') + ->willReturn($this->adapterMock); $this->client = new TransactionSubmitForSettlement( $criticalLoggerMock, - $this->logger, - $this->adapter + $this->loggerMock, + $adapterFactoryMock ); } @@ -59,13 +69,13 @@ protected function setUp() public function testPlaceRequestWithException() { $exception = new \Exception('Transaction has been declined'); - $this->adapter->expects(static::once()) + $this->adapterMock->expects(static::once()) ->method('submitForSettlement') ->willThrowException($exception); - /** @var TransferInterface|\PHPUnit_Framework_MockObject_MockObject $transferObjectMock */ - $transferObjectMock = $this->getTransferObjectMock(); - $this->client->placeRequest($transferObjectMock); + /** @var TransferInterface|MockObject $transferObject */ + $transferObject = $this->getTransferObjectMock(); + $this->client->placeRequest($transferObject); } /** @@ -74,19 +84,21 @@ public function testPlaceRequestWithException() public function testPlaceRequest() { $data = new Successful(['success'], [true]); - $this->adapter->expects(static::once()) + $this->adapterMock->expects(static::once()) ->method('submitForSettlement') ->willReturn($data); - /** @var TransferInterface|\PHPUnit_Framework_MockObject_MockObject $transferObjectMock */ - $transferObjectMock = $this->getTransferObjectMock(); - $response = $this->client->placeRequest($transferObjectMock); + /** @var TransferInterface|MockObject $transferObject */ + $transferObject = $this->getTransferObjectMock(); + $response = $this->client->placeRequest($transferObject); static::assertTrue(is_object($response['object'])); static::assertEquals(['object' => $data], $response); } /** - * @return TransferInterface|\PHPUnit_Framework_MockObject_MockObject + * Creates mock for TransferInterface + * + * @return TransferInterface|MockObject */ private function getTransferObjectMock() { @@ -95,7 +107,7 @@ private function getTransferObjectMock() ->method('getBody') ->willReturn([ 'transaction_id' => 'vb4c6b', - 'amount' => 124.00 + 'amount' => 124.00, ]); return $mock; diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionVoidTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionVoidTest.php new file mode 100644 index 0000000000000..17f63d0659b93 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionVoidTest.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Braintree\Test\Unit\Gateway\Http\Client; + +use Magento\Braintree\Gateway\Http\Client\TransactionVoid; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Model\Method\Logger; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Log\LoggerInterface; + +/** + * Tests \Magento\Braintree\Gateway\Http\Client\TransactionVoid. + */ +class TransactionVoidTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var TransactionVoid + */ + private $client; + + /** + * @var Logger|MockObject + */ + private $loggerMock; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapterMock; + + /** + * @var string + */ + private $transactionId = 'px4kpev5'; + + /** + * @inheritdoc + */ + protected function setUp() + { + /** @var LoggerInterface|MockObject $criticalLoggerMock */ + $criticalLoggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapterMock = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($this->adapterMock); + + $this->client = new TransactionVoid($criticalLoggerMock, $this->loggerMock, $adapterFactoryMock); + } + + /** + * @return void + * + * @expectedException \Magento\Payment\Gateway\Http\ClientException + * @expectedExceptionMessage Test messages + */ + public function testPlaceRequestException() + { + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + [ + 'request' => $this->getTransferData(), + 'client' => TransactionVoid::class, + 'response' => [], + ] + ); + + $this->adapterMock->expects($this->once()) + ->method('void') + ->with($this->transactionId) + ->willThrowException(new \Exception('Test messages')); + + /** @var TransferInterface|MockObject $transferObjectMock */ + $transferObjectMock = $this->getTransferObjectMock(); + + $this->client->placeRequest($transferObjectMock); + } + + /** + * @return void + */ + public function testPlaceRequestSuccess() + { + $response = new \stdClass; + $response->success = true; + $this->adapterMock->expects($this->once()) + ->method('void') + ->with($this->transactionId) + ->willReturn($response); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + [ + 'request' => $this->getTransferData(), + 'client' => TransactionVoid::class, + 'response' => ['success' => 1], + ] + ); + + $actualResult = $this->client->placeRequest($this->getTransferObjectMock()); + + $this->assertTrue(is_object($actualResult['object'])); + $this->assertEquals(['object' => $response], $actualResult); + } + + /** + * Creates mock object for TransferInterface. + * + * @return TransferInterface|MockObject + */ + private function getTransferObjectMock() + { + $transferObjectMock = $this->createMock(TransferInterface::class); + $transferObjectMock->expects($this->once()) + ->method('getBody') + ->willReturn($this->getTransferData()); + + return $transferObjectMock; + } + + /** + * Creates stub request data. + * + * @return array + */ + private function getTransferData() + { + return [ + 'transaction_id' => $this->transactionId, + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php index 3f05aed45da60..e1bbf29c63645 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php @@ -9,20 +9,21 @@ use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Payment\Gateway\Data\AddressAdapterInterface; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class AddressDataBuilderTest + * Tests \Magento\Braintree\Gateway\Request\AddressDataBuilder. */ class AddressDataBuilderTest extends \PHPUnit\Framework\TestCase { /** - * @var PaymentDataObjectInterface|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentDataObjectInterface|MockObject */ private $paymentDOMock; /** - * @var OrderAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @var OrderAdapterInterface|MockObject */ private $orderMock; @@ -32,7 +33,7 @@ class AddressDataBuilderTest extends \PHPUnit\Framework\TestCase private $builder; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ private $subjectReaderMock; @@ -138,7 +139,7 @@ public function dataProviderBuild() 'city' => 'Chicago', 'region_code' => 'IL', 'country_id' => 'US', - 'post_code' => '00000' + 'post_code' => '00000', ], [ AddressDataBuilder::SHIPPING_ADDRESS => [ @@ -150,7 +151,7 @@ public function dataProviderBuild() AddressDataBuilder::LOCALITY => 'Chicago', AddressDataBuilder::REGION => 'IL', AddressDataBuilder::POSTAL_CODE => '00000', - AddressDataBuilder::COUNTRY_CODE => 'US' + AddressDataBuilder::COUNTRY_CODE => 'US', ], AddressDataBuilder::BILLING_ADDRESS => [ @@ -162,46 +163,46 @@ public function dataProviderBuild() AddressDataBuilder::LOCALITY => 'Chicago', AddressDataBuilder::REGION => 'IL', AddressDataBuilder::POSTAL_CODE => '00000', - AddressDataBuilder::COUNTRY_CODE => 'US' - ] - ] - ] + AddressDataBuilder::COUNTRY_CODE => 'US', + ], + ], + ], ]; } /** * @param array $addressData - * @return AddressAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @return AddressAdapterInterface|MockObject */ private function getAddressMock($addressData) { $addressMock = $this->createMock(AddressAdapterInterface::class); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getFirstname') ->willReturn($addressData['first_name']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getLastname') ->willReturn($addressData['last_name']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getCompany') ->willReturn($addressData['company']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getStreetLine1') ->willReturn($addressData['street_1']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getStreetLine2') ->willReturn($addressData['street_2']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getCity') ->willReturn($addressData['city']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getRegionCode') ->willReturn($addressData['region_code']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getPostcode') ->willReturn($addressData['post_code']); - $addressMock->expects(static::exactly(2)) + $addressMock->expects(self::exactly(2)) ->method('getCountryId') ->willReturn($addressData['country_id']); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php index 9799b6f18c639..84558be0dab0f 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php @@ -8,37 +8,38 @@ use Magento\Braintree\Gateway\Request\CaptureDataBuilder; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Model\Order\Payment; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class CaptureDataBuilderTest + * Tests \Magento\Braintree\Gateway\Request\CaptureDataBuilder. */ class CaptureDataBuilderTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Braintree\Gateway\Request\CaptureDataBuilder + * @var CaptureDataBuilder */ private $builder; /** - * @var Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Payment|MockObject */ - private $payment; + private $paymentMock; /** - * @var \Magento\Sales\Model\Order\Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Payment|MockObject */ - private $paymentDO; + private $paymentDOMock; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ private $subjectReaderMock; protected function setUp() { - $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class); - $this->payment = $this->getMockBuilder(Payment::class) + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->getMock(); $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) @@ -57,22 +58,22 @@ public function testBuildWithException() { $amount = 10.00; $buildSubject = [ - 'payment' => $this->paymentDO, - 'amount' => $amount + 'payment' => $this->paymentDOMock, + 'amount' => $amount, ]; - $this->payment->expects(static::once()) + $this->paymentMock->expects(self::once()) ->method('getCcTransId') ->willReturn(''); - $this->paymentDO->expects(static::once()) + $this->paymentDOMock->expects(self::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDO); + ->willReturn($this->paymentDOMock); $this->builder->build($buildSubject); } @@ -87,26 +88,26 @@ public function testBuild() $expected = [ 'transaction_id' => $transactionId, - 'amount' => $amount + 'amount' => $amount, ]; $buildSubject = [ - 'payment' => $this->paymentDO, - 'amount' => $amount + 'payment' => $this->paymentDOMock, + 'amount' => $amount, ]; - $this->payment->expects(static::once()) + $this->paymentMock->expects(self::once()) ->method('getCcTransId') ->willReturn($transactionId); - $this->paymentDO->expects(static::once()) + $this->paymentDOMock->expects(self::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDO); + ->willReturn($this->paymentDOMock); $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php index 0f25b26fd2fa3..b19715cf92010 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php @@ -9,20 +9,21 @@ use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Payment\Gateway\Data\AddressAdapterInterface; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class CustomerDataBuilderTest + * Tests \Magento\Braintree\Gateway\Request\CustomerDataBuilder. */ class CustomerDataBuilderTest extends \PHPUnit\Framework\TestCase { /** - * @var PaymentDataObjectInterface|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentDataObjectInterface|MockObject */ private $paymentDOMock; /** - * @var OrderAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @var OrderAdapterInterface|MockObject */ private $orderMock; @@ -32,7 +33,7 @@ class CustomerDataBuilderTest extends \PHPUnit\Framework\TestCase private $builder; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ private $subjectReaderMock; @@ -105,7 +106,7 @@ public function dataProviderBuild() 'last_name' => 'Smith', 'company' => 'Magento', 'phone' => '555-555-555', - 'email' => 'john@magento.com' + 'email' => 'john@magento.com', ], [ CustomerDataBuilder::CUSTOMER => [ @@ -114,15 +115,15 @@ public function dataProviderBuild() CustomerDataBuilder::COMPANY => 'Magento', CustomerDataBuilder::PHONE => '555-555-555', CustomerDataBuilder::EMAIL => 'john@magento.com', - ] - ] - ] + ], + ], + ], ]; } /** * @param array $billingData - * @return AddressAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @return AddressAdapterInterface|MockObject */ private function getBillingMock($billingData) { diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php index 761d88b636ed7..1a87e5254bc50 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php @@ -7,6 +7,9 @@ use Magento\Braintree\Gateway\Config\Config; use Magento\Braintree\Gateway\Request\DescriptorDataBuilder; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -14,10 +17,15 @@ */ class DescriptorDataBuilderTest extends \PHPUnit\Framework\TestCase { + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + /** * @var Config|MockObject */ - private $config; + private $configMock; /** * @var DescriptorDataBuilder @@ -26,27 +34,41 @@ class DescriptorDataBuilderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->config = $this->getMockBuilder(Config::class) + $this->configMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->setMethods(['getDynamicDescriptors']) ->getMock(); + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) + ->disableOriginalConstructor() + ->getMock(); - $this->builder = new DescriptorDataBuilder($this->config); + $this->builder = new DescriptorDataBuilder($this->configMock, $this->subjectReaderMock); } /** - * @covers \Magento\Braintree\Gateway\Request\DescriptorDataBuilder::build * @param array $descriptors * @param array $expected * @dataProvider buildDataProvider */ public function testBuild(array $descriptors, array $expected) { - $this->config->expects(static::once()) - ->method('getDynamicDescriptors') - ->willReturn($descriptors); + $paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $buildSubject = [ + 'payment' => $paymentDOMock, + ]; + $this->subjectReaderMock->expects(self::once()) + ->method('readPayment') + ->with($buildSubject) + ->willReturn($paymentDOMock); + + $order = $this->createMock(OrderAdapterInterface::class); + $order->expects(self::once())->method('getStoreId')->willReturn('1'); + + $paymentDOMock->expects(self::once())->method('getOrder')->willReturn($order); + + $this->configMock->method('getDynamicDescriptors')->willReturn($descriptors); - $actual = $this->builder->build([]); + $actual = $this->builder->build(['payment' => $paymentDOMock]); static::assertEquals($expected, $actual); } @@ -64,42 +86,42 @@ public function buildDataProvider() 'descriptors' => [ 'name' => $name, 'phone' => $phone, - 'url' => $url + 'url' => $url, ], 'expected' => [ 'descriptor' => [ 'name' => $name, 'phone' => $phone, - 'url' => $url - ] - ] + 'url' => $url, + ], + ], ], [ 'descriptors' => [ 'name' => $name, - 'phone' => $phone + 'phone' => $phone, ], 'expected' => [ 'descriptor' => [ 'name' => $name, - 'phone' => $phone - ] - ] + 'phone' => $phone, + ], + ], ], [ 'descriptors' => [ - 'name' => $name + 'name' => $name, ], 'expected' => [ 'descriptor' => [ - 'name' => $name - ] - ] + 'name' => $name, + ], + ], ], [ 'descriptors' => [], - 'expected' => [] - ] + 'expected' => [], + ], ]; } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php index ee0907a1ddbbb..6a4aeacba4faf 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php @@ -5,17 +5,17 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Request; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Sales\Model\Order\Payment; use Magento\Braintree\Gateway\Config\Config; use Magento\Braintree\Observer\DataAssignObserver; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Braintree\Gateway\Request\KountPaymentDataBuilder; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class KountPaymentDataBuilderTest - * - * @see \Magento\Braintree\Gateway\Request\KountPaymentDataBuilder + * Tests \Magento\Braintree\Gateway\Request\KountPaymentDataBuilder. */ class KountPaymentDataBuilderTest extends \PHPUnit\Framework\TestCase { @@ -27,19 +27,19 @@ class KountPaymentDataBuilderTest extends \PHPUnit\Framework\TestCase private $builder; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ private $configMock; /** - * @var Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Payment|MockObject */ private $paymentMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var PaymentDataObjectInterface|MockObject */ - private $paymentDO; + private $paymentDOMock; /** * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject @@ -48,7 +48,7 @@ class KountPaymentDataBuilderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); $this->configMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); @@ -69,7 +69,7 @@ public function testBuildReadPaymentException() { $buildSubject = []; - $this->configMock->expects(static::once()) + $this->configMock->expects(self::never()) ->method('hasFraudProtection') ->willReturn(true); @@ -84,31 +84,34 @@ public function testBuildReadPaymentException() public function testBuild() { $additionalData = [ - DataAssignObserver::DEVICE_DATA => self::DEVICE_DATA + DataAssignObserver::DEVICE_DATA => self::DEVICE_DATA, ]; $expectedResult = [ KountPaymentDataBuilder::DEVICE_DATA => self::DEVICE_DATA, ]; - $buildSubject = ['payment' => $this->paymentDO]; + $order = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->expects(self::once())->method('getOrder')->willReturn($order); - $this->paymentMock->expects(static::exactly(count($additionalData))) + $buildSubject = ['payment' => $this->paymentDOMock]; + + $this->paymentMock->expects(self::exactly(count($additionalData))) ->method('getAdditionalInformation') ->willReturn($additionalData); - $this->configMock->expects(static::once()) + $this->configMock->expects(self::once()) ->method('hasFraudProtection') ->willReturn(true); - $this->paymentDO->expects(static::once()) + $this->paymentDOMock->expects(self::once()) ->method('getPayment') ->willReturn($this->paymentMock); $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDO); + ->willReturn($this->paymentDOMock); static::assertEquals( $expectedResult, diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php index fba65354d6095..c618ab66b95bc 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php @@ -5,31 +5,31 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Request\PayPal; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Request\PayPal\DeviceDataBuilder; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Payment\Model\InfoInterface; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class DeviceDataBuilderTest + * Tests \Magento\Braintree\Gateway\Request\PayPal\DeviceDataBuilder. */ class DeviceDataBuilderTest extends \PHPUnit\Framework\TestCase { /** * @var SubjectReader|MockObject */ - private $subjectReader; + private $subjectReaderMock; /** * @var PaymentDataObjectInterface|MockObject */ - private $paymentDataObject; + private $paymentDataObjectMock; /** * @var InfoInterface|MockObject */ - private $paymentInfo; + private $paymentInfoMock; /** * @var DeviceDataBuilder @@ -38,16 +38,16 @@ class DeviceDataBuilderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->setMethods(['readPayment']) ->getMock(); - $this->paymentDataObject = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentDataObjectMock = $this->createMock(PaymentDataObjectInterface::class); - $this->paymentInfo = $this->createMock(InfoInterface::class); + $this->paymentInfoMock = $this->createMock(InfoInterface::class); - $this->builder = new DeviceDataBuilder($this->subjectReader); + $this->builder = new DeviceDataBuilder($this->subjectReaderMock); } /** @@ -59,19 +59,19 @@ protected function setUp() public function testBuild(array $paymentData, array $expected) { $subject = [ - 'payment' => $this->paymentDataObject + 'payment' => $this->paymentDataObjectMock, ]; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPayment') ->with($subject) - ->willReturn($this->paymentDataObject); + ->willReturn($this->paymentDataObjectMock); - $this->paymentDataObject->expects(static::once()) + $this->paymentDataObjectMock->expects(static::once()) ->method('getPayment') - ->willReturn($this->paymentInfo); + ->willReturn($this->paymentInfoMock); - $this->paymentInfo->expects(static::once()) + $this->paymentInfoMock->expects(static::once()) ->method('getAdditionalInformation') ->willReturn($paymentData); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php index 8e83254727bf7..5595d5172b194 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Request\PayPal; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Request\PayPal\VaultDataBuilder; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Payment\Model\InfoInterface; @@ -13,24 +13,24 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class VaultDataBuilderTest + * Tests \Magento\Braintree\Gateway\Request\PayPal\VaultDataBuilder. */ class VaultDataBuilderTest extends \PHPUnit\Framework\TestCase { /** * @var SubjectReader|MockObject */ - private $subjectReader; + private $subjectReaderMock; /** * @var PaymentDataObjectInterface|MockObject */ - private $paymentDataObject; + private $paymentDataObjectMock; /** * @var InfoInterface|MockObject */ - private $paymentInfo; + private $paymentInfoMock; /** * @var VaultDataBuilder @@ -39,16 +39,16 @@ class VaultDataBuilderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->paymentDataObject = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentDataObjectMock = $this->createMock(PaymentDataObjectInterface::class); - $this->paymentInfo = $this->createMock(InfoInterface::class); + $this->paymentInfoMock = $this->createMock(InfoInterface::class); - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->setMethods(['readPayment']) ->getMock(); - $this->builder = new VaultDataBuilder($this->subjectReader); + $this->builder = new VaultDataBuilder($this->subjectReaderMock); } /** @@ -60,19 +60,19 @@ protected function setUp() public function testBuild(array $additionalInfo, array $expected) { $subject = [ - 'payment' => $this->paymentDataObject + 'payment' => $this->paymentDataObjectMock, ]; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPayment') ->with($subject) - ->willReturn($this->paymentDataObject); + ->willReturn($this->paymentDataObjectMock); - $this->paymentDataObject->expects(static::once()) + $this->paymentDataObjectMock->expects(static::once()) ->method('getPayment') - ->willReturn($this->paymentInfo); + ->willReturn($this->paymentInfoMock); - $this->paymentInfo->expects(static::once()) + $this->paymentInfoMock->expects(static::once()) ->method('getAdditionalInformation') ->willReturn($additionalInfo); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php index 12c613b8f216b..5620e8ffa92b8 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php @@ -5,23 +5,21 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Request; -use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; use Magento\Braintree\Gateway\Request\PaymentDataBuilder; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Observer\DataAssignObserver; use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Model\Order\Payment; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Braintree\Gateway\Config\Config; /** - * Class PaymentDataBuilderTest - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Tests \Magento\Braintree\Gateway\Request\PaymentDataBuilder. */ class PaymentDataBuilderTest extends \PHPUnit\Framework\TestCase { const PAYMENT_METHOD_NONCE = 'nonce'; - const MERCHANT_ACCOUNT_ID = '245345'; /** * @var PaymentDataBuilder @@ -29,36 +27,31 @@ class PaymentDataBuilderTest extends \PHPUnit\Framework\TestCase private $builder; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject - */ - private $configMock; - - /** - * @var Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Payment|MockObject */ private $paymentMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var PaymentDataObjectInterface|MockObject */ - private $paymentDO; + private $paymentDOMock; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ private $subjectReaderMock; /** - * @var OrderAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @var OrderAdapterInterface|MockObject */ private $orderMock; + /** + * @inheritdoc + */ protected function setUp() { - $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class); - $this->configMock = $this->getMockBuilder(Config::class) - ->disableOriginalConstructor() - ->getMock(); + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->getMock(); @@ -67,13 +60,19 @@ protected function setUp() ->getMock(); $this->orderMock = $this->createMock(OrderAdapterInterface::class); - $this->builder = new PaymentDataBuilder($this->configMock, $this->subjectReaderMock); + /** @var Config $config */ + $config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->builder = new PaymentDataBuilder($config, $this->subjectReaderMock); } /** + * @return void * @expectedException \InvalidArgumentException */ - public function testBuildReadPaymentException() + public function testBuildReadPaymentException(): void { $buildSubject = []; @@ -86,19 +85,20 @@ public function testBuildReadPaymentException() } /** + * @return void * @expectedException \InvalidArgumentException */ - public function testBuildReadAmountException() + public function testBuildReadAmountException(): void { $buildSubject = [ - 'payment' => $this->paymentDO, - 'amount' => null + 'payment' => $this->paymentDOMock, + 'amount' => null, ]; $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDO); + ->willReturn($this->paymentDOMock); $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) @@ -107,57 +107,55 @@ public function testBuildReadAmountException() $this->builder->build($buildSubject); } - public function testBuild() + /** + * @return void + */ + public function testBuild(): void { $additionalData = [ [ DataAssignObserver::PAYMENT_METHOD_NONCE, - self::PAYMENT_METHOD_NONCE - ] + self::PAYMENT_METHOD_NONCE, + ], ]; $expectedResult = [ PaymentDataBuilder::AMOUNT => 10.00, PaymentDataBuilder::PAYMENT_METHOD_NONCE => self::PAYMENT_METHOD_NONCE, - PaymentDataBuilder::ORDER_ID => '000000101', - PaymentDataBuilder::MERCHANT_ACCOUNT_ID => self::MERCHANT_ACCOUNT_ID, + PaymentDataBuilder::ORDER_ID => '000000101' ]; $buildSubject = [ - 'payment' => $this->paymentDO, - 'amount' => 10.00 + 'payment' => $this->paymentDOMock, + 'amount' => 10.00, ]; - $this->paymentMock->expects(static::exactly(count($additionalData))) + $this->paymentMock->expects(self::exactly(count($additionalData))) ->method('getAdditionalInformation') ->willReturnMap($additionalData); - $this->configMock->expects(static::once()) - ->method('getMerchantAccountId') - ->willReturn(self::MERCHANT_ACCOUNT_ID); - - $this->paymentDO->expects(static::once()) + $this->paymentDOMock->expects(self::once()) ->method('getPayment') ->willReturn($this->paymentMock); - $this->paymentDO->expects(static::once()) + $this->paymentDOMock->expects(self::once()) ->method('getOrder') ->willReturn($this->orderMock); $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDO); + ->willReturn($this->paymentDOMock); $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) ->willReturn(10.00); - $this->orderMock->expects(static::once()) + $this->orderMock->expects(self::once()) ->method('getOrderIncrementId') ->willReturn('000000101'); - static::assertEquals( + self::assertEquals( $expectedResult, $this->builder->build($buildSubject) ); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php index 5aa383d095a1e..dffe293c5a32f 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php @@ -5,65 +5,78 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Request; -use Magento\Braintree\Gateway\Helper\SubjectReader; use Magento\Braintree\Gateway\Request\PaymentDataBuilder; use Magento\Braintree\Gateway\Request\RefundDataBuilder; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Api\Data\TransactionInterface; use Magento\Sales\Model\Order\Payment; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Tests \Magento\Braintree\Gateway\Request\RefundDataBuilder. + */ class RefundDataBuilderTest extends \PHPUnit\Framework\TestCase { /** - * @var SubjectReader | \PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var Payment|MockObject */ - private $subjectReader; + private $paymentModelMock; /** * @var RefundDataBuilder */ private $dataBuilder; + /** + * @var string + */ + private $transactionId = 'xsd7n'; + public function setUp() { - $this->subjectReader = $this->getMockBuilder( - SubjectReader::class - )->disableOriginalConstructor() + $this->paymentModelMock = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() ->getMock(); - $this->dataBuilder = new RefundDataBuilder($this->subjectReader); + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->dataBuilder = new RefundDataBuilder($this->subjectReaderMock); } public function testBuild() { - $paymentDO = $this->createMock(PaymentDataObjectInterface::class); - $paymentModel = $this->getMockBuilder( - Payment::class - )->disableOriginalConstructor() - ->getMock(); + $this->initPaymentDOMock(); + $buildSubject = ['payment' => $this->paymentDOMock, 'amount' => 12.358]; - $buildSubject = ['payment' => $paymentDO, 'amount' => 12.358]; - $transactionId = 'xsd7n'; - - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($paymentDO); - $paymentDO->expects(static::once()) - ->method('getPayment') - ->willReturn($paymentModel); - $paymentModel->expects(static::once()) + ->willReturn($this->paymentDOMock); + $this->paymentModelMock->expects(self::once()) ->method('getParentTransactionId') - ->willReturn($transactionId); - $this->subjectReader->expects(static::once()) + ->willReturn($this->transactionId); + $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) ->willReturn($buildSubject['amount']); static::assertEquals( [ - 'transaction_id' => $transactionId, - PaymentDataBuilder::AMOUNT => '12.36' + 'transaction_id' => $this->transactionId, + PaymentDataBuilder::AMOUNT => '12.36', ], $this->dataBuilder->build($buildSubject) ); @@ -71,34 +84,25 @@ public function testBuild() public function testBuildNullAmount() { - $paymentDO = $this->createMock(PaymentDataObjectInterface::class); - $paymentModel = $this->getMockBuilder( - Payment::class - )->disableOriginalConstructor() - ->getMock(); - - $buildSubject = ['payment' => $paymentDO]; - $transactionId = 'xsd7n'; + $this->initPaymentDOMock(); + $buildSubject = ['payment' => $this->paymentDOMock]; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($paymentDO); - $paymentDO->expects(static::once()) - ->method('getPayment') - ->willReturn($paymentModel); - $paymentModel->expects(static::once()) + ->willReturn($this->paymentDOMock); + $this->paymentModelMock->expects(self::once()) ->method('getParentTransactionId') - ->willReturn($transactionId); - $this->subjectReader->expects(static::once()) + ->willReturn($this->transactionId); + $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) ->willThrowException(new \InvalidArgumentException()); static::assertEquals( [ - 'transaction_id' => $transactionId, - PaymentDataBuilder::AMOUNT => null + 'transaction_id' => $this->transactionId, + PaymentDataBuilder::AMOUNT => null, ], $this->dataBuilder->build($buildSubject) ); @@ -106,37 +110,41 @@ public function testBuildNullAmount() public function testBuildCutOffLegacyTransactionIdPostfix() { - $paymentDO = $this->createMock(PaymentDataObjectInterface::class); - $paymentModel = $this->getMockBuilder( - Payment::class - )->disableOriginalConstructor() - ->getMock(); - - $buildSubject = ['payment' => $paymentDO]; + $this->initPaymentDOMock(); + $buildSubject = ['payment' => $this->paymentDOMock]; $legacyTxnId = 'xsd7n-' . TransactionInterface::TYPE_CAPTURE; - $transactionId = 'xsd7n'; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($paymentDO); - $paymentDO->expects(static::once()) - ->method('getPayment') - ->willReturn($paymentModel); - $paymentModel->expects(static::once()) + ->willReturn($this->paymentDOMock); + $this->paymentModelMock->expects(self::once()) ->method('getParentTransactionId') ->willReturn($legacyTxnId); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) ->willThrowException(new \InvalidArgumentException()); static::assertEquals( [ - 'transaction_id' => $transactionId, - PaymentDataBuilder::AMOUNT => null + 'transaction_id' => $this->transactionId, + PaymentDataBuilder::AMOUNT => null, ], $this->dataBuilder->build($buildSubject) ); } + + /** + * Creates mock object for PaymentDataObjectInterface + * + * @return PaymentDataObjectInterface|MockObject + */ + private function initPaymentDOMock() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentDOMock->expects(self::once()) + ->method('getPayment') + ->willReturn($this->paymentModelMock); + } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php index c28ac0c3ac372..f12d1365d0b34 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php @@ -6,14 +6,15 @@ namespace Magento\Braintree\Test\Unit\Gateway\Request; use Magento\Braintree\Gateway\Config\Config; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder; -use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; -use Magento\Payment\Gateway\Data\Order\OrderAdapter; use Magento\Payment\Gateway\Data\Order\AddressAdapter; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Data\Order\OrderAdapter; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class ThreeDSecureDataBuilderTest + * Tests \Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder. */ class ThreeDSecureDataBuilderTest extends \PHPUnit\Framework\TestCase { @@ -23,41 +24,49 @@ class ThreeDSecureDataBuilderTest extends \PHPUnit\Framework\TestCase private $builder; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ private $configMock; /** - * @var PaymentDataObjectInterface|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentDataObjectInterface|MockObject */ - private $paymentDO; + private $paymentDOMock; /** - * @var OrderAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var OrderAdapter|MockObject */ - private $order; + private $orderMock; /** - * @var \Magento\Payment\Gateway\Data\Order\AddressAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var AddressAdapter|MockObject */ - private $billingAddress; + private $billingAddressMock; /** * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject */ private $subjectReaderMock; + /** + * @var int + */ + private $storeId = 1; + + /** + * @inheritdoc + */ protected function setUp() { $this->initOrderMock(); - $this->paymentDO = $this->getMockBuilder(PaymentDataObjectInterface::class) + $this->paymentDOMock = $this->getMockBuilder(PaymentDataObjectInterface::class) ->disableOriginalConstructor() ->setMethods(['getOrder', 'getPayment']) ->getMock(); - $this->paymentDO->expects(static::once()) + $this->paymentDOMock->expects(static::once()) ->method('getOrder') - ->willReturn($this->order); + ->willReturn($this->orderMock); $this->configMock = $this->getMockBuilder(Config::class) ->setMethods(['isVerify3DSecure', 'getThresholdAmount', 'get3DSecureSpecificCountries']) @@ -82,41 +91,45 @@ protected function setUp() public function testBuild($verify, $thresholdAmount, $countryId, array $countries, array $expected) { $buildSubject = [ - 'payment' => $this->paymentDO, - 'amount' => 25 + 'payment' => $this->paymentDOMock, + 'amount' => 25, ]; $this->configMock->expects(static::once()) ->method('isVerify3DSecure') + ->with(self::equalTo($this->storeId)) ->willReturn($verify); $this->configMock->expects(static::any()) ->method('getThresholdAmount') + ->with(self::equalTo($this->storeId)) ->willReturn($thresholdAmount); $this->configMock->expects(static::any()) ->method('get3DSecureSpecificCountries') + ->with(self::equalTo($this->storeId)) ->willReturn($countries); - $this->billingAddress->expects(static::any()) + $this->billingAddressMock->expects(static::any()) ->method('getCountryId') ->willReturn($countryId); $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDO); + ->willReturn($this->paymentDOMock); $this->subjectReaderMock->expects(self::once()) ->method('readAmount') ->with($buildSubject) ->willReturn(25); $result = $this->builder->build($buildSubject); - static::assertEquals($expected, $result); + self::assertEquals($expected, $result); } /** - * Get list of variations for build test + * Gets list of variations to build request data. + * * @return array */ public function buildDataProvider() @@ -144,22 +157,26 @@ public function buildDataProvider() } /** - * Create mock object for order adapter + * Creates mock object for order adapter. + * + * @return void */ private function initOrderMock() { - $this->billingAddress = $this->getMockBuilder(AddressAdapter::class) + $this->billingAddressMock = $this->getMockBuilder(AddressAdapter::class) ->disableOriginalConstructor() ->setMethods(['getCountryId']) ->getMock(); - $this->order = $this->getMockBuilder(OrderAdapter::class) + $this->orderMock = $this->getMockBuilder(OrderAdapter::class) ->disableOriginalConstructor() - ->setMethods(['getBillingAddress']) + ->setMethods(['getBillingAddress', 'getStoreId']) ->getMock(); - $this->order->expects(static::any()) + $this->orderMock->expects(static::any()) ->method('getBillingAddress') - ->willReturn($this->billingAddress); + ->willReturn($this->billingAddressMock); + $this->orderMock->method('getStoreId') + ->willReturn($this->storeId); } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php index df11938ddba70..d4e1f2745e3f3 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php @@ -3,15 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Gateway\Request; -use Magento\Braintree\Gateway\Helper\SubjectReader; use Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Api\Data\OrderPaymentExtension; use Magento\Sales\Model\Order\Payment; use Magento\Vault\Model\PaymentToken; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Tests \Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder. + */ class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase { /** @@ -20,28 +26,30 @@ class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase private $builder; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var PaymentDataObjectInterface|MockObject */ private $paymentDO; /** - * @var Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Payment|MockObject */ private $payment; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ private $subjectReader; - public function setUp() + /** + * @inheritdoc + */ + protected function setUp(): void { $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class); $this->payment = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->getMock(); - $this->paymentDO->expects(static::once()) - ->method('getPayment') + $this->paymentDO->method('getPayment') ->willReturn($this->payment); $this->subjectReader = $this->getMockBuilder(SubjectReader::class) @@ -52,52 +60,84 @@ public function setUp() } /** - * \Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder::build + * Checks the result after builder execution. */ - public function testBuild() + public function testBuild(): void { $amount = 30.00; $token = '5tfm4c'; $buildSubject = [ 'payment' => $this->paymentDO, - 'amount' => $amount + 'amount' => $amount, ]; $expected = [ 'amount' => $amount, - 'paymentMethodToken' => $token + 'paymentMethodToken' => $token, ]; - $this->subjectReader->expects(self::once()) - ->method('readPayment') + $this->subjectReader->method('readPayment') ->with($buildSubject) ->willReturn($this->paymentDO); - $this->subjectReader->expects(self::once()) - ->method('readAmount') + $this->subjectReader->method('readAmount') ->with($buildSubject) ->willReturn($amount); + /** @var OrderPaymentExtension|MockObject $paymentExtension */ $paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) ->setMethods(['getVaultPaymentToken']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + /** @var PaymentToken|MockObject $paymentToken */ $paymentToken = $this->getMockBuilder(PaymentToken::class) ->disableOriginalConstructor() ->getMock(); - $paymentExtension->expects(static::once()) - ->method('getVaultPaymentToken') + $paymentExtension->method('getVaultPaymentToken') ->willReturn($paymentToken); - $this->payment->expects(static::once()) - ->method('getExtensionAttributes') + $this->payment->method('getExtensionAttributes') ->willReturn($paymentExtension); - $paymentToken->expects(static::once()) - ->method('getGatewayToken') + $paymentToken->method('getGatewayToken') ->willReturn($token); $result = $this->builder->build($buildSubject); - static::assertEquals($expected, $result); + self::assertEquals($expected, $result); + } + + /** + * Checks a builder execution if Payment Token doesn't exist. + * + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage The Payment Token is not available to perform the request. + */ + public function testBuildWithoutPaymentToken(): void + { + $amount = 30.00; + $buildSubject = [ + 'payment' => $this->paymentDO, + 'amount' => $amount, + ]; + + $this->subjectReader->method('readPayment') + ->with($buildSubject) + ->willReturn($this->paymentDO); + $this->subjectReader->method('readAmount') + ->with($buildSubject) + ->willReturn($amount); + + /** @var OrderPaymentExtension|MockObject $paymentExtension */ + $paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) + ->setMethods(['getVaultPaymentToken']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->payment->method('getExtensionAttributes') + ->willReturn($paymentExtension); + $paymentExtension->method('getVaultPaymentToken') + ->willReturn(null); + + $this->builder->build($buildSubject); } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VoidDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VoidDataBuilderTest.php new file mode 100644 index 0000000000000..88713885b5c7d --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VoidDataBuilderTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Braintree\Test\Unit\Gateway\Request; + +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Gateway\Request\VoidDataBuilder; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Tests \Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder. + */ +class VoidDataBuilderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var VoidDataBuilder + */ + private $builder; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + */ + private $subjectReaderMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->getMock(); + $this->paymentDOMock->expects(static::once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->builder = new VoidDataBuilder($this->subjectReaderMock); + } + + /** + * @param string|null $parentTransactionId + * @param string $callLastTransId + * @param string $lastTransId + * @param string $expected + * @return void + * @dataProvider buildDataProvider + */ + public function testBuild($parentTransactionId, $callLastTransId, $lastTransId, $expected) + { + $amount = 30.00; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'amount' => $amount, + ]; + + $this->subjectReaderMock->expects(self::once()) + ->method('readPayment') + ->with($buildSubject) + ->willReturn($this->paymentDOMock); + + $this->paymentMock->expects(self::once()) + ->method('getParentTransactionId') + ->willReturn($parentTransactionId); + $this->paymentMock->expects(self::$callLastTransId()) + ->method('getLastTransId') + ->willReturn($lastTransId); + + $result = $this->builder->build($buildSubject); + self::assertEquals( + ['transaction_id' => $expected], + $result + ); + } + + /** + * @return array + */ + public function buildDataProvider() + { + return [ + [ + 'parentTransactionId' => 'b3b99d', + 'callLastTransId' => 'never', + 'lastTransId' => 'd45d22', + 'expected' => 'b3b99d', + ], + [ + 'parentTransactionId' => null, + 'callLastTransId' => 'once', + 'expected' => 'd45d22', + 'lastTransId' => 'd45d22', + ], + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CancelDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CancelDetailsHandlerTest.php new file mode 100644 index 0000000000000..2fa3d2ea65836 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CancelDetailsHandlerTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Gateway\Response; + +use Magento\Braintree\Gateway\Response\CancelDetailsHandler; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Tests \Magento\Braintree\Gateway\Response\CancelDetailsHandler. + */ +class CancelDetailsHandlerTest extends TestCase +{ + /** + * @var CancelDetailsHandler + */ + private $handler; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->handler = new CancelDetailsHandler(new SubjectReader()); + } + + /** + * Checks a case when cancel handler closes the current and parent transactions. + * + * @return void + */ + public function testHandle(): void + { + /** @var OrderAdapterInterface|MockObject $order */ + $order = $this->getMockForAbstractClass(OrderAdapterInterface::class); + /** @var Payment|MockObject $payment */ + $payment = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->setMethods(['setOrder']) + ->getMock(); + + $paymentDO = new PaymentDataObject($order, $payment); + $response = [ + 'payment' => $paymentDO, + ]; + + $this->handler->handle($response, []); + + self::assertTrue($payment->getIsTransactionClosed(), 'The current transaction should be closed.'); + self::assertTrue($payment->getShouldCloseParentTransaction(), 'The parent transaction should be closed.'); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php index 87e8e4e413c1b..a70993e14e50c 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php @@ -5,16 +5,15 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Response; -use Braintree\Result\Successful; use Braintree\Transaction; use Magento\Braintree\Gateway\Response\CardDetailsHandler; use Magento\Payment\Gateway\Data\PaymentDataObject; use Magento\Sales\Model\Order\Payment; use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; /** - * Class CardDetailsHandlerTest + * Tests \Magento\Braintree\Gateway\Response\CardDetailsHandler. */ class CardDetailsHandlerTest extends \PHPUnit\Framework\TestCase { @@ -26,12 +25,12 @@ class CardDetailsHandlerTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Sales\Model\Order\Payment|\PHPUnit_Framework_MockObject_MockObject */ - private $payment; + private $paymentMock; /** * @var \Magento\Braintree\Gateway\Config\Config|\PHPUnit_Framework_MockObject_MockObject */ - private $config; + private $configMock; /** * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject @@ -45,7 +44,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->cardHandler = new CardDetailsHandler($this->config, $this->subjectReaderMock); + $this->cardHandler = new CardDetailsHandler($this->configMock, $this->subjectReaderMock); } /** @@ -53,30 +52,30 @@ protected function setUp() */ public function testHandle() { - $paymentData = $this->getPaymentDataObjectMock(); + $paymentDataMock = $this->getPaymentDataObjectMock(); $transaction = $this->getBraintreeTransaction(); - $subject = ['payment' => $paymentData]; + $subject = ['payment' => $paymentDataMock]; $response = ['object' => $transaction]; $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($subject) - ->willReturn($paymentData); + ->willReturn($paymentDataMock); $this->subjectReaderMock->expects(self::once()) ->method('readTransaction') ->with($response) ->willReturn($transaction); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('setCcLast4'); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('setCcExpMonth'); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('setCcExpYear'); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('setCcType'); - $this->payment->expects(static::exactly(2)) + $this->paymentMock->expects(static::exactly(2)) ->method('setAdditionalInformation'); $this->cardHandler->handle($subject, $response); @@ -87,12 +86,12 @@ public function testHandle() */ private function initConfigMock() { - $this->config = $this->getMockBuilder(Config::class) + $this->configMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->setMethods(['getCctypesMapper']) ->getMock(); - $this->config->expects(static::once()) + $this->configMock->expects(static::once()) ->method('getCctypesMapper') ->willReturn([ 'american-express' => 'AE', @@ -110,7 +109,7 @@ private function initConfigMock() */ private function getPaymentDataObjectMock() { - $this->payment = $this->getMockBuilder(Payment::class) + $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->setMethods([ 'setCcLast4', @@ -128,7 +127,7 @@ private function getPaymentDataObjectMock() $mock->expects(static::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); return $mock; } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php index fdf3dc941bd77..b3a7f8b9ee76a 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php @@ -5,9 +5,10 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Response\PayPal; +use Braintree\Result\Successful; use Braintree\Transaction; use Braintree\Transaction\PayPalDetails; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Response\PayPal\VaultDetailsHandler; use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -15,54 +16,54 @@ use Magento\Sales\Api\Data\OrderPaymentExtensionInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory; use Magento\Sales\Model\Order\Payment; +use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; use Magento\Vault\Api\Data\PaymentTokenInterface; -use Magento\Vault\Api\Data\PaymentTokenInterfaceFactory; -use Magento\Vault\Model\AccountPaymentTokenFactory; use Magento\Vault\Model\PaymentToken; +use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class VaultDetailsHandlerTest + * Tests \Magento\Braintree\Gateway\Response\PayPal\VaultDetailsHandler. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class VaultDetailsHandlerTest extends \PHPUnit\Framework\TestCase +class VaultDetailsHandlerTest extends TestCase { private static $transactionId = '1n2suy'; - /** - * @var SubjectReader|MockObject - */ - private $subjectReader; + private static $token = 'rc39al'; + + private static $payerEmail = 'john.doe@example.com'; /** * @var PaymentDataObjectInterface|MockObject */ - private $paymentDataObject; + private $paymentDataObjectMock; /** * @var Payment|MockObject */ - private $paymentInfo; + private $paymentInfoMock; /** - * @var AccountPaymentTokenFactory|MockObject + * @var PaymentTokenFactoryInterface|MockObject */ - private $paymentTokenFactory; + private $paymentTokenFactoryMock; /** * @var PaymentTokenInterface|MockObject */ - protected $paymentToken; + protected $paymentTokenMock; /** * @var OrderPaymentExtension|MockObject */ - private $paymentExtension; + private $paymentExtensionMock; /** * @var OrderPaymentExtensionInterfaceFactory|MockObject */ - private $paymentExtensionFactory; + private $paymentExtensionFactoryMock; /** * @var VaultDetailsHandler @@ -72,7 +73,7 @@ class VaultDetailsHandlerTest extends \PHPUnit\Framework\TestCase /** * @var DateTimeFactory|MockObject */ - private $dateTimeFactory; + private $dateTimeFactoryMock; /** * @var array @@ -83,146 +84,119 @@ protected function setUp() { $objectManager = new ObjectManager($this); - $this->paymentDataObject = $this->getMockForAbstractClass(PaymentDataObjectInterface::class); + $this->paymentDataObjectMock = $this->getMockForAbstractClass(PaymentDataObjectInterface::class); - $this->paymentInfo = $this->getMockBuilder(Payment::class) + $this->paymentInfoMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() - ->setMethods(['__wakeup']) + ->setMethods(['__wakeup', 'getExtensionAttributes']) ->getMock(); - $this->paymentToken = $objectManager->getObject(PaymentToken::class); + $this->paymentTokenMock = $objectManager->getObject(PaymentToken::class); - $this->paymentTokenFactory = $this->getMockBuilder(AccountPaymentTokenFactory::class) + $this->paymentTokenFactoryMock = $this->getMockBuilder(PaymentTokenFactoryInterface::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->paymentExtension = $this->getMockBuilder(OrderPaymentExtensionInterface::class) + $this->paymentExtensionMock = $this->getMockBuilder(OrderPaymentExtensionInterface::class) ->setMethods(['setVaultPaymentToken', 'getVaultPaymentToken']) ->disableOriginalConstructor() ->getMock(); - $this->paymentExtensionFactory = $this->getMockBuilder(OrderPaymentExtensionInterfaceFactory::class) + $this->paymentExtensionFactoryMock = $this->getMockBuilder(OrderPaymentExtensionInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); + $this->paymentInfoMock->expects(self::any()) + ->method('getExtensionAttributes') + ->willReturn($this->paymentExtensionMock); + $this->subject = [ - 'payment' => $this->paymentDataObject, + 'payment' => $this->paymentDataObjectMock, ]; - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) - ->disableOriginalConstructor() - ->setMethods(['readPayment', 'readTransaction']) - ->getMock(); - $this->subjectReader->expects(static::once()) - ->method('readPayment') - ->with($this->subject) - ->willReturn($this->paymentDataObject); - $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + $this->dateTimeFactoryMock = $this->getMockBuilder(DateTimeFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); $this->handler = new VaultDetailsHandler( - $this->paymentTokenFactory, - $this->paymentExtensionFactory, - $this->subjectReader, - $this->dateTimeFactory + $this->paymentTokenFactoryMock, + $this->paymentExtensionFactoryMock, + new SubjectReader(), + $this->dateTimeFactoryMock ); } - /** - * @covers \Magento\Braintree\Gateway\Response\PayPal\VaultDetailsHandler::handle - */ public function testHandle() { - /** @var Transaction $transaction */ $transaction = $this->getTransaction(); $response = [ 'object' => $transaction ]; - $this->paymentExtension->expects(static::once()) - ->method('setVaultPaymentToken') - ->with($this->paymentToken); - $this->paymentExtension->expects(static::once()) - ->method('getVaultPaymentToken') - ->willReturn($this->paymentToken); - - $this->subjectReader->expects(static::once()) - ->method('readTransaction') - ->with($response) - ->willReturn($transaction); + $this->paymentExtensionMock->method('setVaultPaymentToken') + ->with($this->paymentTokenMock); + $this->paymentExtensionMock->method('getVaultPaymentToken') + ->willReturn($this->paymentTokenMock); - $this->paymentDataObject->expects(static::once()) - ->method('getPayment') - ->willReturn($this->paymentInfo); + $this->paymentDataObjectMock->method('getPayment') + ->willReturn($this->paymentInfoMock); - $this->paymentTokenFactory->expects(static::once()) - ->method('create') - ->willReturn($this->paymentToken); + $this->paymentTokenFactoryMock->method('create') + ->with(PaymentTokenFactoryInterface::TOKEN_TYPE_ACCOUNT) + ->willReturn($this->paymentTokenMock); - $this->paymentExtensionFactory->expects(static::once()) - ->method('create') - ->willReturn($this->paymentExtension); + $this->paymentExtensionFactoryMock->method('create') + ->willReturn($this->paymentExtensionMock); $dateTime = new \DateTime('2016-07-05 00:00:00', new \DateTimeZone('UTC')); $expirationDate = '2017-07-05 00:00:00'; - $this->dateTimeFactory->expects(static::once()) - ->method('create') + $this->dateTimeFactoryMock->method('create') ->willReturn($dateTime); $this->handler->handle($this->subject, $response); - $extensionAttributes = $this->paymentInfo->getExtensionAttributes(); - /** @var PaymentTokenInterface $paymentToken */ + $extensionAttributes = $this->paymentInfoMock->getExtensionAttributes(); $paymentToken = $extensionAttributes->getVaultPaymentToken(); - static::assertNotNull($paymentToken); + self::assertNotNull($paymentToken); $tokenDetails = json_decode($paymentToken->getTokenDetails(), true); - static::assertSame($this->paymentToken, $paymentToken); - static::assertEquals($transaction->paypalDetails->token, $paymentToken->getGatewayToken()); - static::assertEquals($transaction->paypalDetails->payerEmail, $tokenDetails['payerEmail']); - static::assertEquals($expirationDate, $paymentToken->getExpiresAt()); + self::assertSame($this->paymentTokenMock, $paymentToken); + self::assertEquals(self::$token, $paymentToken->getGatewayToken()); + self::assertEquals(self::$payerEmail, $tokenDetails['payerEmail']); + self::assertEquals($expirationDate, $paymentToken->getExpiresAt()); } - /** - * @covers \Magento\Braintree\Gateway\Response\PayPal\VaultDetailsHandler::handle - */ public function testHandleWithoutToken() { $transaction = $this->getTransaction(); - $transaction->paypalDetails->token = null; + $transaction->transaction->paypalDetails->token = null; $response = [ 'object' => $transaction ]; - $this->subjectReader->expects(static::once()) - ->method('readTransaction') - ->with($response) - ->willReturn($transaction); - - $this->paymentDataObject->expects(static::once()) - ->method('getPayment') - ->willReturn($this->paymentInfo); + $this->paymentDataObjectMock->method('getPayment') + ->willReturn($this->paymentInfoMock); - $this->paymentTokenFactory->expects(static::never()) + $this->paymentTokenFactoryMock->expects(self::never()) ->method('create'); - $this->dateTimeFactory->expects(static::never()) + $this->dateTimeFactoryMock->expects(self::never()) ->method('create'); $this->handler->handle($this->subject, $response); - static::assertNull($this->paymentInfo->getExtensionAttributes()); + self::assertNotNull($this->paymentInfoMock->getExtensionAttributes()); } /** - * Create Braintree transaction - * @return Transaction + * Creates Braintree transaction. + * + * @return Successful */ - private function getTransaction() + private function getTransaction(): Successful { $attributes = [ 'id' => self::$transactionId, @@ -230,19 +204,21 @@ private function getTransaction() ]; $transaction = Transaction::factory($attributes); + $result = new Successful(['transaction' => $transaction]); - return $transaction; + return $result; } /** - * Get PayPal transaction details + * Gets PayPal transaction details. + * * @return PayPalDetails */ - private function getPayPalDetails() + private function getPayPalDetails(): PayPalDetails { $attributes = [ - 'token' => 'rc39al', - 'payerEmail' => 'john.doe@example.com' + 'token' => self::$token, + 'payerEmail' => self::$payerEmail ]; $details = new PayPalDetails($attributes); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php index f1420ee895e5b..1b2c8c6bb4ad1 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php @@ -8,9 +8,8 @@ use Braintree\Transaction; use Magento\Braintree\Gateway\Response\PayPalDetailsHandler; use Magento\Payment\Gateway\Data\PaymentDataObject; -use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -26,26 +25,26 @@ class PayPalDetailsHandlerTest extends \PHPUnit\Framework\TestCase /** * @var Payment|MockObject */ - private $payment; + private $paymentMock; /** * @var SubjectReader|MockObject */ - private $subjectReader; + private $subjectReaderMock; protected function setUp() { - $this->payment = $this->getMockBuilder(Payment::class) + $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->setMethods([ 'setAdditionalInformation', ]) ->getMock(); - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->getMock(); - $this->payPalHandler = new PayPalDetailsHandler($this->subjectReader); + $this->payPalHandler = new PayPalDetailsHandler($this->subjectReaderMock); } /** @@ -53,26 +52,26 @@ protected function setUp() */ public function testHandle() { - $paymentData = $this->getPaymentDataObjectMock(); + $paymentDataMock = $this->getPaymentDataObjectMock(); $transaction = $this->getBraintreeTransaction(); - $subject = ['payment' => $paymentData]; + $subject = ['payment' => $paymentDataMock]; $response = ['object' => $transaction]; - $this->subjectReader->expects(self::once()) + $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($subject) - ->willReturn($paymentData); - $this->subjectReader->expects(self::once()) + ->willReturn($paymentDataMock); + $this->subjectReaderMock->expects(self::once()) ->method('readTransaction') ->with($response) ->willReturn($transaction); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPayPal') ->with($transaction) ->willReturn($transaction->paypal); - $this->payment->expects(static::exactly(2)) + $this->paymentMock->expects(static::exactly(2)) ->method('setAdditionalInformation'); $this->payPalHandler->handle($subject, $response); @@ -91,7 +90,7 @@ private function getPaymentDataObjectMock() $mock->expects(static::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); return $mock; } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php index d90caa84b447b..69beab38f001b 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php @@ -8,13 +8,12 @@ use Braintree\Transaction; use Magento\Braintree\Gateway\Response\PaymentDetailsHandler; use Magento\Payment\Gateway\Data\PaymentDataObject; -use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class PaymentDetailsHandlerTest + * Tests \\Magento\Braintree\Gateway\Response\PaymentDetailsHandler. */ class PaymentDetailsHandlerTest extends \PHPUnit\Framework\TestCase { @@ -28,35 +27,35 @@ class PaymentDetailsHandlerTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Sales\Model\Order\Payment|MockObject */ - private $payment; + private $paymentMock; /** * @var SubjectReader|MockObject */ - private $subjectReader; + private $subjectReaderMock; protected function setUp() { - $this->payment = $this->getMockBuilder(Payment::class) + $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->setMethods([ 'setCcTransId', 'setLastTransId', - 'setAdditionalInformation' + 'setAdditionalInformation', ]) ->getMock(); - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->getMock(); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('setCcTransId'); - $this->payment->expects(static::once()) + $this->paymentMock->expects(static::once()) ->method('setLastTransId'); - $this->payment->expects(static::any()) + $this->paymentMock->expects(static::any()) ->method('setAdditionalInformation'); - $this->paymentHandler = new PaymentDetailsHandler($this->subjectReader); + $this->paymentHandler = new PaymentDetailsHandler($this->subjectReaderMock); } /** @@ -64,17 +63,17 @@ protected function setUp() */ public function testHandle() { - $paymentData = $this->getPaymentDataObjectMock(); + $paymentDataMock = $this->getPaymentDataObjectMock(); $transaction = $this->getBraintreeTransaction(); - $subject = ['payment' => $paymentData]; + $subject = ['payment' => $paymentDataMock]; $response = ['object' => $transaction]; - $this->subjectReader->expects(self::once()) + $this->subjectReaderMock->expects(self::once()) ->method('readPayment') ->with($subject) - ->willReturn($paymentData); - $this->subjectReader->expects(self::once()) + ->willReturn($paymentDataMock); + $this->subjectReaderMock->expects(self::once()) ->method('readTransaction') ->with($response) ->willReturn($transaction); @@ -95,7 +94,7 @@ private function getPaymentDataObjectMock() $mock->expects(static::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); return $mock; } @@ -113,7 +112,7 @@ private function getBraintreeTransaction() 'cvvResponseCode' => 'M', 'processorAuthorizationCode' => 'W1V8XK', 'processorResponseCode' => '1000', - 'processorResponseText' => 'Approved' + 'processorResponseText' => 'Approved', ]; return Transaction::factory($attributes); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php index 2365c396c2f4a..b86952ebf07a5 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php @@ -6,7 +6,7 @@ namespace Magento\Braintree\Test\Unit\Gateway\Response; use Braintree\Transaction; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Response\RiskDataHandler; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Model\Order\Payment; @@ -27,19 +27,19 @@ class RiskDataHandlerTest extends \PHPUnit\Framework\TestCase /** * @var SubjectReader|MockObject */ - private $subjectReader; + private $subjectReaderMock; /** - * Set up + * @inheritdoc */ protected function setUp() { - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->setMethods(['readPayment', 'readTransaction']) ->getMock(); - $this->riskDataHandler = new RiskDataHandler($this->subjectReader); + $this->riskDataHandler = new RiskDataHandler($this->subjectReaderMock); } /** @@ -76,11 +76,11 @@ public function testHandle($riskDecision, $isFraud) 'payment' => $paymentDO, ]; - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readPayment') ->with($handlingSubject) ->willReturn($paymentDO); - $this->subjectReader->expects(static::once()) + $this->subjectReaderMock->expects(static::once()) ->method('readTransaction') ->with($response) ->willReturn($transaction); diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php index 9ca9ca6aa07ae..e97eefc8a3444 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php @@ -8,9 +8,8 @@ use Braintree\Transaction; use Magento\Braintree\Gateway\Response\ThreeDSecureDetailsHandler; use Magento\Payment\Gateway\Data\PaymentDataObject; -use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -29,7 +28,7 @@ class ThreeDSecureDetailsHandlerTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Sales\Model\Order\Payment|MockObject */ - private $payment; + private $paymentMock; /** * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject @@ -38,7 +37,7 @@ class ThreeDSecureDetailsHandlerTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->payment = $this->getMockBuilder(Payment::class) + $this->paymentMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->setMethods([ 'unsAdditionalInformation', @@ -74,10 +73,10 @@ public function testHandle() ->with($response) ->willReturn($transaction); - $this->payment->expects(static::at(1)) + $this->paymentMock->expects(static::at(1)) ->method('setAdditionalInformation') ->with('liabilityShifted', 'Yes'); - $this->payment->expects(static::at(2)) + $this->paymentMock->expects(static::at(2)) ->method('setAdditionalInformation') ->with('liabilityShiftPossible', 'Yes'); @@ -97,7 +96,7 @@ private function getPaymentDataObjectMock() $mock->expects(static::once()) ->method('getPayment') - ->willReturn($this->payment); + ->willReturn($this->paymentMock); return $mock; } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php index 3a2d2f7073573..6cbca707242f1 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Response; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Response\TransactionIdHandler; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Model\Order\Payment; diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php index fb8f507bf1214..c8ec52560be29 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php @@ -5,19 +5,23 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Response; +use Braintree\Result\Successful; use Braintree\Transaction; use Braintree\Transaction\CreditCardDetails; use Magento\Braintree\Gateway\Config\Config; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Gateway\Response\PaymentDetailsHandler; use Magento\Braintree\Gateway\Response\VaultDetailsHandler; -use Magento\Framework\DataObject; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Sales\Api\Data\OrderPaymentExtension; use Magento\Sales\Api\Data\OrderPaymentExtensionInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory; -use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; -use Magento\Vault\Api\Data\PaymentTokenInterface; -use Magento\Vault\Model\CreditCardTokenFactory; +use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; +use Magento\Vault\Model\PaymentToken; +use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -25,193 +29,143 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class VaultDetailsHandlerTest extends \PHPUnit\Framework\TestCase +class VaultDetailsHandlerTest extends TestCase { - const TRANSACTION_ID = '432erwwe'; + private static $transactionId = '432erwwe'; + + private static $token = 'rh3gd4'; /** - * @var \Magento\Braintree\Gateway\Response\PaymentDetailsHandler + * @var PaymentDetailsHandler */ private $paymentHandler; /** - * @var \Magento\Sales\Model\Order\Payment|MockObject + * @var Payment|MockObject */ private $payment; /** - * @var CreditCardTokenFactory|MockObject + * @var PaymentTokenFactoryInterface|MockObject */ private $paymentTokenFactory; /** - * @var PaymentTokenInterface|MockObject - */ - protected $paymentToken; - - /** - * @var \Magento\Sales\Api\Data\OrderPaymentExtension|MockObject + * @var OrderPaymentExtension|MockObject */ private $paymentExtension; /** - * @var \Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory|MockObject + * @var OrderPaymentExtensionInterfaceFactory|MockObject */ private $paymentExtensionFactory; - /** - * @var SubjectReader|MockObject - */ - private $subjectReader; - - /** - * @var Config|MockObject - */ - private $config; - protected function setUp() { - $this->paymentToken = $this->createMock(PaymentTokenInterface::class); - $this->paymentTokenFactory = $this->getMockBuilder(CreditCardTokenFactory::class) + $objectManager = new ObjectManager($this); + $paymentToken = $objectManager->getObject(PaymentToken::class); + $this->paymentTokenFactory = $this->getMockBuilder(PaymentTokenFactoryInterface::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->paymentTokenFactory->expects(self::once()) - ->method('create') - ->willReturn($this->paymentToken); + $this->paymentTokenFactory->method('create') + ->with(PaymentTokenFactoryInterface::TOKEN_TYPE_CREDIT_CARD) + ->willReturn($paymentToken); - $this->paymentExtension = $this->getMockBuilder(OrderPaymentExtensionInterface::class) - ->setMethods(['setVaultPaymentToken', 'getVaultPaymentToken']) - ->disableOriginalConstructor() - ->getMock(); - $this->paymentExtensionFactory = $this->getMockBuilder(OrderPaymentExtensionInterfaceFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->paymentExtensionFactory->expects(self::once()) - ->method('create') - ->willReturn($this->paymentExtension); + $this->initPaymentExtensionAttributesMock(); + $this->paymentExtension->method('setVaultPaymentToken') + ->with($paymentToken); + $this->paymentExtension->method('getVaultPaymentToken') + ->willReturn($paymentToken); $this->payment = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() - ->setMethods(['__wakeup']) - ->getMock(); - - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) - ->disableOriginalConstructor() - ->getMock(); - - $mapperArray = [ - "american-express" => "AE", - "discover" => "DI", - "jcb" => "JCB", - "mastercard" => "MC", - "master-card" => "MC", - "visa" => "VI", - "maestro" => "MI", - "diners-club" => "DN", - "unionpay" => "CUP" - ]; - - $this->config = $this->getMockBuilder(Config::class) - ->setMethods(['getCctypesMapper']) - ->disableOriginalConstructor() + ->setMethods(['__wakeup', 'getExtensionAttributes']) ->getMock(); - $this->config->expects(self::once()) - ->method('getCctypesMapper') - ->willReturn($mapperArray); + $this->payment->expects(self::any())->method('getExtensionAttributes')->willReturn($this->paymentExtension); - $this->serializer = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + $config = $this->getConfigMock(); $this->paymentHandler = new VaultDetailsHandler( $this->paymentTokenFactory, $this->paymentExtensionFactory, - $this->config, - $this->subjectReader, - $this->serializer + $config, + new SubjectReader(), + new Json() ); } - /** - * @covers \Magento\Braintree\Gateway\Response\VaultDetailsHandler::handle - */ public function testHandle() { - $this->paymentExtension->expects(self::once()) - ->method('setVaultPaymentToken') - ->with($this->paymentToken); - $this->paymentExtension->expects(self::once()) - ->method('getVaultPaymentToken') - ->willReturn($this->paymentToken); - $paymentData = $this->getPaymentDataObjectMock(); - $transaction = $this->getBraintreeTransaction(); $subject = ['payment' => $paymentData]; - $response = ['object' => $transaction]; - - $this->subjectReader->expects(self::once()) - ->method('readPayment') - ->with($subject) - ->willReturn($paymentData); - $this->subjectReader->expects(self::once()) - ->method('readTransaction') - ->with($response) - ->willReturn($transaction); - $this->paymentToken->expects(static::once()) - ->method('setGatewayToken') - ->with('rh3gd4'); - $this->paymentToken->expects(static::once()) - ->method('setExpiresAt') - ->with('2022-01-01 00:00:00'); + $response = ['object' => $this->getBraintreeTransaction()]; $this->paymentHandler->handle($subject, $response); - $this->assertSame($this->paymentToken, $this->payment->getExtensionAttributes()->getVaultPaymentToken()); + $paymentToken = $this->payment->getExtensionAttributes() + ->getVaultPaymentToken(); + + self::assertEquals(self::$token, $paymentToken->getGatewayToken()); + self::assertEquals('2022-01-01 00:00:00', $paymentToken->getExpiresAt()); + + $details = json_decode($paymentToken->getTokenDetails(), true); + self::assertEquals( + [ + 'type' => 'AE', + 'maskedCC' => 1231, + 'expirationDate' => '12/2021' + ], + $details + ); } /** - * Create mock for payment data object and order payment - * @return MockObject + * Creates mock for payment data object and order payment. + * + * @return PaymentDataObject|MockObject */ - private function getPaymentDataObjectMock() + private function getPaymentDataObjectMock(): PaymentDataObject { $mock = $this->getMockBuilder(PaymentDataObject::class) ->setMethods(['getPayment']) ->disableOriginalConstructor() ->getMock(); - $mock->expects($this->once()) - ->method('getPayment') + $mock->method('getPayment') ->willReturn($this->payment); return $mock; } /** - * Create Braintree transaction - * @return MockObject + * Creates Braintree transaction. + * + * @return Successful */ private function getBraintreeTransaction() { $attributes = [ - 'id' => self::TRANSACTION_ID, + 'id' => self::$transactionId, 'creditCardDetails' => $this->getCreditCardDetails() ]; $transaction = Transaction::factory($attributes); + $result = new Successful(['transaction' => $transaction]); - return $transaction; + return $result; } /** - * Create Braintree transaction - * @return \Braintree\Transaction\CreditCardDetails + * Creates Braintree transaction. + * + * @return CreditCardDetails */ - private function getCreditCardDetails() + private function getCreditCardDetails(): CreditCardDetails { $attributes = [ - 'token' => 'rh3gd4', + 'token' => self::$token, 'bin' => '5421', 'cardType' => 'American Express', 'expirationMonth' => 12, @@ -223,4 +177,54 @@ private function getCreditCardDetails() return $creditCardDetails; } + + /** + * Creates mock of config class. + * + * @return Config|MockObject + */ + private function getConfigMock(): Config + { + $mapperArray = [ + 'american-express' => 'AE', + 'discover' => 'DI', + 'jcb' => 'JCB', + 'mastercard' => 'MC', + 'master-card' => 'MC', + 'visa' => 'VI', + 'maestro' => 'MI', + 'diners-club' => 'DN', + 'unionpay' => 'CUP' + ]; + + $config = $this->getMockBuilder(Config::class) + ->setMethods(['getCctypesMapper']) + ->disableOriginalConstructor() + ->getMock(); + + $config->method('getCctypesMapper') + ->willReturn($mapperArray); + + return $config; + } + + /** + * Initializes payment extension attributes mocks. + * + * @return void + */ + private function initPaymentExtensionAttributesMock() + { + $this->paymentExtension = $this->getMockBuilder(OrderPaymentExtensionInterface::class) + ->setMethods(['setVaultPaymentToken', 'getVaultPaymentToken']) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentExtensionFactory = $this->getMockBuilder(OrderPaymentExtensionInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->paymentExtensionFactory->method('create') + ->willReturn($this->paymentExtension); + } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php index 398349a9692b7..a541b0115fe63 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php @@ -5,7 +5,7 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Response; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Response\VoidHandler; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Model\Order\Payment; diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php new file mode 100644 index 0000000000000..fd524a10ba531 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/SubjectReaderTest.php @@ -0,0 +1,167 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Braintree\Test\Unit\Gateway; + +use Braintree\Result\Successful; +use Braintree\Transaction; +use Magento\Braintree\Gateway\SubjectReader; + +/** + * Class SubjectReaderTest + */ +class SubjectReaderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->subjectReader = new SubjectReader(); + } + + /** + * @covers \Magento\Braintree\Gateway\SubjectReader::readCustomerId + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The "customerId" field does not exists + * @return void + */ + public function testReadCustomerIdWithException(): void + { + $this->subjectReader->readCustomerId([]); + } + + /** + * @covers \Magento\Braintree\Gateway\SubjectReader::readCustomerId + * @return void + */ + public function testReadCustomerId(): void + { + $customerId = 1; + $this->assertEquals($customerId, $this->subjectReader->readCustomerId(['customer_id' => $customerId])); + } + + /** + * @covers \Magento\Braintree\Gateway\SubjectReader::readPublicHash + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The "public_hash" field does not exists + * @return void + */ + public function testReadPublicHashWithException(): void + { + $this->subjectReader->readPublicHash([]); + } + + /** + * @covers \Magento\Braintree\Gateway\SubjectReader::readPublicHash + * @return void + */ + public function testReadPublicHash(): void + { + $hash = 'fj23djf2o1fd'; + $this->assertEquals($hash, $this->subjectReader->readPublicHash(['public_hash' => $hash])); + } + + /** + * @covers \Magento\Braintree\Gateway\SubjectReader::readPayPal + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Transaction has't paypal attribute + * @return void + */ + public function testReadPayPalWithException(): void + { + $transaction = Transaction::factory([ + 'id' => 'u38rf8kg6vn', + ]); + $this->subjectReader->readPayPal($transaction); + } + + /** + * @covers \Magento\Braintree\Gateway\SubjectReader::readPayPal + * @return void + */ + public function testReadPayPal(): void + { + $paypal = [ + 'paymentId' => '3ek7dk7fn0vi1', + 'payerEmail' => 'payer@example.com', + ]; + $transaction = Transaction::factory([ + 'id' => '4yr95vb', + 'paypal' => $paypal, + ]); + + $this->assertEquals($paypal, $this->subjectReader->readPayPal($transaction)); + } + + /** + * Checks a case when subject reader retrieves successful Braintree transaction. + * + * @return void + */ + public function testReadTransaction(): void + { + $transaction = Transaction::factory(['id' => 1]); + $response = [ + 'object' => new Successful($transaction, 'transaction'), + ]; + $actual = $this->subjectReader->readTransaction($response); + + $this->assertSame($transaction, $actual); + } + + /** + * Checks a case when subject reader retrieves invalid data instead transaction details. + * + * @param array $response + * @param string $expectedMessage + * @dataProvider invalidTransactionResponseDataProvider + * @expectedException \InvalidArgumentException + * @return void + */ + public function testReadTransactionWithInvalidResponse(array $response, string $expectedMessage): void + { + $this->expectExceptionMessage($expectedMessage); + $this->subjectReader->readTransaction($response); + } + + /** + * Gets list of variations with invalid subject data. + * + * @return array + */ + public function invalidTransactionResponseDataProvider(): array + { + $transaction = new \stdClass(); + $response = new \stdClass(); + $response->transaction = $transaction; + + return [ + [ + 'response' => [ + 'object' => [], + ], + 'expectedMessage' => 'Response object does not exist.', + ], + [ + 'response' => [ + 'object' => new \stdClass(), + ], + 'expectedMessage' => 'The object is not a class \Braintree\Transaction.', + ], + [ + 'response' => [ + 'object' => $response, + ], + 'expectedMessage' => 'The object is not a class \Braintree\Transaction.', + ], + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/CancelResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/CancelResponseValidatorTest.php new file mode 100644 index 0000000000000..65386272fe511 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/CancelResponseValidatorTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Gateway\Validator; + +use Braintree\Result\Error; +use Magento\Braintree\Gateway\Validator\CancelResponseValidator; +use PHPUnit\Framework\TestCase; +use Magento\Braintree\Gateway\Validator\GeneralResponseValidator; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Tests \Magento\Braintree\Gateway\Validator\CancelResponseValidator class. + */ +class CancelResponseValidatorTest extends TestCase +{ + /** + * @var CancelResponseValidator + */ + private $validator; + + /** + * @var GeneralResponseValidator|MockObject + */ + private $generalValidator; + + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->generalValidator = $this->getMockBuilder(GeneralResponseValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resultFactory = $this->getMockBuilder(ResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->validator = new CancelResponseValidator( + $this->resultFactory, + $this->generalValidator, + new SubjectReader() + ); + } + + /** + * Checks a case when response is successful and additional validation doesn't needed. + * + * @return void + */ + public function testValidateSuccessfulTransaction(): void + { + /** @var ResultInterface|MockObject $result */ + $result = $this->getMockForAbstractClass(ResultInterface::class); + $result->method('isValid')->willReturn(true); + $this->generalValidator->method('validate')->willReturn($result); + $actual = $this->validator->validate([]); + + $this->assertSame($result, $actual); + } + + /** + * Checks a case when response contains error related to expired authorization transaction and + * validator should return positive result. + * + * @return void + */ + public function testValidateExpiredTransaction(): void + { + /** @var ResultInterface|MockObject $result */ + $result = $this->getMockForAbstractClass(ResultInterface::class); + $result->method('isValid')->willReturn(false); + $this->generalValidator->method('validate')->willReturn($result); + + $expected = $this->getMockForAbstractClass(ResultInterface::class); + $expected->method('isValid')->willReturn(true); + $this->resultFactory->method('create') + ->with( + [ + 'isValid' => true, + 'failsDescription' => ['Transaction is cancelled offline.'], + 'errorCodes' => [] + ] + )->willReturn($expected); + + $errors = [ + 'errors' => [ + [ + 'code' => 91504, + 'message' => 'Transaction can only be voided if status is authorized.', + ], + ], + ]; + $buildSubject = [ + 'response' => [ + 'object' => new Error(['errors' => $errors]), + ], + ]; + + $actual = $this->validator->validate($buildSubject); + + $this->assertSame($expected, $actual); + } + + /** + * Checks a case when response contains multiple errors and validator should return negative result. + * + * @param array $responseErrors + * @return void + * @dataProvider getErrorsDataProvider + */ + public function testValidateWithMultipleErrors(array $responseErrors): void + { + /** @var ResultInterface|MockObject $result */ + $result = $this->getMockForAbstractClass(ResultInterface::class); + $result->method('isValid')->willReturn(false); + + $this->generalValidator->method('validate')->willReturn($result); + + $this->resultFactory->expects($this->never())->method('create'); + + $errors = [ + 'errors' => $responseErrors, + ]; + $buildSubject = [ + 'response' => [ + 'object' => new Error(['errors' => $errors]), + ] + ]; + + $actual = $this->validator->validate($buildSubject); + + $this->assertSame($result, $actual); + } + + /** + * Gets list of errors variations. + * + * @return array + */ + public function getErrorsDataProvider(): array + { + return [ + [ + 'errors' => [ + [ + 'code' => 91734, + 'message' => 'Credit card type is not accepted by this merchant account.', + ], + [ + 'code' => 91504, + 'message' => 'Transaction can only be voided if status is authorized.', + ], + ], + ], + [ + 'errors' => [ + [ + 'code' => 91734, + 'message' => 'Credit card type is not accepted by this merchant account.', + ], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php index 1a9e547e90636..4741a3ea38c6f 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php @@ -5,12 +5,14 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Validator; -use Braintree\Transaction; +use Braintree\Result\Error; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Gateway\Validator\ErrorCodeProvider; +use Magento\Braintree\Gateway\Validator\GeneralResponseValidator; use Magento\Framework\Phrase; -use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\Result; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; -use Magento\Braintree\Gateway\Validator\GeneralResponseValidator; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class GeneralResponseValidatorTest extends \PHPUnit\Framework\TestCase { @@ -20,14 +22,9 @@ class GeneralResponseValidatorTest extends \PHPUnit\Framework\TestCase private $responseValidator; /** - * @var ResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResultInterfaceFactory|MockObject */ - private $resultInterfaceFactoryMock; - - /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject - */ - private $subjectReaderMock; + private $resultInterfaceFactory; /** * Set up @@ -36,85 +33,105 @@ class GeneralResponseValidatorTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->resultInterfaceFactoryMock = $this->getMockBuilder( - \Magento\Payment\Gateway\Validator\ResultInterfaceFactory::class - )->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) + $this->resultInterfaceFactory = $this->getMockBuilder(ResultInterfaceFactory::class) ->disableOriginalConstructor() + ->setMethods(['create']) ->getMock(); $this->responseValidator = new GeneralResponseValidator( - $this->resultInterfaceFactoryMock, - $this->subjectReaderMock + $this->resultInterfaceFactory, + new SubjectReader(), + new ErrorCodeProvider() ); } /** - * Run test for validate method + * Checks a case when the validator processes successful and failed transactions. * * @param array $validationSubject * @param bool $isValid * @param Phrase[] $messages + * @param array $errorCodes * @return void * * @dataProvider dataProviderTestValidate */ - public function testValidate(array $validationSubject, $isValid, $messages) + public function testValidate(array $validationSubject, bool $isValid, $messages, array $errorCodes) { - /** @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject $resultMock */ - $resultMock = $this->createMock(ResultInterface::class); - - $this->subjectReaderMock->expects(self::once()) - ->method('readResponseObject') - ->with($validationSubject) - ->willReturn($validationSubject['response']['object']); + $result = new Result($isValid, $messages); - $this->resultInterfaceFactoryMock->expects(self::once()) - ->method('create') + $this->resultInterfaceFactory->method('create') ->with([ 'isValid' => $isValid, - 'failsDescription' => $messages + 'failsDescription' => $messages, + 'errorCodes' => $errorCodes ]) - ->willReturn($resultMock); + ->willReturn($result); - $actualMock = $this->responseValidator->validate($validationSubject); + $actual = $this->responseValidator->validate($validationSubject); - self::assertEquals($resultMock, $actualMock); + self::assertEquals($result, $actual); } /** + * Gets variations for different type of response. + * * @return array */ public function dataProviderTestValidate() { - $successTrue = new \stdClass(); - $successTrue->success = true; + $successTransaction = new \stdClass(); + $successTransaction->success = true; - $successFalse = new \stdClass(); - $successFalse->success = false; + $failureTransaction = new \stdClass(); + $failureTransaction->success = false; + $failureTransaction->message = 'Transaction was failed.'; + + $errors = [ + 'errors' => [ + [ + 'code' => 81804, + 'attribute' => 'base', + 'message' => 'Cannot process transaction.' + ] + ] + ]; + $errorTransaction = new Error(['errors' => $errors]); return [ [ 'validationSubject' => [ 'response' => [ - 'object' => $successTrue + 'object' => $successTransaction ], ], 'isValid' => true, - [] + [], + 'errorCodes' => [] ], [ 'validationSubject' => [ 'response' => [ - 'object' => $successFalse + 'object' => $failureTransaction + ] + ], + 'isValid' => false, + [ + __('Transaction was failed.') + ], + 'errorCodes' => [] + ], + [ + 'validationSubject' => [ + 'response' => [ + 'object' => $errorTransaction ] ], 'isValid' => false, [ __('Braintree error response.') - ] + ], + 'errorCodes' => ['81804'] ] ]; } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php index 294226b1656ec..530945c974ceb 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php @@ -5,15 +5,13 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Validator; -use Braintree\Transaction; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Gateway\Validator\ErrorCodeProvider; use Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator; -use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\Result; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; -use Magento\Braintree\Gateway\Helper\SubjectReader; +use PHPUnit_Framework_MockObject_MockObject as MockObject; -/** - * Class PaymentNonceResponseValidatorTest - */ class PaymentNonceResponseValidatorTest extends \PHPUnit\Framework\TestCase { /** @@ -22,35 +20,24 @@ class PaymentNonceResponseValidatorTest extends \PHPUnit\Framework\TestCase private $validator; /** - * @var ResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResultInterfaceFactory|MockObject */ private $resultInterfaceFactory; - /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject - */ - private $subjectReader; - protected function setUp() { $this->resultInterfaceFactory = $this->getMockBuilder(ResultInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) - ->disableOriginalConstructor() - ->setMethods(['readResponseObject']) - ->getMock(); $this->validator = new PaymentNonceResponseValidator( $this->resultInterfaceFactory, - $this->subjectReader + new SubjectReader(), + new ErrorCodeProvider() ); } - /** - * @covers \Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator::validate - */ public function testFailedValidate() { $obj = new \stdClass(); @@ -61,23 +48,12 @@ public function testFailedValidate() ] ]; - $this->subjectReader->expects(static::once()) - ->method('readResponseObject') - ->willReturn($obj); - - $result = $this->createMock(ResultInterface::class); - $this->resultInterfaceFactory->expects(self::once()) - ->method('create') - ->with([ - 'isValid' => false, - 'failsDescription' => [ - __('Payment method nonce can\'t be retrieved.') - ] - ]) + $result = new Result(false, [__('Payment method nonce can\'t be retrieved.')]); + $this->resultInterfaceFactory->method('create') ->willReturn($result); $actual = $this->validator->validate($subject); - static::assertEquals($result, $actual); + self::assertEquals($result, $actual); } public function testValidateSuccess() @@ -93,20 +69,11 @@ public function testValidateSuccess() ] ]; - $this->subjectReader->expects(static::once()) - ->method('readResponseObject') - ->willReturn($obj); - - $result = $this->createMock(ResultInterface::class); - $this->resultInterfaceFactory->expects(self::once()) - ->method('create') - ->with([ - 'isValid' => true, - 'failsDescription' => [] - ]) + $result = new Result(true); + $this->resultInterfaceFactory->method('create') ->willReturn($result); $actual = $this->validator->validate($subject); - static::assertEquals($result, $actual); + self::assertEquals($result, $actual); } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php index aeb9b4a83077c..360e1ff0525b6 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php @@ -5,15 +5,16 @@ */ namespace Magento\Braintree\Test\Unit\Gateway\Validator; +use Braintree\Result\Successful; use Braintree\Transaction; +use Magento\Braintree\Gateway\SubjectReader; +use Magento\Braintree\Gateway\Validator\ErrorCodeProvider; +use Magento\Braintree\Gateway\Validator\ResponseValidator; use Magento\Framework\Phrase; +use Magento\Payment\Gateway\Validator\Result; use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; -use Magento\Braintree\Gateway\Validator\ResponseValidator; -use Magento\Braintree\Gateway\Helper\SubjectReader; use PHPUnit_Framework_MockObject_MockObject as MockObject; -use Braintree\Result\Error; -use Braintree\Result\Successful; /** * Class ResponseValidatorTest @@ -30,11 +31,6 @@ class ResponseValidatorTest extends \PHPUnit\Framework\TestCase */ private $resultInterfaceFactory; - /** - * @var SubjectReader|MockObject - */ - private $subjectReader; - /** * Set up * @@ -46,13 +42,11 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->subjectReader = $this->getMockBuilder(SubjectReader::class) - ->disableOriginalConstructor() - ->getMock(); $this->responseValidator = new ResponseValidator( $this->resultInterfaceFactory, - $this->subjectReader + new SubjectReader(), + new ErrorCodeProvider() ); } @@ -65,11 +59,6 @@ public function testValidateReadResponseException() 'response' => null ]; - $this->subjectReader->expects(self::once()) - ->method('readResponseObject') - ->with($validationSubject) - ->willThrowException(new \InvalidArgumentException()); - $this->responseValidator->validate($validationSubject); } @@ -82,11 +71,6 @@ public function testValidateReadResponseObjectException() 'response' => ['object' => null] ]; - $this->subjectReader->expects(self::once()) - ->method('readResponseObject') - ->with($validationSubject) - ->willThrowException(new \InvalidArgumentException()); - $this->responseValidator->validate($validationSubject); } @@ -103,19 +87,9 @@ public function testValidateReadResponseObjectException() public function testValidate(array $validationSubject, $isValid, $messages) { /** @var ResultInterface|MockObject $result */ - $result = $this->createMock(ResultInterface::class); - - $this->subjectReader->expects(self::once()) - ->method('readResponseObject') - ->with($validationSubject) - ->willReturn($validationSubject['response']['object']); - - $this->resultInterfaceFactory->expects(self::once()) - ->method('create') - ->with([ - 'isValid' => $isValid, - 'failsDescription' => $messages - ]) + $result = new Result($isValid, $messages); + + $this->resultInterfaceFactory->method('create') ->willReturn($result); $actual = $this->responseValidator->validate($validationSubject); @@ -141,8 +115,6 @@ public function dataProviderTestValidate() $transactionDeclined->transaction = new \stdClass(); $transactionDeclined->transaction->status = Transaction::SETTLEMENT_DECLINED; - $errorResult = new Error(['errors' => []]); - return [ [ 'validationSubject' => [ @@ -175,18 +147,6 @@ public function dataProviderTestValidate() [ __('Wrong transaction status') ] - ], - [ - 'validationSubject' => [ - 'response' => [ - 'object' => $errorResult, - ] - ], - 'isValid' => false, - [ - __('Braintree error response.'), - __('Wrong transaction status') - ] ] ]; } diff --git a/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php b/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php index 9b80a2237a8fb..c82634d36db31 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php @@ -84,11 +84,11 @@ public function testGetCodeWithException() public function getCodeDataProvider() { return [ - ['avsZip' => null, 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => null, 'avsStreet' => 'M', 'expected' => 'U'], - ['avsZip' => 'M', 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => 'M', 'avsStreet' => 'Unknown', 'expected' => 'U'], - ['avsZip' => 'I', 'avsStreet' => 'A', 'expected' => 'U'], + ['avsZip' => null, 'avsStreet' => null, 'expected' => ''], + ['avsZip' => null, 'avsStreet' => 'M', 'expected' => ''], + ['avsZip' => 'M', 'avsStreet' => null, 'expected' => ''], + ['avsZip' => 'M', 'avsStreet' => 'Unknown', 'expected' => ''], + ['avsZip' => 'I', 'avsStreet' => 'A', 'expected' => ''], ['avsZip' => 'M', 'avsStreet' => 'M', 'expected' => 'Y'], ['avsZip' => 'N', 'avsStreet' => 'M', 'expected' => 'A'], ['avsZip' => 'M', 'avsStreet' => 'N', 'expected' => 'Z'], diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/AvailabilityCheckerTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/AvailabilityCheckerTest.php new file mode 100644 index 0000000000000..2248aab1aad2e --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/AvailabilityCheckerTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Model\InstantPurchase\CreditCard; + +use Magento\Braintree\Gateway\Config\Config; +use Magento\Braintree\Model\InstantPurchase\CreditCard\AvailabilityChecker; + +/** + * @covers \Magento\Braintree\Model\InstantPurchase\CreditCard\AvailabilityChecker + */ +class AvailabilityCheckerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var AvailabilityChecker + */ + private $availabilityChecker; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + $this->availabilityChecker = new AvailabilityChecker($this->configMock); + } + + /** + * Test isAvailable method + * + * @dataProvider isAvailableDataProvider + * + * @param bool $isVerify3DSecure + * @param bool $expected + * + * @return void + */ + public function testIsAvailable(bool $isVerify3DSecure, bool $expected) + { + $this->configMock->expects($this->once())->method('isVerify3DSecure')->willReturn($isVerify3DSecure); + $actual = $this->availabilityChecker->isAvailable(); + self::assertEquals($expected, $actual); + } + + /** + * Data provider for isAvailable method test + * + * @return array + */ + public function isAvailableDataProvider() + { + return [ + [true, false], + [false, true], + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/TokenFormatterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/TokenFormatterTest.php new file mode 100644 index 0000000000000..a5c7cd743d85f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/TokenFormatterTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Model\InstantPurchase\CreditCard; + +use Magento\Braintree\Model\InstantPurchase\CreditCard\TokenFormatter as CreditCardTokenFormatter; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +class TokenFormatterTest extends TestCase +{ + /** + * @var PaymentTokenInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $paymentTokenMock; + + /** + * @var CreditCardTokenFormatter + */ + private $creditCardTokenFormatter; + + /** + * @var array + */ + private $tokenDetails = [ + 'type' => 'visa', + 'maskedCC' => '1111************9999', + 'expirationDate' => '01-01-2020' + ]; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->paymentTokenMock = $this->getMockBuilder(PaymentTokenInterface::class) + ->getMockForAbstractClass(); + + $this->creditCardTokenFormatter = new CreditCardTokenFormatter(); + } + + /** + * Testing the payment format with a known credit card type + * + * @return void + */ + public function testFormatPaymentTokenWithKnownCardType() + { + $this->tokenDetails['type'] = key(CreditCardTokenFormatter::$baseCardTypes); + $this->paymentTokenMock->expects($this->once()) + ->method('getTokenDetails') + ->willReturn(json_encode($this->tokenDetails)); + + $formattedString = sprintf( + '%s: %s, %s: %s (%s: %s)', + __('Credit Card'), + reset(CreditCardTokenFormatter::$baseCardTypes), + __('ending'), + $this->tokenDetails['maskedCC'], + __('expires'), + $this->tokenDetails['expirationDate'] + ); + + self::assertEquals( + $formattedString, + $this->creditCardTokenFormatter->formatPaymentToken($this->paymentTokenMock) + ); + } + + /** + * Testing the payment format with a unknown credit card type + * + * @return void + */ + public function testFormatPaymentTokenWithUnknownCardType() + { + $this->paymentTokenMock->expects($this->once()) + ->method('getTokenDetails') + ->willReturn(json_encode($this->tokenDetails)); + + $formattedString = sprintf( + '%s: %s, %s: %s (%s: %s)', + __('Credit Card'), + $this->tokenDetails['type'], + __('ending'), + $this->tokenDetails['maskedCC'], + __('expires'), + $this->tokenDetails['expirationDate'] + ); + + self::assertEquals( + $formattedString, + $this->creditCardTokenFormatter->formatPaymentToken($this->paymentTokenMock) + ); + } + + /** + * Testing the payment format with wrong card data + * + * @return void + */ + public function testFormatPaymentTokenWithWrongData() + { + unset($this->tokenDetails['type']); + $this->paymentTokenMock->expects($this->once()) + ->method('getTokenDetails') + ->willReturn(json_encode($this->tokenDetails)); + self::expectException('\InvalidArgumentException'); + + $this->creditCardTokenFormatter->formatPaymentToken($this->paymentTokenMock); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PayPal/TokenFormatterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PayPal/TokenFormatterTest.php new file mode 100644 index 0000000000000..e4cd8fd58043b --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PayPal/TokenFormatterTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Model\InstantPurchase\PayPal; + +use Magento\Braintree\Model\InstantPurchase\CreditCard\TokenFormatter as PaypalTokenFormatter; +use Magento\Vault\Api\Data\PaymentTokenInterface; + +class TokenFormatterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var PaymentTokenInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentTokenMock; + + /** + * @var PaypalTokenFormatter + */ + private $paypalTokenFormatter; + + /** + * @var array + */ + private $tokenDetails = [ + 'type' => 'visa', + 'maskedCC' => '4444************9999', + 'expirationDate' => '07-07-2025' + ]; + + /** + * Test setup + */ + protected function setUp() + { + $this->paymentTokenMock = $this->getMockBuilder(PaymentTokenInterface::class) + ->getMockForAbstractClass(); + + $this->paypalTokenFormatter = new PaypalTokenFormatter(); + } + + /** + * testFormatPaymentTokenWithKnownCardType + */ + public function testFormatPaymentTokenWithKnownCardType() + { + $this->tokenDetails['type'] = key(PaypalTokenFormatter::$baseCardTypes); + $this->paymentTokenMock->expects($this->once()) + ->method('getTokenDetails') + ->willReturn(json_encode($this->tokenDetails)); + + $formattedString = sprintf( + '%s: %s, %s: %s (%s: %s)', + __('Credit Card'), + reset(PaypalTokenFormatter::$baseCardTypes), + __('ending'), + $this->tokenDetails['maskedCC'], + __('expires'), + $this->tokenDetails['expirationDate'] + ); + + self::assertEquals($formattedString, $this->paypalTokenFormatter->formatPaymentToken($this->paymentTokenMock)); + } + + /** + * testFormatPaymentTokenWithUnknownCardType + */ + public function testFormatPaymentTokenWithUnknownCardType() + { + $this->paymentTokenMock->expects($this->once()) + ->method('getTokenDetails') + ->willReturn(json_encode($this->tokenDetails)); + + $formattedString = sprintf( + '%s: %s, %s: %s (%s: %s)', + __('Credit Card'), + $this->tokenDetails['type'], + __('ending'), + $this->tokenDetails['maskedCC'], + __('expires'), + $this->tokenDetails['expirationDate'] + ); + + self::assertEquals($formattedString, $this->paypalTokenFormatter->formatPaymentToken($this->paymentTokenMock)); + } + + /** + * testFormatPaymentTokenWithWrongData + */ + public function testFormatPaymentTokenWithWrongData() + { + unset($this->tokenDetails['type']); + + $this->paymentTokenMock->expects($this->once()) + ->method('getTokenDetails') + ->willReturn(json_encode($this->tokenDetails)); + self::expectException('\InvalidArgumentException'); + + $this->paypalTokenFormatter->formatPaymentToken($this->paymentTokenMock); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PaymentAdditionalInformationProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PaymentAdditionalInformationProviderTest.php new file mode 100644 index 0000000000000..2631fcbe5f5b5 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PaymentAdditionalInformationProviderTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Model\InstantPurchase; + +use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider; +use Magento\Payment\Gateway\Command\Result\ArrayResult; +use Magento\Vault\Api\Data\PaymentTokenInterface; + +/** + * @covers \Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider + */ +class PaymentAdditionalInformationProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var PaymentAdditionalInformationProvider + */ + private $paymentAdditionalInformationProvider; + + /** + * @var GetPaymentNonceCommand|\PHPUnit_Framework_MockObject_MockObject + */ + private $getPaymentNonceCommandMock; + + /** + * @var PaymentTokenInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentTokenMock; + + /** + * @var ArrayResult|\PHPUnit_Framework_MockObject_MockObject + */ + private $arrayResultMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->getPaymentNonceCommandMock = $this->createMock(GetPaymentNonceCommand::class); + $this->paymentTokenMock = $this->createMock(PaymentTokenInterface::class); + $this->arrayResultMock = $this->createMock(ArrayResult::class); + $this->paymentAdditionalInformationProvider = new PaymentAdditionalInformationProvider( + $this->getPaymentNonceCommandMock + ); + } + + /** + * Test getAdditionalInformation method + * + * @return void + */ + public function testGetAdditionalInformation() + { + $customerId = 15; + $publicHash = '3n4b7sn48g'; + $paymentMethodNonce = 'test'; + + $this->paymentTokenMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->paymentTokenMock->expects($this->once())->method('getPublicHash')->willReturn($publicHash); + $this->getPaymentNonceCommandMock->expects($this->once())->method('execute')->with([ + PaymentTokenInterface::CUSTOMER_ID => $customerId, + PaymentTokenInterface::PUBLIC_HASH => $publicHash, + ])->willReturn($this->arrayResultMock); + $this->arrayResultMock->expects($this->once())->method('get') + ->willReturn(['paymentMethodNonce' => $paymentMethodNonce]); + + $expected = [ + 'payment_method_nonce' => $paymentMethodNonce, + ]; + $actual = $this->paymentAdditionalInformationProvider->getAdditionalInformation($this->paymentTokenMock); + self::assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/LocaleResolverTest.php b/app/code/Magento/Braintree/Test/Unit/Model/LocaleResolverTest.php new file mode 100644 index 0000000000000..b6ef534c55c29 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/LocaleResolverTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Test\Unit\Model; + +use Magento\Braintree\Gateway\Config\PayPal\Config; +use Magento\Braintree\Model\LocaleResolver; +use Magento\Framework\Locale\ResolverInterface; + +/** + * @covers \Magento\Braintree\Model\LocaleResolver + */ +class LocaleResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var LocaleResolver + */ + private $localeResolver; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var ResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $resolverMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + $this->resolverMock = $this->createMock(ResolverInterface::class); + $this->localeResolver = new LocaleResolver($this->resolverMock, $this->configMock); + } + + /** + * Test getDefaultLocalePath method + * + * @return void + */ + public function testGetDefaultLocalePath() + { + $expected = 'general/locale/code'; + $this->resolverMock->expects($this->once())->method('getDefaultLocalePath')->willReturn($expected); + $actual = $this->localeResolver->getDefaultLocalePath(); + self::assertEquals($expected, $actual); + } + + /** + * Test setDefaultLocale method + * + * @return void + */ + public function testSetDefaultLocale() + { + $defaultLocale = 'en_US'; + $this->resolverMock->expects($this->once())->method('setDefaultLocale')->with($defaultLocale); + $this->localeResolver->setDefaultLocale($defaultLocale); + } + + /** + * Test getDefaultLocale method + * + * @return void + */ + public function testGetDefaultLocale() + { + $expected = 'fr_FR'; + $this->resolverMock->expects($this->once())->method('getDefaultLocale')->willReturn($expected); + $actual = $this->localeResolver->getDefaultLocale(); + self::assertEquals($expected, $actual); + } + + /** + * Test setLocale method + * + * @return void + */ + public function testSetLocale() + { + $locale = 'en_GB'; + $this->resolverMock->expects($this->once())->method('setLocale')->with($locale); + $this->localeResolver->setLocale($locale); + } + + /** + * Test getLocale method + * + * @return void + */ + public function testGetLocale() + { + $locale = 'en_TEST'; + $allowedLocales = 'en_US,en_GB,en_AU,da_DK,fr_FR,fr_CA,de_DE,zh_HK,it_IT,nl_NL'; + $this->resolverMock->expects($this->once())->method('getLocale')->willReturn($locale); + $this->configMock->expects($this->once())->method('getValue')->with('supported_locales') + ->willReturn($allowedLocales); + + $expected = 'en_US'; + $actual = $this->localeResolver->getLocale(); + self::assertEquals($expected, $actual); + } + + /** + * Test emulate method + * + * @return void + */ + public function testEmulate() + { + $scopeId = 12; + $this->resolverMock->expects($this->once())->method('emulate')->with($scopeId); + $this->localeResolver->emulate($scopeId); + } + + /** + * Test revert method + * + * @return void + */ + public function testRevert() + { + $this->resolverMock->expects($this->once())->method('revert'); + $this->localeResolver->revert(); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php index 1aecba91b9afc..c8524017274a4 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Model\Paypal\Helper; use Magento\Braintree\Model\Paypal\Helper\OrderPlace; +use Magento\Braintree\Model\Paypal\OrderCancellationService; use Magento\Checkout\Api\AgreementsValidatorInterface; use Magento\Checkout\Helper\Data; use Magento\Checkout\Model\Type\Onepage; @@ -14,6 +17,7 @@ use Magento\Quote\Api\CartManagementInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; +use PHPUnit\Framework\MockObject\MockObject; /** * Class OrderPlaceTest @@ -27,62 +31,80 @@ class OrderPlaceTest extends \PHPUnit\Framework\TestCase const TEST_EMAIL = 'test@test.loc'; /** - * @var CartManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CartManagementInterface|MockObject */ - private $cartManagementMock; + private $cartManagement; /** - * @var AgreementsValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AgreementsValidatorInterface|MockObject */ - private $agreementsValidatorMock; + private $agreementsValidator; /** - * @var Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - private $customerSessionMock; + private $customerSession; /** - * @var Data|\PHPUnit_Framework_MockObject_MockObject + * @var Data|MockObject */ - private $checkoutHelperMock; + private $checkoutHelper; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $billingAddressMock; + private $billingAddress; /** * @var OrderPlace */ private $orderPlace; + /** + * @var OrderCancellationService|MockObject + */ + private $orderCancellation; + + /** + * @inheritdoc + */ protected function setUp() { - $this->cartManagementMock = $this->getMockBuilder(CartManagementInterface::class) + $this->cartManagement = $this->getMockBuilder(CartManagementInterface::class) ->getMockForAbstractClass(); - $this->agreementsValidatorMock = $this->getMockBuilder(AgreementsValidatorInterface::class) + $this->agreementsValidator = $this->getMockBuilder(AgreementsValidatorInterface::class) ->getMockForAbstractClass(); - $this->customerSessionMock = $this->getMockBuilder(Session::class) + $this->customerSession = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->checkoutHelper = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); - $this->checkoutHelperMock = $this->getMockBuilder(Data::class) + + $this->orderCancellation = $this->getMockBuilder(OrderCancellationService::class) ->disableOriginalConstructor() ->getMock(); $this->orderPlace = new OrderPlace( - $this->cartManagementMock, - $this->agreementsValidatorMock, - $this->customerSessionMock, - $this->checkoutHelperMock + $this->cartManagement, + $this->agreementsValidator, + $this->customerSession, + $this->checkoutHelper, + $this->orderCancellation ); } + /** + * Checks a scenario for a guest customer. + * + * @throws \Exception + */ public function testExecuteGuest() { $agreement = ['test', 'test']; $quoteMock = $this->getQuoteMock(); - $this->agreementsValidatorMock->expects(self::once()) + $this->agreementsValidator->expects(self::once()) ->method('isValid') ->willReturn(true); @@ -97,7 +119,7 @@ public function testExecuteGuest() ->method('getId') ->willReturn(10); - $this->cartManagementMock->expects(self::once()) + $this->cartManagement->expects(self::once()) ->method('placeOrder') ->with(10); @@ -105,9 +127,11 @@ public function testExecuteGuest() } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Disables address validation. + * + * @param MockObject $quoteMock */ - private function disabledQuoteAddressValidationStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function disabledQuoteAddressValidationStep(MockObject $quoteMock) { $billingAddressMock = $this->getBillingAddressMock($quoteMock); $shippingAddressMock = $this->getMockBuilder(Address::class) @@ -115,26 +139,21 @@ private function disabledQuoteAddressValidationStep(\PHPUnit_Framework_MockObjec ->disableOriginalConstructor() ->getMock(); - $quoteMock->expects(self::once()) - ->method('getShippingAddress') + $quoteMock->method('getShippingAddress') ->willReturn($shippingAddressMock); - $billingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $billingAddressMock->method('setShouldIgnoreValidation') ->with(true) ->willReturnSelf(); - $quoteMock->expects(self::once()) - ->method('getIsVirtual') + $quoteMock->method('getIsVirtual') ->willReturn(false); - $shippingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $shippingAddressMock->method('setShouldIgnoreValidation') ->with(true) ->willReturnSelf(); - $billingAddressMock->expects(self::any()) - ->method('getEmail') + $billingAddressMock->method('getEmail') ->willReturn(self::TEST_EMAIL); $billingAddressMock->expects(self::never()) @@ -142,25 +161,24 @@ private function disabledQuoteAddressValidationStep(\PHPUnit_Framework_MockObjec } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Prepares checkout step. + * + * @param MockObject $quoteMock */ - private function getCheckoutMethodStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function getCheckoutMethodStep(MockObject $quoteMock) { - $this->customerSessionMock->expects(self::once()) - ->method('isLoggedIn') + $this->customerSession->method('isLoggedIn') ->willReturn(false); $quoteMock->expects(self::at(1)) ->method('getCheckoutMethod') ->willReturn(null); - $this->checkoutHelperMock->expects(self::once()) - ->method('isAllowedGuestCheckout') + $this->checkoutHelper->method('isAllowedGuestCheckout') ->with($quoteMock) ->willReturn(true); - $quoteMock->expects(self::once()) - ->method('setCheckoutMethod') + $quoteMock->method('setCheckoutMethod') ->with(Onepage::METHOD_GUEST); $quoteMock->expects(self::at(2)) @@ -169,9 +187,11 @@ private function getCheckoutMethodStep(\PHPUnit_Framework_MockObject_MockObject } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Prepares quote. + * + * @param MockObject $quoteMock */ - private function prepareGuestQuoteStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function prepareGuestQuoteStep(MockObject $quoteMock) { $billingAddressMock = $this->getBillingAddressMock($quoteMock); @@ -184,44 +204,44 @@ private function prepareGuestQuoteStep(\PHPUnit_Framework_MockObject_MockObject ->method('getEmail') ->willReturn(self::TEST_EMAIL); - $quoteMock->expects(self::once()) - ->method('setCustomerEmail') + $quoteMock->method('setCustomerEmail') ->with(self::TEST_EMAIL) ->willReturnSelf(); - $quoteMock->expects(self::once()) - ->method('setCustomerIsGuest') + $quoteMock->method('setCustomerIsGuest') ->with(true) ->willReturnSelf(); - $quoteMock->expects(self::once()) - ->method('setCustomerGroupId') + $quoteMock->method('setCustomerGroupId') ->with(Group::NOT_LOGGED_IN_ID) ->willReturnSelf(); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock - * @return Address|\PHPUnit_Framework_MockObject_MockObject + * Gets a mock object for a billing address entity. + * + * @param MockObject $quoteMock + * @return Address|MockObject */ - private function getBillingAddressMock(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function getBillingAddressMock(MockObject $quoteMock) { - if (!isset($this->billingAddressMock)) { - $this->billingAddressMock = $this->getMockBuilder(Address::class) + if (!isset($this->billingAddress)) { + $this->billingAddress = $this->getMockBuilder(Address::class) ->setMethods(['setShouldIgnoreValidation', 'getEmail', 'setSameAsBilling']) ->disableOriginalConstructor() ->getMock(); } - $quoteMock->expects(self::any()) - ->method('getBillingAddress') - ->willReturn($this->billingAddressMock); + $quoteMock->method('getBillingAddress') + ->willReturn($this->billingAddress); - return $this->billingAddressMock; + return $this->billingAddress; } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Gets a mock object for a quote. + * + * @return Quote|MockObject */ private function getQuoteMock() { diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php index 39863e6561c43..a2b5380d2884b 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php @@ -3,22 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Model\Paypal\Helper; +use Magento\Braintree\Gateway\Config\PayPal\Config; +use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; +use Magento\Braintree\Observer\DataAssignObserver; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartExtensionInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Payment; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; -use Magento\Braintree\Observer\DataAssignObserver; -use Magento\Braintree\Gateway\Config\PayPal\Config; -use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class QuoteUpdaterTest * - * @see \Magento\Braintree\Model\Paypal\Helper\QuoteUpdater - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase @@ -26,39 +28,42 @@ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase const TEST_NONCE = '3ede7045-2aea-463e-9754-cd658ffeeb48'; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ - private $configMock; + private $config; /** - * @var CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CartRepositoryInterface|MockObject */ - private $quoteRepositoryMock; + private $quoteRepository; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $billingAddressMock; + private $billingAddress; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $shippingAddressMock; + private $shippingAddress; /** * @var QuoteUpdater */ private $quoteUpdater; + /** + * @inheritdoc + */ protected function setUp() { - $this->configMock = $this->getMockBuilder(Config::class) + $this->config = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->quoteRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) + $this->quoteRepository = $this->getMockBuilder(CartRepositoryInterface::class) ->getMockForAbstractClass(); - $this->billingAddressMock = $this->getMockBuilder(Address::class) + $this->billingAddress = $this->getMockBuilder(Address::class) ->setMethods( [ 'setLastname', @@ -73,9 +78,10 @@ protected function setUp() 'setShouldIgnoreValidation', 'getEmail' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); - $this->shippingAddressMock = $this->getMockBuilder(Address::class) + $this->shippingAddress = $this->getMockBuilder(Address::class) ->setMethods( [ 'setLastname', @@ -89,54 +95,61 @@ protected function setUp() 'setPostcode', 'setShouldIgnoreValidation' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); $this->quoteUpdater = new QuoteUpdater( - $this->configMock, - $this->quoteRepositoryMock + $this->config, + $this->quoteRepository ); } - public function testExecute() + /** + * Checks if quote details can be update by the response from Braintree. + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testExecute(): void { $details = $this->getDetails(); - $quoteMock = $this->getQuoteMock(); - $paymentMock = $this->getPaymentMock(); + $quote = $this->getQuoteMock(); + $payment = $this->getPaymentMock(); - $quoteMock->expects(self::once()) - ->method('getPayment') - ->willReturn($paymentMock); + $quote->method('getPayment') + ->willReturn($payment); - $paymentMock->expects(self::once()) - ->method('setMethod') + $payment->method('setMethod') ->with(ConfigProvider::PAYPAL_CODE); - $paymentMock->expects(self::once()) - ->method('setAdditionalInformation') + $payment->method('setAdditionalInformation') ->with(DataAssignObserver::PAYMENT_METHOD_NONCE, self::TEST_NONCE); - $this->updateQuoteStep($quoteMock, $details); + $this->updateQuoteStep($quote, $details); - $this->quoteUpdater->execute(self::TEST_NONCE, $details, $quoteMock); + $this->quoteUpdater->execute(self::TEST_NONCE, $details, $quote); } - private function disabledQuoteAddressValidationStep() + /** + * Disables quote's addresses validation. + * + * @return void + */ + private function disabledQuoteAddressValidationStep(): void { - $this->billingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $this->billingAddress->method('setShouldIgnoreValidation') ->with(true); - $this->shippingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $this->shippingAddress->method('setShouldIgnoreValidation') ->with(true); - $this->billingAddressMock->expects(self::once()) - ->method('getEmail') + $this->billingAddress->method('getEmail') ->willReturn('bt_buyer_us@paypal.com'); } /** + * Gets quote's details. + * * @return array */ - private function getDetails() + private function getDetails(): array { return [ 'email' => 'bt_buyer_us@paypal.com', @@ -166,54 +179,51 @@ private function getDetails() } /** + * Updates shipping address details. + * * @param array $details */ - private function updateShippingAddressStep(array $details) + private function updateShippingAddressStep(array $details): void { - $this->shippingAddressMock->expects(self::once()) - ->method('setLastname') + $this->shippingAddress->method('setLastname') ->with($details['lastName']); - $this->shippingAddressMock->expects(self::once()) - ->method('setFirstname') + $this->shippingAddress->method('setFirstname') ->with($details['firstName']); - $this->shippingAddressMock->expects(self::once()) - ->method('setEmail') + $this->shippingAddress->method('setEmail') ->with($details['email']); - $this->shippingAddressMock->expects(self::once()) - ->method('setCollectShippingRates') + $this->shippingAddress->method('setCollectShippingRates') ->with(true); - $this->updateAddressDataStep($this->shippingAddressMock, $details['shippingAddress']); + $this->updateAddressDataStep($this->shippingAddress, $details['shippingAddress']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $addressMock + * Updates address details. + * + * @param MockObject $address * @param array $addressData */ - private function updateAddressDataStep(\PHPUnit_Framework_MockObject_MockObject $addressMock, array $addressData) + private function updateAddressDataStep(MockObject $address, array $addressData): void { - $addressMock->expects(self::once()) - ->method('setStreet') + $address->method('setStreet') ->with([$addressData['streetAddress'], $addressData['extendedAddress']]); - $addressMock->expects(self::once()) - ->method('setCity') + $address->method('setCity') ->with($addressData['locality']); - $addressMock->expects(self::once()) - ->method('setRegionCode') + $address->method('setRegionCode') ->with($addressData['region']); - $addressMock->expects(self::once()) - ->method('setCountryId') + $address->method('setCountryId') ->with($addressData['countryCodeAlpha2']); - $addressMock->expects(self::once()) - ->method('setPostcode') + $address->method('setPostcode') ->with($addressData['postalCode']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Updates quote's address details. + * + * @param MockObject $quoteMock * @param array $details */ - private function updateQuoteAddressStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock, array $details) + private function updateQuoteAddressStep(MockObject $quoteMock, array $details): void { $quoteMock->expects(self::exactly(2)) ->method('getIsVirtual') @@ -224,64 +234,61 @@ private function updateQuoteAddressStep(\PHPUnit_Framework_MockObject_MockObject } /** + * Updates billing address details. + * * @param array $details */ - private function updateBillingAddressStep(array $details) + private function updateBillingAddressStep(array $details): void { - $this->configMock->expects(self::once()) - ->method('isRequiredBillingAddress') + $this->config->method('isRequiredBillingAddress') ->willReturn(true); - $this->updateAddressDataStep($this->billingAddressMock, $details['billingAddress']); + $this->updateAddressDataStep($this->billingAddress, $details['billingAddress']); - $this->billingAddressMock->expects(self::once()) - ->method('setLastname') + $this->billingAddress->method('setLastname') ->with($details['lastName']); - $this->billingAddressMock->expects(self::once()) - ->method('setFirstname') + $this->billingAddress->method('setFirstname') ->with($details['firstName']); - $this->billingAddressMock->expects(self::once()) - ->method('setEmail') + $this->billingAddress->method('setEmail') ->with($details['email']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Updates quote details. + * + * @param MockObject $quote * @param array $details */ - private function updateQuoteStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock, array $details) + private function updateQuoteStep(MockObject $quote, array $details): void { - $quoteMock->expects(self::once()) - ->method('setMayEditShippingAddress') + $quote->method('setMayEditShippingAddress') ->with(false); - $quoteMock->expects(self::once()) - ->method('setMayEditShippingMethod') + $quote->method('setMayEditShippingMethod') ->with(true); - $quoteMock->expects(self::exactly(2)) - ->method('getShippingAddress') - ->willReturn($this->shippingAddressMock); - $quoteMock->expects(self::exactly(2)) + $quote->method('getShippingAddress') + ->willReturn($this->shippingAddress); + $quote->expects(self::exactly(2)) ->method('getBillingAddress') - ->willReturn($this->billingAddressMock); + ->willReturn($this->billingAddress); - $this->updateQuoteAddressStep($quoteMock, $details); + $this->updateQuoteAddressStep($quote, $details); $this->disabledQuoteAddressValidationStep(); - $quoteMock->expects(self::once()) - ->method('collectTotals'); + $quote->method('collectTotals'); - $this->quoteRepositoryMock->expects(self::once()) - ->method('save') - ->with($quoteMock); + $this->quoteRepository->method('save') + ->with($quote); } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Quote object. + * + * @return Quote|MockObject */ - private function getQuoteMock() + private function getQuoteMock(): MockObject { - return $this->getMockBuilder(Quote::class) + $quote = $this->getMockBuilder(Quote::class) ->setMethods( [ 'getIsVirtual', @@ -291,15 +298,29 @@ private function getQuoteMock() 'collectTotals', 'getShippingAddress', 'getBillingAddress', + 'getExtensionAttributes' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); + + $cartExtension = $this->getMockBuilder(CartExtensionInterface::class) + ->setMethods(['setShippingAssignments']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $quote->method('getExtensionAttributes') + ->willReturn($cartExtension); + + return $quote; } /** - * @return Payment|\PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Payment object. + * + * @return Payment|MockObject */ - private function getPaymentMock() + private function getPaymentMock(): MockObject { return $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php b/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php index 5c28b94ac9811..372415d3530c0 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php @@ -34,10 +34,9 @@ public function __get($name) { if (array_key_exists($name, $this->_attributes)) { return $this->_attributes[$name]; - } else { - trigger_error('Undefined property on ' . get_class($this) . ': ' . $name, E_USER_NOTICE); - return null; } + trigger_error('Undefined property on ' . get_class($this) . ': ' . $name, E_USER_NOTICE); + return null; } /** diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php index e43e67c18744f..f33af81b19746 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php @@ -6,10 +6,12 @@ namespace Magento\Braintree\Test\Unit\Model\Report; use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Braintree\Model\Report\FilterMapper; use Magento\Braintree\Model\Report\TransactionsCollection; use Magento\Framework\Api\Search\DocumentInterface; use Magento\Framework\Data\Collection\EntityFactoryInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class TransactionsCollectionTest @@ -19,22 +21,27 @@ class TransactionsCollectionTest extends \PHPUnit\Framework\TestCase { /** - * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject + * @var BraintreeAdapter|MockObject */ private $braintreeAdapterMock; /** - * @var EntityFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var BraintreeAdapterFactory|MockObject + */ + private $adapterFactoryMock; + + /** + * @var EntityFactoryInterface|MockObject */ private $entityFactoryMock; /** - * @var FilterMapper|\PHPUnit_Framework_MockObject_MockObject + * @var FilterMapper|MockObject */ private $filterMapperMock; /** - * @var DocumentInterface|\PHPUnit_Framework_MockObject_MockObject + * @var DocumentInterface|MockObject */ private $transactionMapMock; @@ -61,6 +68,11 @@ protected function setUp() ->setMethods(['search']) ->disableOriginalConstructor() ->getMock(); + $this->adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapterFactoryMock->method('create') + ->willReturn($this->braintreeAdapterMock); } /** @@ -82,7 +94,7 @@ public function testGetItems() $collection = new TransactionsCollection( $this->entityFactoryMock, - $this->braintreeAdapterMock, + $this->adapterFactoryMock, $this->filterMapperMock ); @@ -111,7 +123,7 @@ public function testGetItemsEmptyCollection() $collection = new TransactionsCollection( $this->entityFactoryMock, - $this->braintreeAdapterMock, + $this->adapterFactoryMock, $this->filterMapperMock ); @@ -125,7 +137,7 @@ public function testGetItemsEmptyCollection() */ public function testGetItemsWithLimit() { - $transations = range(1, TransactionsCollection::TRANSACTION_MAXIMUM_COUNT + 10); + $transactions = range(1, TransactionsCollection::TRANSACTION_MAXIMUM_COUNT + 10); $this->filterMapperMock->expects($this->once()) ->method('getFilter') @@ -133,7 +145,7 @@ public function testGetItemsWithLimit() $this->braintreeAdapterMock->expects($this->once()) ->method('search') - ->willReturn($transations); + ->willReturn($transactions); $this->entityFactoryMock->expects($this->exactly(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT)) ->method('create') @@ -141,7 +153,7 @@ public function testGetItemsWithLimit() $collection = new TransactionsCollection( $this->entityFactoryMock, - $this->braintreeAdapterMock, + $this->adapterFactoryMock, $this->filterMapperMock ); $collection->setPageSize(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT); @@ -157,7 +169,7 @@ public function testGetItemsWithLimit() */ public function testGetItemsWithNullLimit() { - $transations = range(1, TransactionsCollection::TRANSACTION_MAXIMUM_COUNT + 10); + $transactions = range(1, TransactionsCollection::TRANSACTION_MAXIMUM_COUNT + 10); $this->filterMapperMock->expects($this->once()) ->method('getFilter') @@ -165,7 +177,7 @@ public function testGetItemsWithNullLimit() $this->braintreeAdapterMock->expects($this->once()) ->method('search') - ->willReturn($transations); + ->willReturn($transactions); $this->entityFactoryMock->expects($this->exactly(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT)) ->method('create') @@ -173,7 +185,7 @@ public function testGetItemsWithNullLimit() $collection = new TransactionsCollection( $this->entityFactoryMock, - $this->braintreeAdapterMock, + $this->adapterFactoryMock, $this->filterMapperMock ); $collection->setPageSize(null); @@ -198,7 +210,7 @@ public function testAddToFilter($field, $condition, $filterMapperCall, $expected $collection = new TransactionsCollection( $this->entityFactoryMock, - $this->braintreeAdapterMock, + $this->adapterFactoryMock, $this->filterMapperMock ); diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php index 6c85ae68eb7af..24bc4eae960be 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php @@ -7,7 +7,9 @@ use Magento\Braintree\Gateway\Config\Config; use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; use Magento\Braintree\Model\Ui\ConfigProvider; +use Magento\Customer\Model\Session; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -31,6 +33,11 @@ class ConfigProviderTest extends \PHPUnit\Framework\TestCase */ private $braintreeAdapter; + /** + * @var Session|MockObject + */ + private $session; + /** * @var ConfigProvider */ @@ -45,10 +52,24 @@ protected function setUp() $this->braintreeAdapter = $this->getMockBuilder(BraintreeAdapter::class) ->disableOriginalConstructor() ->getMock(); + /** @var BraintreeAdapterFactory|MockObject $adapterFactoryMock */ + $adapterFactoryMock = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactoryMock->method('create') + ->willReturn($this->braintreeAdapter); + + $this->session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getStoreId']) + ->getMock(); + $this->session->method('getStoreId') + ->willReturn(null); $this->configProvider = new ConfigProvider( $this->config, - $this->braintreeAdapter + $adapterFactoryMock, + $this->session ); } diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php index 22f7f46bd98f1..42469fe0faf45 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php @@ -77,6 +77,9 @@ public function testGetConfig($expected) 'width' => 30, 'height' => 26, 'url' => 'https://icon.test.url' ]); + $this->config->method('isRequiredBillingAddress') + ->willReturn(1); + self::assertEquals($expected, $this->configProvider->getConfig()); } @@ -101,7 +104,8 @@ public function getConfigDataProvider() 'skipOrderReview' => false, 'paymentIcon' => [ 'width' => 30, 'height' => 26, 'url' => 'https://icon.test.url' - ] + ], + 'isRequiredBillingAddress' => true ] ] ] diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index c14addf550dfb..5af56a2afd3fe 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -5,32 +5,32 @@ "sort-packages": true }, "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", - "braintree/braintree_php": "3.22.0", - "magento/framework": "100.3.*", + "php": "~7.1.3||~7.2.0", + "braintree/braintree_php": "3.35.0", + "magento/framework": "*", "magento/magento-composer-installer": "*", - "magento/module-catalog": "101.2.*", - "magento/module-backend": "100.3.*", - "magento/module-checkout": "100.3.*", - "magento/module-config": "100.3.*", - "magento/module-customer": "100.3.*", - "magento/module-directory": "100.3.*", - "magento/module-instant-purchase": "100.3.*", - "magento/module-payment": "100.3.*", - "magento/module-paypal": "100.3.*", - "magento/module-quote": "100.3.*", - "magento/module-sales": "100.3.*", - "magento/module-ui": "100.3.*", - "magento/module-vault": "100.3.*" + "magento/module-catalog": "*", + "magento/module-backend": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-instant-purchase": "*", + "magento/module-payment": "*", + "magento/module-paypal": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-ui": "*", + "magento/module-vault": "*" }, "suggest": { - "magento/module-checkout-agreements": "100.3.*", - "magento/module-theme": "100.3.*" + "magento/module-checkout-agreements": "*", + "magento/module-theme": "*" }, "type": "magento2-module", - "version": "100.3.0-dev", "license": [ - "proprietary" + "OSL-3.0", + "AFL-3.0" ], "autoload": { "files": [ diff --git a/app/code/Magento/Braintree/etc/acl.xml b/app/code/Magento/Braintree/etc/acl.xml index a188586cc0a26..066ccc818d4e6 100644 --- a/app/code/Magento/Braintree/etc/acl.xml +++ b/app/code/Magento/Braintree/etc/acl.xml @@ -11,7 +11,16 @@ <resource id="Magento_Backend::admin"> <resource id="Magento_Reports::report"> <resource id="Magento_Reports::salesroot"> - <resource id="Magento_Braintree::settlement_report" title="Braintree Settlement" sortOrder="80" /> + <resource id="Magento_Braintree::settlement_report" title="Braintree Settlement" translate="title" sortOrder="80" /> + </resource> + </resource> + <resource id="Magento_Sales::sales"> + <resource id="Magento_Sales::sales_operation"> + <resource id="Magento_Sales::sales_order"> + <resource id="Magento_Sales::actions"> + <resource id="Magento_Braintree::get_client_token" title="Get Client Token Braintree" sortOrder="170" /> + </resource> + </resource> </resource> </resource> </resource> diff --git a/app/code/Magento/Braintree/etc/adminhtml/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/adminhtml/braintree_error_mapping.xml new file mode 100644 index 0000000000000..611f9372518fc --- /dev/null +++ b/app/code/Magento/Braintree/etc/adminhtml/braintree_error_mapping.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/error_mapping.xsd"> + <message_list> + <message code="81509" translate="true">Credit card type is not accepted by this merchant account.</message> + <message code="91504" translate="true">Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending.</message> + <message code="91505" translate="true">Credit transactions cannot be refunded.</message> + <message code="91506" translate="true">Cannot refund a transaction unless it is settled.</message> + <message code="91507" translate="true">Cannot submit for settlement unless status is authorized.</message> + <message code="91511" translate="true">Customer does not have any credit cards.</message> + <message code="91512" translate="true">Transaction has already been completely refunded.</message> + <message code="91517" translate="true">Payment instrument type is not accepted by this merchant account.</message> + <message code="91519" translate="true">Processor authorization code cannot be set unless for a voice authorization.</message> + <message code="91521" translate="true">Refund amount is too large.</message> + <message code="91522" translate="true">Settlement amount is too large.</message> + <message code="91530" translate="true">Cannot provide a billing address unless also providing a credit card.</message> + <message code="91538" translate="true">Cannot refund a transaction with a suspended merchant account.</message> + <message code="91547" translate="true">Merchant account does not support refunds.</message> + <message code="91574" translate="true">Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled.</message> + <message code="91576" translate="true">PayPal is not enabled for your merchant account.</message> + <message code="915102" translate="true">Partial settlements are not supported by this processor.</message> + <message code="915103" translate="true">Cannot submit for partial settlement.</message> + <message code="915148" translate="true">Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending.</message> + <message code="915149" translate="true">Too many concurrent attempts to refund this transaction. Try again later.</message> + <message code="915151" translate="true">Too many concurrent attempts to void this transaction. Try again later.</message> + </message_list> +</mapping> diff --git a/app/code/Magento/Braintree/etc/adminhtml/di.xml b/app/code/Magento/Braintree/etc/adminhtml/di.xml index d0469ded83b67..9de1ad48d2261 100644 --- a/app/code/Magento/Braintree/etc/adminhtml/di.xml +++ b/app/code/Magento/Braintree/etc/adminhtml/di.xml @@ -28,6 +28,8 @@ <item name="address" xsi:type="string">Magento\Braintree\Gateway\Request\AddressDataBuilder</item> <item name="vault" xsi:type="string">Magento\Braintree\Gateway\Request\VaultDataBuilder</item> <item name="dynamic_descriptor" xsi:type="string">Magento\Braintree\Gateway\Request\DescriptorDataBuilder</item> + <item name="store" xsi:type="string">Magento\Braintree\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder</item> </argument> </arguments> </virtualType> @@ -39,10 +41,25 @@ <item name="channel" xsi:type="string">Magento\Braintree\Gateway\Request\ChannelDataBuilder</item> <item name="address" xsi:type="string">Magento\Braintree\Gateway\Request\AddressDataBuilder</item> <item name="dynamic_descriptor" xsi:type="string">Magento\Braintree\Gateway\Request\DescriptorDataBuilder</item> + <item name="store" xsi:type="string">Magento\Braintree\Gateway\Request\StoreConfigBuilder</item> + <item name="merchant_account" xsi:type="string">Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder</item> </argument> </arguments> </virtualType> + <!-- Braintree commands --> + <type name="BraintreeVoidCommand"> + <arguments> + <argument name="errorMessageMapper" xsi:type="object">Magento\Braintree\Gateway\ErrorMapper\VirtualErrorMessageMapper</argument> + </arguments> + </type> + <type name="BraintreeRefundCommand"> + <arguments> + <argument name="errorMessageMapper" xsi:type="object">Magento\Braintree\Gateway\ErrorMapper\VirtualErrorMessageMapper</argument> + </arguments> + </type> + <!-- END Braintree commands --> + <type name="Magento\Vault\Model\Ui\Adminhtml\TokensConfigProvider"> <arguments> <argument name="tokenUiComponentProviders" xsi:type="array"> diff --git a/app/code/Magento/Braintree/etc/adminhtml/menu.xml b/app/code/Magento/Braintree/etc/adminhtml/menu.xml index 590d5b3dce008..ce4dd4844f3bc 100644 --- a/app/code/Magento/Braintree/etc/adminhtml/menu.xml +++ b/app/code/Magento/Braintree/etc/adminhtml/menu.xml @@ -10,6 +10,7 @@ <add id="Magento_Braintree::settlement_report" title="Braintree Settlement" + translate="title" module="Magento_Braintree" sortOrder="80" parent="Magento_Reports::report_salesroot" diff --git a/app/code/Magento/Braintree/etc/adminhtml/system.xml b/app/code/Magento/Braintree/etc/adminhtml/system.xml index f46da366b64a3..5215dbc00b7ef 100644 --- a/app/code/Magento/Braintree/etc/adminhtml/system.xml +++ b/app/code/Magento/Braintree/etc/adminhtml/system.xml @@ -44,7 +44,7 @@ <comment>http://docs.magento.com/m2/ce/user_guide/payment/braintree.html</comment> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> </group> - <group id="braintree_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="5"> + <group id="braintree_required" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="5"> <label>Basic Braintree Settings</label> <attribute type="expanded">1</attribute> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> @@ -77,25 +77,25 @@ <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> </field> </group> - <group id="braintree_advanced" translate="label" showInDefault="1" showInWebsite="1" sortOrder="20"> + <group id="braintree_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="0" sortOrder="20"> <label>Advanced Braintree Settings</label> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> <field id="braintree_cc_vault_title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Vault Title</label> <config_path>payment/braintree_cc_vault/title</config_path> </field> - <field id="merchant_account_id" translate="label" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="merchant_account_id" translate="label comment" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Merchant Account ID</label> <comment>If you don't specify the merchant account to use to process a transaction, Braintree will process it using your default merchant account.</comment> <config_path>payment/braintree/merchant_account_id</config_path> </field> - <field id="fraudprotection" translate="label" type="select" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="fraudprotection" translate="label comment" type="select" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Advanced Fraud Protection</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Be sure to Enable Advanced Fraud Protection in Your Braintree Account in Settings/Processing Section</comment> <config_path>payment/braintree/fraudprotection</config_path> </field> - <field id="kount_id" translate="label" sortOrder="35" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="kount_id" translate="label comment" sortOrder="35" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Kount Merchant ID</label> <comment><![CDATA[Used for direct fraud tool integration. Make sure you also contact <a href="mailto:accounts@braintreepayments.com">accounts@braintreepayments.com</a> to setup your Kount account.]]></comment> <depends> @@ -108,7 +108,7 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/braintree/debug</config_path> </field> - <field id="useccv" translate="label" type="select" sortOrder="150" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="useccv" translate="label comment" type="select" sortOrder="150" showInDefault="1" showInWebsite="1" showInStore="0"> <label>CVV Verification</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Be sure to Enable AVS and/or CVV in Your Braintree Account in Settings/Processing Section.</comment> @@ -125,7 +125,7 @@ <config_path>payment/braintree/sort_order</config_path> </field> </group> - <group id="braintree_country_specific" translate="label" showInDefault="1" showInWebsite="1" sortOrder="30"> + <group id="braintree_country_specific" translate="label" showInDefault="1" showInWebsite="1" showInStore="0" sortOrder="30"> <label>Country Specific Settings</label> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> <field id="allowspecific" translate="label" type="allowspecific" sortOrder="200" showInDefault="1" showInWebsite="1" showInStore="0"> @@ -146,10 +146,10 @@ <config_path>payment/braintree/countrycreditcard</config_path> </field> </group> - <group id="braintree_paypal" translate="label" showInDefault="1" showInWebsite="1" sortOrder="40"> + <group id="braintree_paypal" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> <label>PayPal through Braintree</label> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> - <field id="title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="title" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Title</label> <config_path>payment/braintree_paypal/title</config_path> <comment>It is recommended to set this value to "PayPal" per store views.</comment> @@ -187,7 +187,7 @@ <can_be_empty>1</can_be_empty> <config_path>payment/braintree_paypal/specificcountry</config_path> </field> - <field id="require_billing_address" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="require_billing_address" translate="label comment" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Require Customer's Billing Address</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/braintree_paypal/require_billing_address</config_path> @@ -203,7 +203,7 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/braintree_paypal/debug</config_path> </field> - <field id="display_on_shopping_cart" translate="label" type="select" sortOrder="120" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="display_on_shopping_cart" translate="label comment" type="select" sortOrder="120" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Display on Shopping Cart</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>payment/braintree_paypal/display_on_shopping_cart</config_path> @@ -215,7 +215,7 @@ <config_path>payment/braintree_paypal/skip_order_review</config_path> </field> </group> - <group id="braintree_3dsecure" translate="label" showInDefault="1" showInWebsite="1" sortOrder="41"> + <group id="braintree_3dsecure" translate="label" showInDefault="1" showInWebsite="1" showInStore="0" sortOrder="41"> <label>3D Secure Verification Settings</label> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> <field id="verify_3dsecure" translate="label" type="select" sortOrder="150" showInDefault="1" showInWebsite="1" showInStore="0"> @@ -239,14 +239,14 @@ <config_path>payment/braintree/verify_specific_countries</config_path> </field> </group> - <group id="braintree_dynamic_descriptor" translate="label" showInDefault="1" showInWebsite="1" sortOrder="50"> + <group id="braintree_dynamic_descriptor" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> <label>Dynamic Descriptors</label> <comment><![CDATA[Dynamic descriptors are sent on a per-transaction basis and define what will appear on your customers credit card statements for a specific purchase. The clearer the description of your product, the less likely customers will issue chargebacks due to confusion or non-recognition. Dynamic descriptors are not enabled on all accounts by default. If you receive a validation error of 92203 or if your dynamic descriptors are not displaying as expected, please <a href="mailto:support@getbraintree.com">Braintree Support</a>.]]></comment> <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> - <field id="name" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="name" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Name</label> <config_path>payment/braintree/descriptor_name</config_path> <comment> @@ -254,14 +254,14 @@ and the product descriptor can be up to 18, 14, or 9 characters respectively (with an * in between for a total descriptor name of 22 characters). </comment> </field> - <field id="phone" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="phone" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Phone</label> <config_path>payment/braintree/descriptor_phone</config_path> <comment> The value in the phone number field of a customer's statement. Phone must be 10-14 characters and can only contain numbers, dashes, parentheses and periods. </comment> </field> - <field id="url" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="url" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <label>URL</label> <config_path>payment/braintree/descriptor_url</config_path> <comment> diff --git a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml new file mode 100644 index 0000000000000..81da0a252e567 --- /dev/null +++ b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/error_mapping.xsd"> + <message_list> + <message code="81703" translate="true">Credit card type is not accepted by this merchant account.</message> + <message code="81706" translate="true">CVV is required.</message> + <message code="81707" translate="true">CVV must be 4 digits for American Express and 3 digits for other card types.</message> + <message code="81709" translate="true">Expiration date is required.</message> + <message code="81710" translate="true">Expiration date is invalid.</message> + <message code="81711" translate="true">Expiration year is invalid. It must be between 1975 and 2201.</message> + <message code="81712" translate="true">Expiration month is invalid.</message> + <message code="81713" translate="true">Expiration year is invalid.</message> + <message code="81714" translate="true">Credit card number is required.</message> + <message code="81715" translate="true">Credit card number is invalid.</message> + <message code="81716" translate="true">Credit card number must be 12-19 digits.</message> + <message code="81723" translate="true">Cardholder name is too long.</message> + <message code="81736" translate="true">CVV verification failed.</message> + <message code="81737" translate="true">Postal code verification failed.</message> + <message code="81750" translate="true">Credit card number is prohibited.</message> + <message code="81801" translate="true">Addresses must have at least one field filled in.</message> + <message code="81802" translate="true">Company is too long.</message> + <message code="81804" translate="true">Extended address is too long.</message> + <message code="81805" translate="true">First name is too long.</message> + <message code="81806" translate="true">Last name is too long.</message> + <message code="81807" translate="true">Locality is too long.</message> + <message code="81808" translate="true">Postal code is required.</message> + <message code="81809" translate="true">Postal code may contain no more than 9 letter or number characters.</message> + <message code="81810" translate="true">Region is too long.</message> + <message code="81811" translate="true">Street address is required.</message> + <message code="81812" translate="true">Street address is too long.</message> + <message code="81813" translate="true">Postal code can only contain letters, numbers, spaces, and hyphens.</message> + <message code="81827" translate="true">US state codes must be two characters to meet PayPal Seller Protection requirements.</message> + <message code="82901" translate="true">Incomplete PayPal account information.</message> + <message code="82903" translate="true">Invalid PayPal account information.</message> + <message code="82904" translate="true">PayPal Accounts are not accepted by this merchant account.</message> + <message code="91726" translate="true">Credit card type is not accepted by this merchant account.</message> + <message code="91734" translate="true">Credit card type is not accepted by this merchant account.</message> + <message code="91744" translate="true">Billing address format is invalid.</message> + <message code="91803" translate="true">Country name is not an accepted country.</message> + <message code="91814" translate="true">Country code is not accepted. Please contact the store administrator.</message> + <message code="91815" translate="true">Provided country information is inconsistent.</message> + <message code="91816" translate="true">Country code is not accepted. Please contact the store administrator.</message> + <message code="91817" translate="true">Country code is not accepted. Please contact the store administrator.</message> + <message code="91818" translate="true">Customer has already reached the maximum of 50 addresses.</message> + <message code="91819" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91820" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91821" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91822" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91823" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91824" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91825" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91826" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="91828" translate="true">Address is invalid. Please contact the store administrator.</message> + <message code="92910" translate="true">Error communicating with PayPal.</message> + <message code="92911" translate="true">PayPal authentication expired.</message> + <message code="92916" translate="true">Error executing PayPal order.</message> + <message code="92917" translate="true">Error executing PayPal billing agreement.</message> + </message_list> +</mapping> diff --git a/app/code/Magento/Braintree/etc/config.xml b/app/code/Magento/Braintree/etc/config.xml index 6bc2fb1735b52..a830c29368755 100644 --- a/app/code/Magento/Braintree/etc/config.xml +++ b/app/code/Magento/Braintree/etc/config.xml @@ -82,8 +82,7 @@ <title>Stored Accounts (Braintree PayPal) 1 - Magento\Braintree\Model\InstantPurchase\CreditCard\AvailabilityChecker - Magento\Braintree\Model\InstantPurchase\CreditCard\TokenFormatter + Magento\Braintree\Model\InstantPurchase\PayPal\TokenFormatter Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index 2a451e132eab0..b81513caf17a2 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -133,8 +133,8 @@ BraintreeVaultCaptureCommand BraintreeVoidCommand BraintreeRefundCommand - BraintreeVoidCommand - BraintreeVoidCommand + Magento\Braintree\Gateway\CancelCommand + Magento\Braintree\Gateway\CancelCommand @@ -150,7 +150,7 @@ BraintreeVaultCaptureCommand BraintreeVoidCommand BraintreeRefundCommand - BraintreeVoidCommand + Magento\Braintree\Gateway\CancelCommand @@ -193,6 +193,23 @@ + + + braintree_error_mapping.xml + + + + + Magento\Braintree\Gateway\ErrorMapper\VirtualConfigReader + braintree_error_mapper + + + + + Magento\Braintree\Gateway\ErrorMapper\VirtualMappingData + + + @@ -201,6 +218,7 @@ Magento\Braintree\Gateway\Http\Client\TransactionSale BraintreeAuthorizationHandler Magento\Braintree\Gateway\Validator\ResponseValidator + Magento\Braintree\Gateway\ErrorMapper\VirtualErrorMessageMapper @@ -214,6 +232,8 @@ Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder Magento\Braintree\Gateway\Request\KountPaymentDataBuilder Magento\Braintree\Gateway\Request\DescriptorDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder @@ -239,12 +259,14 @@ Magento\Braintree\Gateway\Http\Client\TransactionSubmitForSettlement Magento\Braintree\Gateway\Response\TransactionIdHandler Magento\Braintree\Gateway\Validator\ResponseValidator + Magento\Braintree\Gateway\ErrorMapper\VirtualErrorMessageMapper Magento\Braintree\Gateway\Request\CaptureDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder @@ -256,6 +278,7 @@ Magento\Braintree\Gateway\Http\Client\TransactionSale BraintreeVaultResponseHandler Magento\Braintree\Gateway\Validator\ResponseValidator + Magento\Braintree\Gateway\ErrorMapper\VirtualErrorMessageMapper @@ -268,6 +291,8 @@ Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder Magento\Braintree\Gateway\Request\KountPaymentDataBuilder Magento\Braintree\Gateway\Request\DescriptorDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder @@ -293,6 +318,7 @@ Magento\Braintree\Gateway\Http\Client\TransactionSale Magento\Braintree\Gateway\Response\TransactionIdHandler Magento\Braintree\Gateway\Validator\ResponseValidator + Magento\Braintree\Gateway\ErrorMapper\VirtualErrorMessageMapper @@ -300,6 +326,8 @@ Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder Magento\Braintree\Gateway\Request\SettlementDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder @@ -321,6 +349,8 @@ Magento\Braintree\Gateway\Request\PayPal\VaultDataBuilder Magento\Braintree\Gateway\Request\PayPal\DeviceDataBuilder Magento\Braintree\Gateway\Request\DescriptorDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder @@ -353,6 +383,8 @@ Magento\Braintree\Gateway\Request\ChannelDataBuilder Magento\Braintree\Gateway\Request\AddressDataBuilder Magento\Braintree\Gateway\Request\DescriptorDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder @@ -375,7 +407,7 @@ - Magento\Vault\Model\CreditCardTokenFactory + Magento\Vault\Api\Data\PaymentTokenFactoryInterface @@ -420,7 +452,7 @@ - Magento\Vault\Model\AccountPaymentTokenFactory + Magento\Vault\Api\Data\PaymentTokenFactoryInterface @@ -463,23 +495,49 @@ Magento\Braintree\Gateway\Http\Client\TransactionVoid - Magento\Braintree\Gateway\Request\VoidDataBuilder + BraintreeVoidRequestBuilder Magento\Braintree\Gateway\Response\VoidHandler Magento\Braintree\Gateway\Validator\GeneralResponseValidator Magento\Braintree\Gateway\Http\TransferFactory + + + + Magento\Braintree\Gateway\Request\VoidDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + + + + + + + + Magento\Braintree\Gateway\Response\CancelDetailsHandler + Magento\Braintree\Gateway\Validator\CancelResponseValidator + + + Magento\Braintree\Gateway\Http\Client\TransactionRefund - Magento\Braintree\Gateway\Request\RefundDataBuilder + BraintreeRefundBuilder Magento\Braintree\Gateway\Validator\GeneralResponseValidator Magento\Braintree\Gateway\Response\RefundHandler Magento\Braintree\Gateway\Http\TransferFactory + + + + Magento\Braintree\Gateway\Request\RefundDataBuilder + Magento\Braintree\Gateway\Request\StoreConfigBuilder + + + + @@ -494,7 +552,7 @@ - + @@ -566,4 +624,8 @@ + + + + diff --git a/app/code/Magento/Braintree/etc/module.xml b/app/code/Magento/Braintree/etc/module.xml index e3415c4935ff6..8be79268e7b58 100644 --- a/app/code/Magento/Braintree/etc/module.xml +++ b/app/code/Magento/Braintree/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Braintree/i18n/en_US.csv b/app/code/Magento/Braintree/i18n/en_US.csv index 116f459a1c1c8..e9145b35b56ef 100644 --- a/app/code/Magento/Braintree/i18n/en_US.csv +++ b/app/code/Magento/Braintree/i18n/en_US.csv @@ -129,3 +129,68 @@ Amount,Amount "Refund Ids","Refund Ids" "Settlement Batch ID","Settlement Batch ID" Currency,Currency +"Addresses must have at least one field filled in.","Addresses must have at least one field filled in." +"Company is too long.","Company is too long." +"Extended address is too long.","Extended address is too long." +"First name is too long.","First name is too long." +"Last name is too long.","Last name is too long." +"Locality is too long.","Locality is too long." +"Postal code can only contain letters, numbers, spaces, and hyphens.","Postal code can only contain letters, numbers, spaces, and hyphens." +"Postal code is required.","Postal code is required." +"Postal code may contain no more than 9 letter or number characters.","Postal code may contain no more than 9 letter or number characters." +"Region is too long.","Region is too long." +"Street address is required.","Street address is required." +"Street address is too long.","Street address is too long." +"US state codes must be two characters to meet PayPal Seller Protection requirements.","US state codes must be two characters to meet PayPal Seller Protection requirements." +"Country name is not an accepted country.","Country name is not an accepted country." +"Provided country information is inconsistent.","Provided country information is inconsistent." +"Country code is not accepted. Please contact the store administrator.","Country code is not accepted. Please contact the store administrator." +"Customer has already reached the maximum of 50 addresses.","Customer has already reached the maximum of 50 addresses." +"Address is invalid. Please contact the store administrator.","Address is invalid. Please contact the store administrator." +"Address is invalid.","Address is invalid." +"Billing address format is invalid.","Billing address format is invalid." +"Cardholder name is too long.","Cardholder name is too long." +"Credit card type is not accepted by this merchant account.","Credit card type is not accepted by this merchant account." +"CVV is required.","CVV is required." +"CVV must be 4 digits for American Express and 3 digits for other card types.","CVV must be 4 digits for American Express and 3 digits for other card types." +"Expiration date is required.","Expiration date is required." +"Expiration date is invalid.","Expiration date is invalid." +"Expiration year is invalid. It must be between 1975 and 2201.","Expiration year is invalid. It must be between 1975 and 2201." +"Expiration month is invalid.","Expiration month is invalid." +"Expiration year is invalid.","Expiration year is invalid." +"Credit card number is required.","Credit card number is required." +"Credit card number is invalid.","Credit card number is invalid." +"Credit card number must be 12-19 digits.","Credit card number must be 12-19 digits." +"CVV verification failed.","CVV verification failed." +"Postal code verification failed.","Postal code verification failed." +"Credit card number is prohibited.","Credit card number is prohibited." +"Incomplete PayPal account information.","Incomplete PayPal account information." +"Invalid PayPal account information.","Invalid PayPal account information." +"PayPal Accounts are not accepted by this merchant account.","PayPal Accounts are not accepted by this merchant account." +"Error communicating with PayPal.","Error communicating with PayPal." +"PayPal authentication expired.","PayPal authentication expired." +"Error executing PayPal order.","Error executing PayPal order." +"Error executing PayPal billing agreement.","Error executing PayPal billing agreement." +"Cannot provide a billing address unless also providing a credit card.","Cannot provide a billing address unless also providing a credit card." +"Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending.","Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending." +"Credit transactions cannot be refunded.","Credit transactions cannot be refunded." +"Cannot refund a transaction unless it is settled.","Cannot refund a transaction unless it is settled." +"Cannot submit for settlement unless status is authorized.","Cannot submit for settlement unless status is authorized." +"Customer does not have any credit cards.","Customer does not have any credit cards." +"Transaction has already been completely refunded.","Transaction has already been completely refunded." +"Payment instrument type is not accepted by this merchant account.","Payment instrument type is not accepted by this merchant account." +"Processor authorization code cannot be set unless for a voice authorization.","Processor authorization code cannot be set unless for a voice authorization." +"Refund amount is too large.","Refund amount is too large." +"Settlement amount is too large.","Settlement amount is too large." +"Cannot refund a transaction with a suspended merchant account.","Cannot refund a transaction with a suspended merchant account." +"Merchant account does not support refunds.","Merchant account does not support refunds." +"PayPal is not enabled for your merchant account.","PayPal is not enabled for your merchant account." +"Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled.","Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled." +"Cannot submit for partial settlement.","Cannot submit for partial settlement." +"Partial settlements are not supported by this processor.","Partial settlements are not supported by this processor." +"Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending.","Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending." +"Too many concurrent attempts to refund this transaction. Try again later.","Too many concurrent attempts to refund this transaction. Try again later." +"Too many concurrent attempts to void this transaction. Try again later.","Too many concurrent attempts to void this transaction. Try again later." +"Braintree Settlement","Braintree Settlement" +"The Payment Token is not available to perform the request.","The Payment Token is not available to perform the request." +"The order #%1 cannot be processed.","The order #%1 cannot be processed." diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml index 13249cd8e0e80..535a5a852fe70 100644 --- a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml @@ -84,7 +84,7 @@ $ccType = $block->getInfoData('cc_type'); name="payment[is_active_payment_token_enabler]" class="admin__control-checkbox"/> diff --git a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js index c8aaa65cebb71..ab01565d7f1e5 100644 --- a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js +++ b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js @@ -24,6 +24,7 @@ define([ scriptLoaded: false, braintree: null, selectedCardType: null, + checkout: null, imports: { onActiveChange: 'active' } @@ -116,7 +117,7 @@ define([ }, /** - * Setup Braintree SDK + * Retrieves client token and setup Braintree SDK */ initBraintree: function () { var self = this; @@ -124,35 +125,14 @@ define([ try { $('body').trigger('processStart'); - self.braintree.setup(self.clientToken, 'custom', { - id: self.selector, - hostedFields: self.getHostedFields(), - - /** - * Triggered when sdk was loaded - */ - onReady: function () { - $('body').trigger('processStop'); - }, - - /** - * Callback for success response - * @param {Object} response - */ - onPaymentMethodReceived: function (response) { - if (self.validateCardType()) { - self.setPaymentDetails(response.nonce); - self.placeOrder(); - } - }, + $.getJSON(self.clientTokenUrl).done(function (response) { + self.clientToken = response.clientToken; + self._initBraintree(); + }).fail(function (response) { + var failed = JSON.parse(response.responseText); - /** - * Error callback - * @param {Object} response - */ - onError: function (response) { - self.error(response.message); - } + $('body').trigger('processStop'); + self.error(failed.message); }); } catch (e) { $('body').trigger('processStop'); @@ -160,6 +140,54 @@ define([ } }, + /** + * Setup Braintree SDK + */ + _initBraintree: function () { + var self = this; + + this.disableEventListeners(); + + if (self.checkout) { + self.checkout.teardown(function () { + self.checkout = null; + }); + } + + self.braintree.setup(self.clientToken, 'custom', { + id: self.selector, + hostedFields: self.getHostedFields(), + + /** + * Triggered when sdk was loaded + */ + onReady: function (checkout) { + self.checkout = checkout; + $('body').trigger('processStop'); + self.enableEventListeners(); + }, + + /** + * Callback for success response + * @param {Object} response + */ + onPaymentMethodReceived: function (response) { + if (self.validateCardType()) { + self.setPaymentDetails(response.nonce); + self.placeOrder(); + } + }, + + /** + * Error callback + * @param {Object} response + */ + onError: function (response) { + self.error(response.message); + } + }); + }, + /** * Get hosted fields configuration * @returns {Object} @@ -211,6 +239,7 @@ define([ } if (event.type !== 'fieldStateChange') { + return false; } diff --git a/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml index ab294f8e797b7..c4152e1c3ebf9 100644 --- a/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml @@ -35,6 +35,9 @@ false + + true + diff --git a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml index 1d60d19458a28..c1ef461ecae7c 100644 --- a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml +++ b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml @@ -29,6 +29,7 @@ $config = [ class="action-braintree-paypal-logo" disabled> Pay with PayPal + alt="escapeHtml(__('Pay with PayPal')) ?>" + title="escapeHtml(__('Pay with PayPal')) ?>"/> diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js index 2834c0a683979..39bdf582c8cd7 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js @@ -79,6 +79,7 @@ define( */ onError: function (response) { braintree.showError($t('Payment ' + this.getTitle() + ' can\'t be initialized')); + this.isPlaceOrderActionAllowed(true); throw response.message; }, diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js index d2faac6ed792f..05c09abdb7b2e 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js @@ -61,7 +61,7 @@ define([ }, /** - * @returns {Bool} + * @returns {Boolean} */ isVaultEnabled: function () { return this.vaultEnabler.isVaultEnabled(); @@ -142,11 +142,20 @@ define([ return true; }, + /** + * Returns state of place order button + * @returns {Boolean} + */ + isButtonActive: function () { + return this.isActive() && this.isPlaceOrderActionAllowed(); + }, + /** * Trigger order placing */ placeOrderClick: function () { if (this.validateCardType()) { + this.isPlaceOrderActionAllowed(false); $(this.getSelector('submit')).trigger('click'); } }, diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index ca9d3686958b4..eaebd8492b0a1 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -103,6 +103,12 @@ define([ } }); + quote.shippingAddress.subscribe(function () { + if (self.isActive()) { + self.reInitPayPal(); + } + }); + // for each component initialization need update property this.isReviewRequired(false); this.initClientConfig(); @@ -204,7 +210,9 @@ define([ beforePlaceOrder: function (data) { this.setPaymentMethodNonce(data.nonce); - if (quote.billingAddress() === null && typeof data.details.billingAddress !== 'undefined') { + if ((this.isRequiredBillingAddress() || quote.billingAddress() === null) && + typeof data.details.billingAddress !== 'undefined' + ) { this.setBillingAddress(data.details, data.details.billingAddress); } @@ -228,6 +236,7 @@ define([ this.disableButton(); this.clientConfig.paypal.amount = this.grandTotalAmount; + this.clientConfig.paypal.shippingAddressOverride = this.getShippingAddress(); Braintree.setConfig(this.clientConfig); Braintree.setup(); @@ -249,6 +258,14 @@ define([ return window.checkoutConfig.payment[this.getCode()].isAllowShippingAddressOverride; }, + /** + * Is billing address required from PayPal side + * @returns {Boolean} + */ + isRequiredBillingAddress: function () { + return window.checkoutConfig.payment[this.getCode()].isRequiredBillingAddress; + }, + /** * Get configuration for PayPal * @returns {Object} @@ -403,14 +420,16 @@ define([ * Triggers when customer click "Continue to PayPal" button */ payWithPayPal: function () { - if (additionalValidators.validate()) { - try { - Braintree.checkout.paypal.initAuthFlow(); - } catch (e) { - this.messageContainer.addErrorMessage({ - message: $t('Payment ' + this.getTitle() + ' can\'t be initialized.') - }); - } + if (!additionalValidators.validate()) { + return; + } + + try { + Braintree.checkout.paypal.initAuthFlow(); + } catch (e) { + this.messageContainer.addErrorMessage({ + message: $t('Payment ' + this.getTitle() + ' can\'t be initialized.') + }); } }, diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/form.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/form.html index d30186c8fb309..819b06ca75788 100644 --- a/app/code/Magento/Braintree/view/frontend/web/template/payment/form.html +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/form.html @@ -141,8 +141,7 @@ data-bind=" click: placeOrderClick, attr: {title: $t('Place Order')}, - css: {disabled: !isPlaceOrderActionAllowed()}, - enable: isActive() + enable: isButtonActive() " disabled> diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/paypal.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/paypal.html index f5c8c15c8f3ba..e1f6a1b4c25ce 100644 --- a/app/code/Magento/Braintree/view/frontend/web/template/payment/paypal.html +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/paypal.html @@ -12,7 +12,7 @@ data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" />